Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ctb): Refactor L1 initializer tests #8198

Merged
merged 6 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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'",
clabby marked this conversation as resolved.
Show resolved Hide resolved
"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";
mds1 marked this conversation as resolved.
Show resolved Hide resolved

/// @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;
mds1 marked this conversation as resolved.
Show resolved Hide resolved
string _type;
}
mds1 marked this conversation as resolved.
Show resolved Hide resolved

/// @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[]));
mds1 marked this conversation as resolved.
Show resolved Hide resolved

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\")'"
clabby marked this conversation as resolved.
Show resolved Hide resolved
);
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)`");
}
}
}