Skip to content

Commit

Permalink
Merge pull request #8198 from ethereum-optimism/cl/ctb/reinit-tests
Browse files Browse the repository at this point in the history
feat(ctb): Refactor L1 initializer tests
  • Loading branch information
clabby authored Nov 21, 2023
2 parents af6b5d9 + 74aa6d1 commit 8cd36c2
Show file tree
Hide file tree
Showing 4 changed files with 204 additions and 26 deletions.
1 change: 0 additions & 1 deletion packages/contracts-bedrock/.gas-snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,6 @@ GovernanceToken_Test:test_mint_fromOwner_succeeds() (gas: 110940)
GovernanceToken_Test:test_transferFrom_succeeds() (gas: 151340)
GovernanceToken_Test:test_transfer_succeeds() (gas: 142867)
Hashing_hashDepositSource_Test:test_hashDepositSource_succeeds() (gas: 700)
Initializer_Test:test_cannotReinitializeL1_succeeds() (gas: 44041)
L1BlockNumberTest:test_fallback_succeeds() (gas: 18677)
L1BlockNumberTest:test_getL1BlockNumber_succeeds() (gas: 10647)
L1BlockNumberTest:test_receive_succeeds() (gas: 25384)
Expand Down
4 changes: 2 additions & 2 deletions packages/contracts-bedrock/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"coverage": "pnpm build:go-ffi && forge coverage",
"coverage:lcov": "pnpm build:go-ffi && forge coverage --report lcov",
"deploy": "./scripts/deploy.sh",
"gas-snapshot:no-build": "forge snapshot --no-match-test 'testDiff|testFuzz|invariant|generateArtifact'",
"gas-snapshot:no-build": "forge snapshot --no-match-test 'testDiff|testFuzz|invariant|generateArtifact' --no-match-contract 'Initializer_Test'",
"gas-snapshot": "pnpm build:go-ffi && pnpm gas-snapshot:no-build",
"storage-snapshot": "./scripts/storage-snapshot.sh",
"semver-lock": "forge script scripts/SemverLock.s.sol",
Expand Down Expand Up @@ -50,4 +50,4 @@
"tsx": "^4.1.1",
"typescript": "^5.2.2"
}
}
}
1 change: 1 addition & 0 deletions packages/contracts-bedrock/scripts/Executables.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ library Executables {
string internal constant forge = "forge";
string internal constant echo = "echo";
string internal constant sed = "sed";
string internal constant find = "find";
}
224 changes: 201 additions & 23 deletions packages/contracts-bedrock/test/Initializable.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,218 @@
pragma solidity 0.8.15;

import { Bridge_Initializer } from "test/setup/Bridge_Initializer.sol";
import { Executables } from "scripts/Executables.sol";
import { CrossDomainMessenger } from "src/universal/CrossDomainMessenger.sol";
import { L2OutputOracle } from "src/L1/L2OutputOracle.sol";
import { SystemConfig } from "src/L1/SystemConfig.sol";
import { ResourceMetering } from "src/L1/ResourceMetering.sol";
import { OptimismPortal } from "src/L1/OptimismPortal.sol";
import "src/L1/ProtocolVersions.sol";

