From 9ff9b98961455a7a153dde9c85fe55008d624064 Mon Sep 17 00:00:00 2001 From: Matt Joiner Date: Tue, 20 Aug 2024 23:42:23 +1000 Subject: [PATCH 1/5] Upgrade bbolt via raft (#11507) --- go.mod | 5 +++-- go.sum | 5 +++++ op-conductor/consensus/raft.go | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index a637310641d5..b2b89d0920be 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/hashicorp/raft v1.7.0 - github.com/hashicorp/raft-boltdb v0.0.0-20231211162105-6c830fa4535e + github.com/hashicorp/raft-boltdb/v2 v2.3.0 github.com/holiman/uint256 v1.3.1 github.com/ipfs/go-datastore v0.6.0 github.com/ipfs/go-ds-leveldb v0.5.0 @@ -113,10 +113,10 @@ require ( github.com/hashicorp/go-bexpr v0.1.11 // indirect github.com/hashicorp/go-hclog v1.6.2 // indirect github.com/hashicorp/go-immutable-radix v1.0.0 // indirect - github.com/hashicorp/go-msgpack v0.5.5 // indirect github.com/hashicorp/go-msgpack/v2 v2.1.1 // indirect github.com/hashicorp/golang-lru v0.5.0 // indirect github.com/hashicorp/golang-lru/arc/v2 v2.0.7 // indirect + github.com/hashicorp/raft-boltdb v0.0.0-20231211162105-6c830fa4535e // indirect github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4 // indirect github.com/holiman/bloomfilter/v2 v2.0.3 // indirect github.com/huin/goupnp v1.3.0 // indirect @@ -214,6 +214,7 @@ require ( github.com/tyler-smith/go-bip39 v1.1.0 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect github.com/yusufpapurcu/wmi v1.2.3 // indirect + go.etcd.io/bbolt v1.3.5 // indirect go.uber.org/automaxprocs v1.5.2 // indirect go.uber.org/dig v1.17.1 // indirect go.uber.org/fx v1.21.1 // indirect diff --git a/go.sum b/go.sum index 4cdf8bf57b27..4266f01b06ff 100644 --- a/go.sum +++ b/go.sum @@ -339,6 +339,8 @@ github.com/hashicorp/raft v1.7.0 h1:4u24Qn6lQ6uwziM++UgsyiT64Q8GyRn43CV41qPiz1o= github.com/hashicorp/raft v1.7.0/go.mod h1:N1sKh6Vn47mrWvEArQgILTyng8GoDRNYlgKyK7PMjs0= github.com/hashicorp/raft-boltdb v0.0.0-20231211162105-6c830fa4535e h1:SK4y8oR4ZMHPvwVHryKI88kJPJda4UyWYvG5A6iEQxc= github.com/hashicorp/raft-boltdb v0.0.0-20231211162105-6c830fa4535e/go.mod h1:EMz/UIuG93P0MBeHh6CbXQAEe8ckVJLZjhD17lBzK5Q= +github.com/hashicorp/raft-boltdb/v2 v2.3.0 h1:fPpQR1iGEVYjZ2OELvUHX600VAK5qmdnDEv3eXOwZUA= +github.com/hashicorp/raft-boltdb/v2 v2.3.0/go.mod h1:YHukhB04ChJsLHLJEUD6vjFyLX2L3dsX3wPBZcX4tmc= github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4 h1:X4egAf/gcS1zATw6wn4Ej8vjuVGxeHdan+bRb2ebyv4= github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4/go.mod h1:5GuXa7vkL8u9FkFuWdVvfR5ix8hRB7DbOAaYULamFpc= github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= @@ -780,6 +782,8 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= +go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -918,6 +922,7 @@ golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/op-conductor/consensus/raft.go b/op-conductor/consensus/raft.go index 3ded0d3befac..c534d0305847 100644 --- a/op-conductor/consensus/raft.go +++ b/op-conductor/consensus/raft.go @@ -10,7 +10,7 @@ import ( "github.com/ethereum/go-ethereum/log" "github.com/hashicorp/raft" - boltdb "github.com/hashicorp/raft-boltdb" + boltdb "github.com/hashicorp/raft-boltdb/v2" "github.com/pkg/errors" "github.com/ethereum-optimism/optimism/op-node/rollup" From 872ff5dbfb4a5d90426faed9f3c3bae935337776 Mon Sep 17 00:00:00 2001 From: Inphi Date: Tue, 20 Aug 2024 10:29:17 -0400 Subject: [PATCH 2/5] deploy-config: Update mainnet prestate for granite HF (#11533) --- packages/contracts-bedrock/deploy-config/mainnet.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts-bedrock/deploy-config/mainnet.json b/packages/contracts-bedrock/deploy-config/mainnet.json index 7331bfce770c..807c12fe1939 100644 --- a/packages/contracts-bedrock/deploy-config/mainnet.json +++ b/packages/contracts-bedrock/deploy-config/mainnet.json @@ -42,7 +42,7 @@ "systemConfigStartBlock": 17422444, "requiredProtocolVersion": "0x0000000000000000000000000000000000000003000000010000000000000000", "recommendedProtocolVersion": "0x0000000000000000000000000000000000000003000000010000000000000000", - "faultGameAbsolutePrestate": "0x03617abec0b255dc7fc7a0513a2c2220140a1dcd7a1c8eca567659bd67e05cea", + "faultGameAbsolutePrestate": "0x03e806a2859a875267a563462a06d4d1d1b455a9efee959a46e21e54b6caf69a", "faultGameMaxDepth": 73, "faultGameClockExtension": 10800, "faultGameMaxClockDuration": 302400, From fb45215a3430f22225f8eae8519607ac29f50397 Mon Sep 17 00:00:00 2001 From: Michael Amadi Date: Tue, 20 Aug 2024 17:09:04 +0100 Subject: [PATCH 3/5] Add Interop Start timestamp to CrossL2Inbox (#11398) * add interop start timestamp to CrossL2Inbox with tests * change to function, add assertions, update tests * correct the preimage of interop start storage slot * rename custome error * require id timestamp must be > interopStartTime not >=, update tests to use realistic interopStartTime (non-zero) * bump CrossL2Inbox semver, run just semver-lock & just snapshots * add natspec for setInteropStart * update semver lock --- packages/contracts-bedrock/semver-lock.json | 4 +- .../snapshots/abi/CrossL2Inbox.json | 30 ++++ .../contracts-bedrock/src/L2/CrossL2Inbox.sol | 43 ++++- .../src/L2/ICrossL2Inbox.sol | 12 ++ .../test/L2/CrossL2Inbox.t.sol | 166 ++++++++++++++++-- 5 files changed, 233 insertions(+), 22 deletions(-) diff --git a/packages/contracts-bedrock/semver-lock.json b/packages/contracts-bedrock/semver-lock.json index 0f65889efa56..65d43f8b624c 100644 --- a/packages/contracts-bedrock/semver-lock.json +++ b/packages/contracts-bedrock/semver-lock.json @@ -68,8 +68,8 @@ "sourceCodeHash": "0x3a725791a0f5ed84dc46dcdae26f6170a759b2fe3dc360d704356d088b76cfd6" }, "src/L2/CrossL2Inbox.sol": { - "initCodeHash": "0x80124454d2127d5ff340b0ef048be6d5bf5984e84c75021b6a1ffa81703a2503", - "sourceCodeHash": "0xfb26fc80fbc7febdc91ac73ea91ceb479b238e0e81804a0a21192d78c261a755" + "initCodeHash": "0x071b53cd8cf0503af856c4ee0ee34643e85059d53c096453891225472e02abfa", + "sourceCodeHash": "0x3c78129b91d9f06afa4787d4b3039f45a3b22b3edf5155ed73d4f0c3ab33c6c8" }, "src/L2/ETHLiquidity.sol": { "initCodeHash": "0x98177562fca0de0dfea5313c9acefe2fdbd73dee5ce6c1232055601f208f0177", diff --git a/packages/contracts-bedrock/snapshots/abi/CrossL2Inbox.json b/packages/contracts-bedrock/snapshots/abi/CrossL2Inbox.json index db1a70423c69..7253ac21bdcb 100644 --- a/packages/contracts-bedrock/snapshots/abi/CrossL2Inbox.json +++ b/packages/contracts-bedrock/snapshots/abi/CrossL2Inbox.json @@ -75,6 +75,19 @@ "stateMutability": "payable", "type": "function" }, + { + "inputs": [], + "name": "interopStart", + "outputs": [ + { + "internalType": "uint256", + "name": "interopStart_", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "logIndex", @@ -101,6 +114,13 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "setInteropStart", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "timestamp", @@ -218,6 +238,11 @@ "name": "ExecutingMessage", "type": "event" }, + { + "inputs": [], + "name": "InteropStartAlreadySet", + "type": "error" + }, { "inputs": [], "name": "InvalidChainId", @@ -228,6 +253,11 @@ "name": "InvalidTimestamp", "type": "error" }, + { + "inputs": [], + "name": "NotDepositor", + "type": "error" + }, { "inputs": [], "name": "NotEntered", diff --git a/packages/contracts-bedrock/src/L2/CrossL2Inbox.sol b/packages/contracts-bedrock/src/L2/CrossL2Inbox.sol index 9c464df590e7..cb0ccfa7714d 100644 --- a/packages/contracts-bedrock/src/L2/CrossL2Inbox.sol +++ b/packages/contracts-bedrock/src/L2/CrossL2Inbox.sol @@ -17,6 +17,12 @@ interface IDependencySet { function isInDependencySet(uint256 _chainId) external view returns (bool); } +/// @notice Thrown when the caller is not DEPOSITOR_ACCOUNT when calling `setInteropStart()` +error NotDepositor(); + +/// @notice Thrown when attempting to set interop start when it's already set. +error InteropStartAlreadySet(); + /// @notice Thrown when a non-written transient storage slot is attempted to be read from. error NotEntered(); @@ -35,6 +41,10 @@ error TargetCallFailed(); /// @notice The CrossL2Inbox is responsible for executing a cross chain message on the destination /// chain. It is permissionless to execute a cross chain message on behalf of any user. contract CrossL2Inbox is ICrossL2Inbox, ISemver, TransientReentrancyAware { + /// @notice Storage slot that the interop start timestamp is stored at. + /// Equal to bytes32(uint256(keccak256("crossl2inbox.interopstart")) - 1) + bytes32 internal constant INTEROP_START_SLOT = 0x5c769ee0ee8887661922049dc52480bb60322d765161507707dd9b190af5c149; + /// @notice Transient storage slot that the origin for an Identifier is stored at. /// Equal to bytes32(uint256(keccak256("crossl2inbox.identifier.origin")) - 1) bytes32 internal constant ORIGIN_SLOT = 0xd2b7c5071ec59eb3ff0017d703a8ea513a7d0da4779b0dbefe845808c300c815; @@ -55,15 +65,42 @@ contract CrossL2Inbox is ICrossL2Inbox, ISemver, TransientReentrancyAware { /// Equal to bytes32(uint256(keccak256("crossl2inbox.identifier.chainid")) - 1) bytes32 internal constant CHAINID_SLOT = 0x6e0446e8b5098b8c8193f964f1b567ec3a2bdaeba33d36acb85c1f1d3f92d313; + /// @notice The address that represents the system caller responsible for L1 attributes + /// transactions. + address internal constant DEPOSITOR_ACCOUNT = 0xDeaDDEaDDeAdDeAdDEAdDEaddeAddEAdDEAd0001; + /// @notice Semantic version. - /// @custom:semver 1.0.0-beta.4 - string public constant version = "1.0.0-beta.4"; + /// @custom:semver 1.0.0-beta.5 + string public constant version = "1.0.0-beta.5"; /// @notice Emitted when a cross chain message is being executed. /// @param msgHash Hash of message payload being executed. /// @param id Encoded Identifier of the message. event ExecutingMessage(bytes32 indexed msgHash, Identifier id); + /// @notice Sets the Interop Start Timestamp for this chain. Can only be performed once and when the caller is the + /// DEPOSITOR_ACCOUNT. + function setInteropStart() external { + // Check that caller is the DEPOSITOR_ACCOUNT + if (msg.sender != DEPOSITOR_ACCOUNT) revert NotDepositor(); + + // Check that it has not been set already + if (interopStart() != 0) revert InteropStartAlreadySet(); + + // Set Interop Start to block.timestamp + assembly { + sstore(INTEROP_START_SLOT, timestamp()) + } + } + + /// @notice Returns the interop start timestamp. + /// @return interopStart_ interop start timestamp. + function interopStart() public view returns (uint256 interopStart_) { + assembly { + interopStart_ := sload(INTEROP_START_SLOT) + } + } + /// @notice Returns the origin address of the Identifier. If not entered, reverts. /// @return Origin address of the Identifier. function origin() external view notEntered returns (address) { @@ -140,7 +177,7 @@ contract CrossL2Inbox is ICrossL2Inbox, ISemver, TransientReentrancyAware { /// is in the destination chain's dependency set. /// @param _id Identifier of the message. function _checkIdentifier(Identifier calldata _id) internal view { - if (_id.timestamp > block.timestamp) revert InvalidTimestamp(); + if (_id.timestamp > block.timestamp || _id.timestamp <= interopStart()) revert InvalidTimestamp(); if (!IDependencySet(Predeploys.L1_BLOCK_ATTRIBUTES).isInDependencySet(_id.chainId)) { revert InvalidChainId(); } diff --git a/packages/contracts-bedrock/src/L2/ICrossL2Inbox.sol b/packages/contracts-bedrock/src/L2/ICrossL2Inbox.sol index 25cc116c1da0..3d8fa8a471a8 100644 --- a/packages/contracts-bedrock/src/L2/ICrossL2Inbox.sol +++ b/packages/contracts-bedrock/src/L2/ICrossL2Inbox.sol @@ -13,6 +13,10 @@ interface ICrossL2Inbox { uint256 chainId; } + /// @notice Returns the interop start timestamp. + /// @return interopStart_ interop start timestamp. + function interopStart() external view returns (uint256 interopStart_); + /// @notice Returns the origin address of the Identifier. /// @return _origin The origin address of the Identifier. function origin() external view returns (address _origin); @@ -44,4 +48,12 @@ interface ICrossL2Inbox { ) external payable; + + /// @notice Validates a cross chain message on the destination chain + /// and emits an ExecutingMessage event. This function is useful + /// for applications that understand the schema of the _message payload and want to + /// process it in a custom way. + /// @param _id Identifier of the message. + /// @param _msgHash Hash of the message payload to call target with. + function validateMessage(Identifier calldata _id, bytes32 _msgHash) external; } diff --git a/packages/contracts-bedrock/test/L2/CrossL2Inbox.t.sol b/packages/contracts-bedrock/test/L2/CrossL2Inbox.t.sol index c126b596ebc2..12b24ae1d479 100644 --- a/packages/contracts-bedrock/test/L2/CrossL2Inbox.t.sol +++ b/packages/contracts-bedrock/test/L2/CrossL2Inbox.t.sol @@ -9,7 +9,15 @@ import { Predeploys } from "src/libraries/Predeploys.sol"; import { TransientContext } from "src/libraries/TransientContext.sol"; // Target contracts -import { CrossL2Inbox, NotEntered, InvalidTimestamp, InvalidChainId, TargetCallFailed } from "src/L2/CrossL2Inbox.sol"; +import { + CrossL2Inbox, + NotEntered, + InvalidTimestamp, + InvalidChainId, + TargetCallFailed, + NotDepositor, + InteropStartAlreadySet +} from "src/L2/CrossL2Inbox.sol"; import { ICrossL2Inbox } from "src/L2/ICrossL2Inbox.sol"; /// @title CrossL2InboxWithModifiableTransientStorage @@ -58,9 +66,20 @@ contract CrossL2InboxTest is Test { /// @dev Selector for the `isInDependencySet` method of the L1Block contract. bytes4 constant L1BlockIsInDependencySetSelector = bytes4(keccak256("isInDependencySet(uint256)")); + /// @dev Storage slot that the interop start timestamp is stored at. + /// Equal to bytes32(uint256(keccak256("crossl2inbox.interopstart")) - 1) + bytes32 internal constant INTEROP_START_SLOT = bytes32(uint256(keccak256("crossl2inbox.interopstart")) - 1); + /// @dev CrossL2Inbox contract instance. CrossL2Inbox crossL2Inbox; + // interop start timestamp + uint256 interopStartTime = 420; + + /// @dev The address that represents the system caller responsible for L1 attributes + /// transactions. + address internal constant DEPOSITOR_ACCOUNT = 0xDeaDDEaDDeAdDeAdDEAdDEaddeAddEAdDEAd0001; + /// @dev Sets up the test suite. function setUp() public { // Deploy the L2ToL2CrossDomainMessenger contract @@ -68,6 +87,56 @@ contract CrossL2InboxTest is Test { crossL2Inbox = CrossL2Inbox(Predeploys.CROSS_L2_INBOX); } + modifier setInteropStart() { + // Set interop start + vm.store(address(crossL2Inbox), INTEROP_START_SLOT, bytes32(interopStartTime)); + + // Set timestamp to be after interop start + vm.warp(interopStartTime + 1 hours); + + _; + } + + /// @dev Tests that the setInteropStart function updates the INTEROP_START_SLOT storage slot correctly + function testFuzz_setInteropStart_succeeds(uint256 time) external { + // Jump to time. + vm.warp(time); + + // Impersonate the depositor account. + vm.prank(DEPOSITOR_ACCOUNT); + + // Set interop start. + crossL2Inbox.setInteropStart(); + + // Check that the storage slot was set correctly and the public getter function returns the right value. + assertEq(crossL2Inbox.interopStart(), time); + assertEq(uint256(vm.load(address(crossL2Inbox), INTEROP_START_SLOT)), time); + } + + /// @dev Tests that the setInteropStart function reverts when the caller is not the DEPOSITOR_ACCOUNT. + function test_setInteropStart_notDepositorAccount_reverts() external { + // Expect revert with OnlyDepositorAccount selector + vm.expectRevert(NotDepositor.selector); + + // Call setInteropStart function + crossL2Inbox.setInteropStart(); + } + + /// @dev Tests that the setInteropStart function reverts if called when already set + function test_setInteropStart_interopStartAlreadySet_reverts() external { + // Impersonate the depositor account. + vm.startPrank(DEPOSITOR_ACCOUNT); + + // Call setInteropStart function + crossL2Inbox.setInteropStart(); + + // Expect revert with InteropStartAlreadySet selector if called a second time + vm.expectRevert(InteropStartAlreadySet.selector); + + // Call setInteropStart function again + crossL2Inbox.setInteropStart(); + } + /// @dev Tests that the `executeMessage` function succeeds. function testFuzz_executeMessage_succeeds( ICrossL2Inbox.Identifier memory _id, @@ -77,9 +146,11 @@ contract CrossL2InboxTest is Test { ) external payable + setInteropStart { - // Ensure that the id's timestamp is valid (less than or equal to the current block timestamp) - _id.timestamp = bound(_id.timestamp, 0, block.timestamp); + // Ensure that the id's timestamp is valid (less than or equal to the current block timestamp and greater than + // interop start time) + _id.timestamp = bound(_id.timestamp, interopStartTime + 1, block.timestamp); // Ensure that the target call is payable if value is sent if (_value > 0) assumePayable(_target); @@ -132,10 +203,12 @@ contract CrossL2InboxTest is Test { ) external payable + setInteropStart { - // Ensure that the ids' timestamp are valid (less than or equal to the current block timestamp) - _id1.timestamp = bound(_id1.timestamp, 0, block.timestamp); - _id2.timestamp = bound(_id2.timestamp, 0, block.timestamp); + // Ensure that the ids' timestamp are valid (less than or equal to the current block timestamp and greater than + // interop start time) + _id1.timestamp = bound(_id1.timestamp, interopStartTime + 1, block.timestamp); + _id2.timestamp = bound(_id2.timestamp, interopStartTime + 1, block.timestamp); // Ensure that id1's chain ID is in the dependency set vm.mockCall({ @@ -189,6 +262,7 @@ contract CrossL2InboxTest is Test { uint256 _value ) external + setInteropStart { // Ensure that the id's timestamp is invalid (greater than the current block timestamp) vm.assume(_id.timestamp > block.timestamp); @@ -203,6 +277,30 @@ contract CrossL2InboxTest is Test { crossL2Inbox.executeMessage{ value: _value }({ _id: _id, _target: _target, _message: _message }); } + /// @dev Tests that the `executeMessage` function reverts when called with an identifier with a timestamp earlier + /// than INTEROP_START timestamp + function testFuzz_executeMessage_invalidTimestamp_interopStart_reverts( + ICrossL2Inbox.Identifier memory _id, + address _target, + bytes calldata _message, + uint256 _value + ) + external + setInteropStart + { + // Ensure that the id's timestamp is invalid (less than or equal to interopStartTime) + _id.timestamp = bound(_id.timestamp, 0, crossL2Inbox.interopStart()); + + // Ensure that the contract has enough balance to send with value + vm.deal(address(this), _value); + + // Expect a revert with the InvalidTimestamp selector + vm.expectRevert(InvalidTimestamp.selector); + + // Call the executeMessage function + crossL2Inbox.executeMessage{ value: _value }({ _id: _id, _target: _target, _message: _message }); + } + /// @dev Tests that the `executeMessage` function reverts when called with an identifier with a chain ID not in /// dependency set. function testFuzz_executeMessage_invalidChainId_reverts( @@ -212,9 +310,11 @@ contract CrossL2InboxTest is Test { uint256 _value ) external + setInteropStart { - // Ensure that the id's timestamp is valid (less than or equal to the current block timestamp) - _id.timestamp = bound(_id.timestamp, 0, block.timestamp); + // Ensure that the id's timestamp is valid (less than or equal to the current block timestamp and greater than + // interop start time) + _id.timestamp = bound(_id.timestamp, interopStartTime + 1, block.timestamp); // Ensure that the chain ID is NOT in the dependency set vm.mockCall({ @@ -241,9 +341,11 @@ contract CrossL2InboxTest is Test { uint256 _value ) external + setInteropStart { - // Ensure that the id's timestamp is valid (less than or equal to the current block timestamp) - _id.timestamp = bound(_id.timestamp, 0, block.timestamp); + // Ensure that the id's timestamp is valid (less than or equal to the current block timestamp and greater than + // interop start time) + _id.timestamp = bound(_id.timestamp, interopStartTime + 1, block.timestamp); // Ensure that the target call is payable if value is sent if (_value > 0) assumePayable(_target); @@ -271,9 +373,16 @@ contract CrossL2InboxTest is Test { crossL2Inbox.executeMessage{ value: _value }({ _id: _id, _target: _target, _message: _message }); } - function testFuzz_validateMessage_succeeds(ICrossL2Inbox.Identifier memory _id, bytes32 _messageHash) external { - // Ensure that the id's timestamp is valid (less than or equal to the current block timestamp) - _id.timestamp = bound(_id.timestamp, 1, block.timestamp); + function testFuzz_validateMessage_succeeds( + ICrossL2Inbox.Identifier memory _id, + bytes32 _messageHash + ) + external + setInteropStart + { + // Ensure that the id's timestamp is valid (less than or equal to the current block timestamp and greater than + // interop start time) + _id.timestamp = bound(_id.timestamp, interopStartTime + 1, block.timestamp); // Ensure that the chain ID is in the dependency set vm.mockCall({ @@ -290,14 +399,16 @@ contract CrossL2InboxTest is Test { crossL2Inbox.validateMessage(_id, _messageHash); } - /// @dev Tests that the `validateMessage` function reverts when called with an identifier with an invalid timestamp. + /// @dev Tests that the `validateMessage` function reverts when called with an identifier with a timestamp later + /// than current block.timestamp. function testFuzz_validateMessage_invalidTimestamp_reverts( ICrossL2Inbox.Identifier calldata _id, bytes32 _messageHash ) external + setInteropStart { - // Ensure that the id's timestamp is invalid (greater thsan the current block timestamp) + // Ensure that the id's timestamp is invalid (greater than the current block timestamp) vm.assume(_id.timestamp > block.timestamp); // Expect a revert with the InvalidTimestamp selector @@ -307,6 +418,25 @@ contract CrossL2InboxTest is Test { crossL2Inbox.validateMessage(_id, _messageHash); } + /// @dev Tests that the `validateMessage` function reverts when called with an identifier with a timestamp earlier + /// than INTEROP_START timestamp + function testFuzz_validateMessage_invalidTimestamp_interopStart_reverts( + ICrossL2Inbox.Identifier memory _id, + bytes32 _messageHash + ) + external + setInteropStart + { + // Ensure that the id's timestamp is invalid (less than or equal to interopStartTime) + _id.timestamp = bound(_id.timestamp, 0, crossL2Inbox.interopStart()); + + // Expect a revert with the InvalidTimestamp selector + vm.expectRevert(InvalidTimestamp.selector); + + // Call the validateMessage function + crossL2Inbox.validateMessage(_id, _messageHash); + } + /// @dev Tests that the `validateMessage` function reverts when called with an identifier with a chain ID not in the /// dependency set. function testFuzz_validateMessage_invalidChainId_reverts( @@ -314,9 +444,11 @@ contract CrossL2InboxTest is Test { bytes32 _messageHash ) external + setInteropStart { - // Ensure that the timestamp is valid (less than or equal to the current block timestamp) - _id.timestamp = bound(_id.timestamp, 0, block.timestamp); + // Ensure that the timestamp is valid (less than or equal to the current block timestamp and greater than + // interopStartTime) + _id.timestamp = bound(_id.timestamp, interopStartTime + 1, block.timestamp); // Ensure that the chain ID is NOT in the dependency set. vm.mockCall({ From 933abde480bf3c6b28c8d20f0d0c6bd52364293b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 20 Aug 2024 13:58:35 -0600 Subject: [PATCH 4/5] dependabot(gomod): bump github.com/prometheus/client_golang (#11540) Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.20.0 to 1.20.1. - [Release notes](https://github.com/prometheus/client_golang/releases) - [Changelog](https://github.com/prometheus/client_golang/blob/v1.20.1/CHANGELOG.md) - [Commits](https://github.com/prometheus/client_golang/compare/v1.20.0...v1.20.1) --- updated-dependencies: - dependency-name: github.com/prometheus/client_golang dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index b2b89d0920be..89aa8a778e56 100644 --- a/go.mod +++ b/go.mod @@ -39,7 +39,7 @@ require ( github.com/onsi/gomega v1.34.1 github.com/pkg/errors v0.9.1 github.com/pkg/profile v1.7.0 - github.com/prometheus/client_golang v1.20.0 + github.com/prometheus/client_golang v1.20.1 github.com/protolambda/ctxlock v0.1.0 github.com/stretchr/testify v1.9.0 github.com/urfave/cli/v2 v2.27.4 diff --git a/go.sum b/go.sum index 4266f01b06ff..f520e1be29c5 100644 --- a/go.sum +++ b/go.sum @@ -644,8 +644,8 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= -github.com/prometheus/client_golang v1.20.0 h1:jBzTZ7B099Rg24tny+qngoynol8LtVYlA2bqx3vEloI= -github.com/prometheus/client_golang v1.20.0/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_golang v1.20.1 h1:IMJXHOD6eARkQpxo8KkhgEVFlBNm+nkrFUyGlIu7Na8= +github.com/prometheus/client_golang v1.20.1/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= From 185fb80d78aab989af43d224898527ea394c1eaa Mon Sep 17 00:00:00 2001 From: Matt Solomon Date: Tue, 20 Aug 2024 14:42:49 -0700 Subject: [PATCH 5/5] OPSM: Deploy Superchain, alternate approach (#11480) * feat: initial DeloySuperchain script * chore: scaffold file-based interfaces * test: additional assertions * chore: appease semgrep * scaffold alternate approach * incorporate feedback * refactor based on feedback * fix tests * test: more robust testing * refactor: dedupe etching of IO contracts and add getter method --- .../scripts/DeploySuperchain.s.sol | 383 ++++++++++++++++++ .../test/DeploySuperchain.t.sol | 107 +++++ 2 files changed, 490 insertions(+) create mode 100644 packages/contracts-bedrock/scripts/DeploySuperchain.s.sol create mode 100644 packages/contracts-bedrock/test/DeploySuperchain.t.sol diff --git a/packages/contracts-bedrock/scripts/DeploySuperchain.s.sol b/packages/contracts-bedrock/scripts/DeploySuperchain.s.sol new file mode 100644 index 000000000000..88fe3c0175cc --- /dev/null +++ b/packages/contracts-bedrock/scripts/DeploySuperchain.s.sol @@ -0,0 +1,383 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import { Script } from "forge-std/Script.sol"; +import { LibString } from "@solady/utils/LibString.sol"; + +import { SuperchainConfig } from "src/L1/SuperchainConfig.sol"; +import { ProtocolVersions, ProtocolVersion } from "src/L1/ProtocolVersions.sol"; +import { ProxyAdmin } from "src/universal/ProxyAdmin.sol"; +import { Proxy } from "src/universal/Proxy.sol"; + +/** + * This comment block defines the requirements and rationale for the architecture used in this forge + * script, along with other scripts that are being written as new Superchain-first deploy scripts to + * complement the OP Stack Manager. The script architecture is a bit different than a standard forge + * deployment script. + * + * There are three categories of users that are expected to interact with the scripts: + * 1. End users that want to run live contract deployments. + * 2. Solidity developers that want to use or test these script in a standard forge test environment. + * 3. Go developers that want to run the deploy scripts as part of e2e testing with other aspects of the OP Stack. + * + * We want each user to interact with the scripts in the way that's simplest for their use case: + * 1. End users: TOML input files that define config, and TOML output files with all output data. + * 2. Solidity developers: Direct calls to the script with input structs and output structs. + * 3. Go developers: The forge scripts can be executed directly in Go. + * + * The following architecture is used to meet the requirements of each user. We use this file's + * `DeploySuperchain` script as an example, but it applies to other scripts as well. + * + * This `DeploySuperchain.s.sol` file contains three contracts: + * 1. `DeploySuperchainInput`: Responsible for parsing, storing, and exposing the input data. + * 2. `DeploySuperchainOutput`: Responsible for storing and exposing the output data. + * 3. `DeploySuperchain`: The core script that executes the deployment. It reads inputs from the + * input contract, and writes outputs to the output contract. + * + * Because the core script performs calls to the input and output contracts, Go developers can + * intercept calls to these addresses (analogous to how forge intercepts calls to the `Vm` address + * to execute cheatcodes), to avoid the need for file I/O or hardcoding the input/output structs. + * + * Public getter methods on the input and output contracts allow individual fields to be accessed + * in a strong, type-safe manner (as opposed to a single struct getter where the caller may + * inadvertently transpose two addresses, for example). + * + * Each deployment step in the core deploy script is modularized into its own function that performs + * the deploy and sets the output on the Output contract, allowing for easy composition and testing + * of deployment steps. The output setter methods requires keying off the four-byte selector of the + * each output field's getter method, ensuring that the output is set for the correct field and + * minimizing the amount of boilerplate needed for each output field. + * + * This script doubles as a reference for documenting the pattern used and therefore contains + * comments explaining the patterns used. Other scripts are not expected to have this level of + * documentation. + * + * Additionally, we intentionally use "Input" and "Output" terminology to clearly distinguish these + * scripts from the existing ones that "Config" and "Artifacts" terminology. + */ +contract DeploySuperchainInput { + // The input struct contains all the input data required for the deployment. + struct Input { + Roles roles; + bool paused; + ProtocolVersion requiredProtocolVersion; + ProtocolVersion recommendedProtocolVersion; + } + + struct Roles { + address proxyAdminOwner; + address protocolVersionsOwner; + address guardian; + } + + // This flag tells us if all inputs have been set. An `input()` getter method that returns all + // inputs reverts if this flag is false. This ensures the deploy script cannot proceed until all + // inputs are validated and set. + bool public inputSet = false; + + // The full input struct is kept in storage. It is not exposed because the return type would be + // a tuple, but it's more convenient for the return type to be the struct itself. Therefore the + // struct is exposed via the `input()` getter method. + Input internal inputs; + + // And each field is exposed via it's own getter method. We can equivalently remove these + // storage variables and add getter methods that return the input struct fields directly, but + // that is more verbose with more boilerplate, especially for larger scripts with many inputs. + // Unlike the `input()` getter, these getters do not revert if the input is not set. The caller + // should check the `inputSet` value before calling any of these getters. + address public proxyAdminOwner; + address public protocolVersionsOwner; + address public guardian; + bool public paused; + ProtocolVersion public requiredProtocolVersion; + ProtocolVersion public recommendedProtocolVersion; + + // Load the input from a TOML file. + function loadInputFile(string memory _infile) public { + _infile; + Input memory parsedInput; + loadInput(parsedInput); + require(false, "DeploySuperchainInput: loadInput is not implemented"); + } + + // Load the input from a struct. + function loadInput(Input memory _input) public { + // As a defensive measure, we only allow inputs to be set once. + require(!inputSet, "DeploySuperchainInput: Input already set"); + + // All assertions on inputs happen here. You cannot set any inputs in Solidity unless + // they're all valid. For Go testing, the input and outputs + require(_input.roles.proxyAdminOwner != address(0), "DeploySuperchainInput: Null proxyAdminOwner"); + require(_input.roles.protocolVersionsOwner != address(0), "DeploySuperchainInput: Null protocolVersionsOwner"); + require(_input.roles.guardian != address(0), "DeploySuperchainInput: Null guardian"); + + // We now set all values in storage. + inputSet = true; + inputs = _input; + + proxyAdminOwner = _input.roles.proxyAdminOwner; + protocolVersionsOwner = _input.roles.protocolVersionsOwner; + guardian = _input.roles.guardian; + paused = _input.paused; + requiredProtocolVersion = _input.requiredProtocolVersion; + recommendedProtocolVersion = _input.recommendedProtocolVersion; + } + + function input() public view returns (Input memory) { + require(inputSet, "DeploySuperchainInput: Input not set"); + return inputs; + } +} + +contract DeploySuperchainOutput { + // The output struct contains all the output data from the deployment. + struct Output { + ProxyAdmin superchainProxyAdmin; + SuperchainConfig superchainConfigImpl; + SuperchainConfig superchainConfigProxy; + ProtocolVersions protocolVersionsImpl; + ProtocolVersions protocolVersionsProxy; + } + + // We use a similar pattern as the input contract to expose outputs. Because outputs are set + // individually, and deployment steps are modular and composable, we do not have an equivalent + // to the overall `input` and `inputSet` variables. + ProxyAdmin public superchainProxyAdmin; + SuperchainConfig public superchainConfigImpl; + SuperchainConfig public superchainConfigProxy; + ProtocolVersions public protocolVersionsImpl; + ProtocolVersions public protocolVersionsProxy; + + // This method lets each field be set individually. The selector of an output's getter method + // is used to determine which field to set. + function set(bytes4 sel, address _address) public { + if (sel == this.superchainProxyAdmin.selector) superchainProxyAdmin = ProxyAdmin(_address); + else if (sel == this.superchainConfigImpl.selector) superchainConfigImpl = SuperchainConfig(_address); + else if (sel == this.superchainConfigProxy.selector) superchainConfigProxy = SuperchainConfig(_address); + else if (sel == this.protocolVersionsImpl.selector) protocolVersionsImpl = ProtocolVersions(_address); + else if (sel == this.protocolVersionsProxy.selector) protocolVersionsProxy = ProtocolVersions(_address); + else revert("DeploySuperchainOutput: Unknown selector"); + } + + // Save the output to a TOML file. + function writeOutputFile(string memory _outfile) public pure { + _outfile; + require(false, "DeploySuperchainOutput: saveOutput not implemented"); + } + + function output() public view returns (Output memory) { + return Output({ + superchainProxyAdmin: superchainProxyAdmin, + superchainConfigImpl: superchainConfigImpl, + superchainConfigProxy: superchainConfigProxy, + protocolVersionsImpl: protocolVersionsImpl, + protocolVersionsProxy: protocolVersionsProxy + }); + } + + function checkOutput() public view { + // Assert that all addresses are non-zero and have code. + // We use LibString to avoid the need for adding cheatcodes to this contract. + address[] memory addresses = new address[](5); + addresses[0] = address(superchainProxyAdmin); + addresses[1] = address(superchainConfigImpl); + addresses[2] = address(superchainConfigProxy); + addresses[3] = address(protocolVersionsImpl); + addresses[4] = address(protocolVersionsProxy); + + for (uint256 i = 0; i < addresses.length; i++) { + address who = addresses[i]; + require(who != address(0), string.concat("check failed: zero address at index ", LibString.toString(i))); + require( + who.code.length > 0, string.concat("check failed: no code at ", LibString.toHexStringChecksummed(who)) + ); + } + + // All addresses should be unique. + for (uint256 i = 0; i < addresses.length; i++) { + for (uint256 j = i + 1; j < addresses.length; j++) { + string memory err = + string.concat("check failed: duplicates at ", LibString.toString(i), ",", LibString.toString(j)); + require(addresses[i] != addresses[j], err); + } + } + } +} + +// For all broadcasts in this script we explicitly specify the deployer as `msg.sender` because for +// testing we deploy this script from a test contract. If we provide no argument, the foundry +// default sender is be the broadcaster during test, but the broadcaster needs to be the deployer +// since they are set to the initial proxy admin owner. +contract DeploySuperchain is Script { + // -------- Core Deployment Methods -------- + + // This entrypoint is for end-users to deploy from an input file and write to an output file. + // In this usage, we don't need the input and output contract functionality, so we deploy them + // here and abstract that architectural detail away from the end user. + function run(string memory _infile) public { + // End-user without file IO, so etch the IO helper contracts. + (DeploySuperchainInput dsi, DeploySuperchainOutput dso) = etchIOContracts(); + + // Load the input file into the input contract. + dsi.loadInputFile(_infile); + + // Run the deployment script and write outputs to the DeploySuperchainOutput contract. + run(dsi, dso); + + // Write the output data to a file. The file + string memory outfile = ""; // This will be derived from input file name, e.g. `foo.in.toml` -> `foo.out.toml` + dso.writeOutputFile(outfile); + require(false, "DeploySuperchain: run is not implemented"); + } + + // This entrypoint is for use with Solidity tests, where the input and outputs are structs. + function run(DeploySuperchainInput.Input memory _input) public returns (DeploySuperchainOutput.Output memory) { + // Solidity without file IO, so etch the IO helper contracts. + (DeploySuperchainInput dsi, DeploySuperchainOutput dso) = etchIOContracts(); + + // Load the input struct into the input contract. + dsi.loadInput(_input); + + // Run the deployment script and write outputs to the DeploySuperchainOutput contract. + run(dsi, dso); + + // Return the output struct from the output contract. + return dso.output(); + } + + // This entrypoint is useful for testing purposes, as it doesn't use any file I/O. + function run(DeploySuperchainInput _dsi, DeploySuperchainOutput _dso) public { + // Verify that the input contract has been set. + require(_dsi.inputSet(), "DeploySuperchain: Input not set"); + + // Deploy the proxy admin, with the owner set to the deployer. + deploySuperchainProxyAdmin(_dsi, _dso); + + // Deploy and initialize the superchain contracts. + deploySuperchainImplementationContracts(_dsi, _dso); + deployAndInitializeSuperchainConfig(_dsi, _dso); + deployAndInitializeProtocolVersions(_dsi, _dso); + + // Transfer ownership of the ProxyAdmin from the deployer to the specified owner. + transferProxyAdminOwnership(_dsi, _dso); + + // Output assertions, to make sure outputs were assigned correctly. + _dso.checkOutput(); + } + + // -------- Deployment Steps -------- + + function deploySuperchainProxyAdmin(DeploySuperchainInput, DeploySuperchainOutput _dso) public { + // Deploy the proxy admin, with the owner set to the deployer. + // We explicitly specify the deployer as `msg.sender` because for testing we deploy this script from a test + // contract. If we provide no argument, the foundry default sender is be the broadcaster during test, but the + // broadcaster needs to be the deployer since they are set to the initial proxy admin owner. + vm.broadcast(msg.sender); + ProxyAdmin superchainProxyAdmin = new ProxyAdmin(msg.sender); + + vm.label(address(superchainProxyAdmin), "SuperchainProxyAdmin"); + _dso.set(_dso.superchainProxyAdmin.selector, address(superchainProxyAdmin)); + } + + function deploySuperchainImplementationContracts(DeploySuperchainInput, DeploySuperchainOutput _dso) public { + // Deploy implementation contracts. + vm.startBroadcast(msg.sender); + SuperchainConfig superchainConfigImpl = new SuperchainConfig(); + ProtocolVersions protocolVersionsImpl = new ProtocolVersions(); + vm.stopBroadcast(); + + vm.label(address(superchainConfigImpl), "SuperchainConfigImpl"); + vm.label(address(protocolVersionsImpl), "ProtocolVersionsImpl"); + + _dso.set(_dso.superchainConfigImpl.selector, address(superchainConfigImpl)); + _dso.set(_dso.protocolVersionsImpl.selector, address(protocolVersionsImpl)); + } + + function deployAndInitializeSuperchainConfig(DeploySuperchainInput _dsi, DeploySuperchainOutput _dso) public { + require(_dsi.inputSet(), "DeploySuperchain: Input not set"); + address guardian = _dsi.guardian(); + bool paused = _dsi.paused(); + + ProxyAdmin superchainProxyAdmin = _dso.superchainProxyAdmin(); + SuperchainConfig superchainConfigImpl = _dso.superchainConfigImpl(); + assertValidContractAddress(address(superchainProxyAdmin)); + assertValidContractAddress(address(superchainConfigImpl)); + + vm.startBroadcast(msg.sender); + SuperchainConfig superchainConfigProxy = SuperchainConfig(address(new Proxy(address(superchainProxyAdmin)))); + superchainProxyAdmin.upgradeAndCall( + payable(address(superchainConfigProxy)), + address(superchainConfigImpl), + abi.encodeCall(SuperchainConfig.initialize, (guardian, paused)) + ); + vm.stopBroadcast(); + + vm.label(address(superchainConfigProxy), "SuperchainConfigProxy"); + _dso.set(_dso.superchainConfigProxy.selector, address(superchainConfigProxy)); + } + + function deployAndInitializeProtocolVersions(DeploySuperchainInput _dsi, DeploySuperchainOutput _dso) public { + require(_dsi.inputSet(), "DeploySuperchain: Input not set"); + + address protocolVersionsOwner = _dsi.protocolVersionsOwner(); + ProtocolVersion requiredProtocolVersion = _dsi.requiredProtocolVersion(); + ProtocolVersion recommendedProtocolVersion = _dsi.recommendedProtocolVersion(); + + ProxyAdmin superchainProxyAdmin = _dso.superchainProxyAdmin(); + ProtocolVersions protocolVersionsImpl = _dso.protocolVersionsImpl(); + assertValidContractAddress(address(superchainProxyAdmin)); + assertValidContractAddress(address(protocolVersionsImpl)); + + vm.startBroadcast(msg.sender); + ProtocolVersions protocolVersionsProxy = ProtocolVersions(address(new Proxy(address(superchainProxyAdmin)))); + superchainProxyAdmin.upgradeAndCall( + payable(address(protocolVersionsProxy)), + address(protocolVersionsImpl), + abi.encodeCall( + ProtocolVersions.initialize, + (protocolVersionsOwner, requiredProtocolVersion, recommendedProtocolVersion) + ) + ); + vm.stopBroadcast(); + + vm.label(address(protocolVersionsProxy), "ProtocolVersionsProxy"); + _dso.set(_dso.protocolVersionsProxy.selector, address(protocolVersionsProxy)); + } + + function transferProxyAdminOwnership(DeploySuperchainInput _dsi, DeploySuperchainOutput _dso) public { + require(_dsi.inputSet(), "DeploySuperchain: Input not set"); + address proxyAdminOwner = _dsi.proxyAdminOwner(); + + ProxyAdmin superchainProxyAdmin = _dso.superchainProxyAdmin(); + assertValidContractAddress(address(superchainProxyAdmin)); + + vm.broadcast(msg.sender); + superchainProxyAdmin.transferOwnership(proxyAdminOwner); + } + + // -------- Utilities -------- + + // This takes a sender and an identifier and returns a deterministic address based on the two. + // The resulting used to etch the input and output contracts to a deterministic address based on + // those two values, where the identifier represents the input or output contract, such as + // `optimism.DeploySuperchainInput` or `optimism.DeployOPChainOutput`. + function toIOAddress(address _sender, string memory _identifier) internal pure returns (address) { + return address(uint160(uint256(keccak256(abi.encode(_sender, _identifier))))); + } + + function etchIOContracts() internal returns (DeploySuperchainInput dsi_, DeploySuperchainOutput dso_) { + (dsi_, dso_) = getIOContracts(); + vm.etch(address(dsi_), type(DeploySuperchainInput).runtimeCode); + vm.etch(address(dso_), type(DeploySuperchainOutput).runtimeCode); + } + + function getIOContracts() public view returns (DeploySuperchainInput dsi_, DeploySuperchainOutput dso_) { + dsi_ = DeploySuperchainInput(toIOAddress(msg.sender, "optimism.DeploySuperchainInput")); + dso_ = DeploySuperchainOutput(toIOAddress(msg.sender, "optimism.DeploySuperchainOutput")); + } + + function assertValidContractAddress(address _address) internal view { + require(_address != address(0), "DeploySuperchain: zero address"); + require(_address.code.length > 0, "DeploySuperchain: no code"); + } +} diff --git a/packages/contracts-bedrock/test/DeploySuperchain.t.sol b/packages/contracts-bedrock/test/DeploySuperchain.t.sol new file mode 100644 index 000000000000..9a3510d48cce --- /dev/null +++ b/packages/contracts-bedrock/test/DeploySuperchain.t.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import { Test } from "forge-std/Test.sol"; + +import { Proxy } from "src/universal/Proxy.sol"; +import { ProtocolVersion } from "src/L1/ProtocolVersions.sol"; +import { DeploySuperchainInput, DeploySuperchain, DeploySuperchainOutput } from "scripts/DeploySuperchain.s.sol"; + +/// @notice Deploys the Superchain contracts that can be shared by many chains. +contract DeploySuperchain_Test is Test { + DeploySuperchain deploySuperchain; + DeploySuperchainInput dsi; + DeploySuperchainOutput dso; + + // Define a default input struct for testing. + DeploySuperchainInput.Input input = DeploySuperchainInput.Input({ + roles: DeploySuperchainInput.Roles({ + proxyAdminOwner: makeAddr("defaultProxyAdminOwner"), + protocolVersionsOwner: makeAddr("defaultProtocolVersionsOwner"), + guardian: makeAddr("defaultGuardian") + }), + paused: false, + requiredProtocolVersion: ProtocolVersion.wrap(1), + recommendedProtocolVersion: ProtocolVersion.wrap(2) + }); + + function setUp() public { + deploySuperchain = new DeploySuperchain(); + (dsi, dso) = deploySuperchain.getIOContracts(); + } + + function unwrap(ProtocolVersion _pv) internal pure returns (uint256) { + return ProtocolVersion.unwrap(_pv); + } + + function test_run_succeeds(DeploySuperchainInput.Input memory _input) public { + vm.assume(_input.roles.proxyAdminOwner != address(0)); + vm.assume(_input.roles.protocolVersionsOwner != address(0)); + vm.assume(_input.roles.guardian != address(0)); + + DeploySuperchainOutput.Output memory output = deploySuperchain.run(_input); + + // Assert that individual input fields were properly set based on the input struct. + assertEq(_input.roles.proxyAdminOwner, dsi.proxyAdminOwner(), "100"); + assertEq(_input.roles.protocolVersionsOwner, dsi.protocolVersionsOwner(), "200"); + assertEq(_input.roles.guardian, dsi.guardian(), "300"); + assertEq(_input.paused, dsi.paused(), "400"); + assertEq(unwrap(_input.requiredProtocolVersion), unwrap(dsi.requiredProtocolVersion()), "500"); + assertEq(unwrap(_input.recommendedProtocolVersion), unwrap(dsi.recommendedProtocolVersion()), "600"); + + // Assert that individual output fields were properly set based on the output struct. + assertEq(address(output.superchainProxyAdmin), address(dso.superchainProxyAdmin()), "700"); + assertEq(address(output.superchainConfigImpl), address(dso.superchainConfigImpl()), "800"); + assertEq(address(output.superchainConfigProxy), address(dso.superchainConfigProxy()), "900"); + assertEq(address(output.protocolVersionsImpl), address(dso.protocolVersionsImpl()), "1000"); + assertEq(address(output.protocolVersionsProxy), address(dso.protocolVersionsProxy()), "1100"); + + // Assert that the full input and output structs were properly set. + assertEq(keccak256(abi.encode(_input)), keccak256(abi.encode(DeploySuperchainInput(dsi).input())), "1200"); + assertEq(keccak256(abi.encode(output)), keccak256(abi.encode(DeploySuperchainOutput(dso).output())), "1300"); + + // Assert inputs were properly passed through to the contract initializers. + assertEq(address(output.superchainProxyAdmin.owner()), _input.roles.proxyAdminOwner, "1400"); + assertEq(address(output.protocolVersionsProxy.owner()), _input.roles.protocolVersionsOwner, "1500"); + assertEq(address(output.superchainConfigProxy.guardian()), _input.roles.guardian, "1600"); + assertEq(output.superchainConfigProxy.paused(), _input.paused, "1700"); + assertEq(unwrap(output.protocolVersionsProxy.required()), unwrap(_input.requiredProtocolVersion), "1800"); + assertEq(unwrap(output.protocolVersionsProxy.recommended()), unwrap(_input.recommendedProtocolVersion), "1900"); + + // Architecture assertions. + // We prank as the zero address due to the Proxy's `proxyCallIfNotAdmin` modifier. + Proxy superchainConfigProxy = Proxy(payable(address(output.superchainConfigProxy))); + Proxy protocolVersionsProxy = Proxy(payable(address(output.protocolVersionsProxy))); + + vm.startPrank(address(0)); + assertEq(superchainConfigProxy.implementation(), address(output.superchainConfigImpl), "900"); + assertEq(protocolVersionsProxy.implementation(), address(output.protocolVersionsImpl), "1000"); + assertEq(superchainConfigProxy.admin(), protocolVersionsProxy.admin(), "1100"); + assertEq(superchainConfigProxy.admin(), address(output.superchainProxyAdmin), "1200"); + vm.stopPrank(); + + // Ensure that `checkOutput` passes. This is called by the `run` function during execution, + // so this just acts as a sanity check. It reverts on failure. + dso.checkOutput(); + } + + function test_run_ZeroAddressRoles_reverts() public { + // Snapshot the state so we can revert to the default `input` struct between assertions. + uint256 snapshotId = vm.snapshot(); + + // Assert over each role being set to the zero address. + input.roles.proxyAdminOwner = address(0); + vm.expectRevert("DeploySuperchainInput: Null proxyAdminOwner"); + deploySuperchain.run(input); + + vm.revertTo(snapshotId); + input.roles.protocolVersionsOwner = address(0); + vm.expectRevert("DeploySuperchainInput: Null protocolVersionsOwner"); + deploySuperchain.run(input); + + vm.revertTo(snapshotId); + input.roles.guardian = address(0); + vm.expectRevert("DeploySuperchainInput: Null guardian"); + deploySuperchain.run(input); + } +}