/// @title Initializer_Test
/// @dev Ensures that the `initialize()` function on contracts cannot be called more than
/// once. This contract inherits from `ERC721Bridge_Initializer` because it is the
/// deepest contract in the inheritance chain for setting up the system contracts.
contract Initializer_Test is Bridge_Initializer {
function test_cannotReinitializeL1_succeeds() public {
vm.expectRevert("Initializable: contract is already initialized");
l2OutputOracle.initialize(0, 0);

vm.expectRevert("Initializable: contract is already initialized");
optimismPortal.initialize(false);

vm.expectRevert("Initializable: contract is already initialized");
systemConfig.initialize({
_owner: address(0xdEaD),
_overhead: 0,
_scalar: 0,
_batcherHash: bytes32(0),
_gasLimit: 1,
_unsafeBlockSigner: address(0),
_config: ResourceMetering.ResourceConfig({
maxResourceLimit: 1,
elasticityMultiplier: 1,
baseFeeMaxChangeDenominator: 2,
minimumBaseFee: 0,
systemTxMaxGas: 0,
maximumBaseFee: 0
/// @notice Contains the address of an `Initializable` contract and the calldata
/// used to initialize it.
struct InitializeableContract {
address target;
bytes initCalldata;
StorageSlot initializedSlot;
}

/// @notice Contains information about a storage slot. Mirrors the layout of the storage
/// slot object in Forge artifacts so that we can deserialize JSON into this struct.
struct StorageSlot {
uint256 astId;
string _contract;
string label;
uint256 offset;
string slot;
string _type;
}

/// @notice Contains the addresses of the contracts to test as well as the calldata
/// used to initialize them.
InitializeableContract[] contracts;

function setUp() public override {
// Run the `Bridge_Initializer`'s `setUp()` function.
super.setUp();

// Initialize the `contracts` array with the addresses of the contracts to test, the
// calldata used to initialize them, and the storage slot of their `_initialized` flag.

// L1CrossDomainMessenger
contracts.push(
InitializeableContract({
target: address(l1CrossDomainMessenger),
initCalldata: abi.encodeCall(l1CrossDomainMessenger.initialize, ()),
initializedSlot: _getInitializedSlot("L1CrossDomainMessenger")
})
});
);
// L2OutputOracle
contracts.push(
InitializeableContract({
target: address(l2OutputOracle),
initCalldata: abi.encodeCall(l2OutputOracle.initialize, (0, 0)),
initializedSlot: _getInitializedSlot("L2OutputOracle")
})
);
// OptimismPortal
contracts.push(
InitializeableContract({
target: address(optimismPortal),
initCalldata: abi.encodeCall(optimismPortal.initialize, (false)),
initializedSlot: _getInitializedSlot("OptimismPortal")
})
);
// SystemConfig
contracts.push(
InitializeableContract({
target: address(systemConfig),
initCalldata: abi.encodeCall(
systemConfig.initialize,
(
address(0xdead),
0,
0,
bytes32(0),
1,
address(0),
ResourceMetering.ResourceConfig({
maxResourceLimit: 1,
elasticityMultiplier: 1,
baseFeeMaxChangeDenominator: 2,
minimumBaseFee: 0,
systemTxMaxGas: 0,
maximumBaseFee: 0
})
)
),
initializedSlot: _getInitializedSlot("SystemConfig")
})
);
// ProtocolVersions
contracts.push(
InitializeableContract({
target: address(protocolVersions),
initCalldata: abi.encodeCall(
protocolVersions.initialize, (address(0), ProtocolVersion.wrap(1), ProtocolVersion.wrap(2))
),
initializedSlot: _getInitializedSlot("ProtocolVersions")
})
);
}

/// @notice Tests that:
/// 1. All `Initializable` contracts in `src/L1` are accounted for in the `contracts` array.
/// 2. The `_initialized` flag of each contract is properly set to `1`, signifying that the
/// contracts are initialized.
/// 3. The `initialize()` function of each contract cannot be called more than once.
function test_cannotReinitializeL1_succeeds() public {
// Ensure that all L1 `Initializable` contracts are accounted for.
assertEq(_getNumL1Initializable(), contracts.length);

// Attempt to re-initialize all contracts within the `contracts` array.
for (uint256 i; i < contracts.length; i++) {
InitializeableContract memory _contract = contracts[i];

// Load the `_initialized` slot from the storage of the target contract.
uint256 initSlotOffset = _contract.initializedSlot.offset;
bytes32 initSlotVal = vm.load(_contract.target, bytes32(vm.parseUint(_contract.initializedSlot.slot)));

// Pull out the 8-bit `_initialized` flag from the storage slot. The offset in forge artifacts is
// relative to the least-significant bit and signifies the *byte offset*, so we need to shift the
// value to the right by the offset * 8 and then mask out the low-order byte to retrieve the flag.
uint8 init = uint8((uint256(initSlotVal) >> (initSlotOffset * 8)) & 0xFF);
assertEq(init, 1);

// Then, attempt to re-initialize the contract. This should fail.
(bool success, bytes memory returnData) = _contract.target.call(_contract.initCalldata);
assertFalse(success);
assertEq(_extractErrorString(returnData), "Initializable: contract is already initialized");
}
}

/// @dev Pulls the `_initialized` storage slot information from the Forge artifacts for a given contract.
function _getInitializedSlot(string memory _contractName) internal returns (StorageSlot memory slot_) {
string memory storageLayout = getStorageLayout(_contractName);

string[] memory command = new string[](3);
command[0] = Executables.bash;
command[1] = "-c";
command[2] = string.concat(
Executables.echo,
" '",
storageLayout,
"'",
" | ",
Executables.jq,
" '.storage[] | select(.label == \"_initialized\" and .type == \"t_uint8\")'"
);
bytes memory rawSlot = vm.parseJson(string(vm.ffi(command)));
slot_ = abi.decode(rawSlot, (StorageSlot));
}

/// @dev Returns the number of contracts that are `Initializable` in `src/L1`.
function _getNumL1Initializable() internal returns (uint256 numContracts_) {
string[] memory command = new string[](3);
command[0] = Executables.bash;
command[1] = "-c";
command[2] = string.concat(
Executables.find,
" src/L1 -type f -exec basename {} \\;",
" | ",
Executables.sed,
" 's/\\.[^.]*$//'",
" | ",
Executables.jq,
" -R -s 'split(\"\n\")[:-1]'"
);
string[] memory contractNames = abi.decode(vm.parseJson(string(vm.ffi(command))), (string[]));

for (uint256 i; i < contractNames.length; i++) {
string memory contractName = contractNames[i];
string memory contractAbi = getAbi(contractName);

// Query the contract's ABI for an `initialize()` function.
command[2] = string.concat(
Executables.echo,
" '",
contractAbi,
"'",
" | ",
Executables.jq,
" '.[] | select(.name == \"initialize\" and .type == \"function\")'"
);
bytes memory res = vm.ffi(command);

// If the contract has an `initialize()` function, the resulting query will be non-empty.
// In this case, increment the number of `Initializable` contracts.
if (res.length > 0) {
numContracts_++;
}
}
}

/// @dev Extracts the revert string from returndata encoded in the form of `Error(string)`.
function _extractErrorString(bytes memory _returnData) internal pure returns (string memory error_) {
// The first 4 bytes of the return data should be the selector for `Error(string)`. If not, revert.
if (bytes4(_returnData) == 0x08c379a0) {
// Extract the error string from the returndata. The error string is located 68 bytes after
// the pointer to `returnData`.
//
// 32 bytes: `returnData` length
// 4 bytes: `Error(string)` selector
// 32 bytes: ABI encoding metadata; String offset
// = 68 bytes
assembly {
error_ := add(_returnData, 0x44)
}
} else {
revert("Initializer_Test: Invalid returndata format. Expected `Error(string)`");
}
}
}

0 comments on commit 8cd36c2

Please sign in to comment.