From a16cca518a5a18582d8521e2ff321589dfa15128 Mon Sep 17 00:00:00 2001 From: 0xDiscotech <131301107+0xDiscotech@users.noreply.github.com> Date: Wed, 2 Oct 2024 17:26:11 -0300 Subject: [PATCH 01/31] chore: add new solady version and import it for erc20 --- .gitmodules | 3 +++ packages/contracts-bedrock/.gas-snapshot | 14 +++++++------- packages/contracts-bedrock/foundry.toml | 1 + packages/contracts-bedrock/lib/forge-std | 2 +- packages/contracts-bedrock/lib/solady-v0.0.245 | 1 + packages/contracts-bedrock/semver-lock.json | 6 +++--- .../snapshots/abi/OptimismSuperchainERC20.json | 5 +++++ .../src/L2/OptimismSuperchainERC20.sol | 4 ++-- .../contracts-bedrock/src/L2/SuperchainERC20.sol | 6 +++--- 9 files changed, 26 insertions(+), 16 deletions(-) create mode 160000 packages/contracts-bedrock/lib/solady-v0.0.245 diff --git a/.gitmodules b/.gitmodules index 21ecaedbb77a..5422163a9599 100644 --- a/.gitmodules +++ b/.gitmodules @@ -29,3 +29,6 @@ [submodule "packages/contracts-bedrock/lib/openzeppelin-contracts-v5"] path = packages/contracts-bedrock/lib/openzeppelin-contracts-v5 url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "packages/contracts-bedrock/lib/solady-v0.0.245"] + path = packages/contracts-bedrock/lib/solady-v0.0.245 + url = https://github.com/vectorized/solady diff --git a/packages/contracts-bedrock/.gas-snapshot b/packages/contracts-bedrock/.gas-snapshot index f42bdc83dcb7..5fa3835835eb 100644 --- a/packages/contracts-bedrock/.gas-snapshot +++ b/packages/contracts-bedrock/.gas-snapshot @@ -4,14 +4,14 @@ GasBenchMark_L1BlockIsthmus_SetValuesIsthmus:test_setL1BlockValuesIsthmus_benchm GasBenchMark_L1BlockIsthmus_SetValuesIsthmus_Warm:test_setL1BlockValuesIsthmus_benchmark() (gas: 5121) GasBenchMark_L1Block_SetValuesEcotone:test_setL1BlockValuesEcotone_benchmark() (gas: 158531) GasBenchMark_L1Block_SetValuesEcotone_Warm:test_setL1BlockValuesEcotone_benchmark() (gas: 7597) -GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_0() (gas: 369242) -GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_1() (gas: 2967382) -GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_0() (gas: 564356) -GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_1() (gas: 4076571) +GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_0() (gas: 369245) +GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_1() (gas: 2967385) +GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_0() (gas: 564368) +GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_1() (gas: 4076583) GasBenchMark_L1StandardBridge_Deposit:test_depositETH_benchmark_0() (gas: 467019) -GasBenchMark_L1StandardBridge_Deposit:test_depositETH_benchmark_1() (gas: 3512701) +GasBenchMark_L1StandardBridge_Deposit:test_depositETH_benchmark_1() (gas: 3512723) GasBenchMark_L1StandardBridge_Finalize:test_finalizeETHWithdrawal_benchmark() (gas: 72618) GasBenchMark_L2OutputOracle:test_proposeL2Output_benchmark() (gas: 92973) -GasBenchMark_OptimismPortal:test_depositTransaction_benchmark() (gas: 68312) -GasBenchMark_OptimismPortal:test_depositTransaction_benchmark_1() (gas: 68943) +GasBenchMark_OptimismPortal:test_depositTransaction_benchmark() (gas: 68357) +GasBenchMark_OptimismPortal:test_depositTransaction_benchmark_1() (gas: 68921) GasBenchMark_OptimismPortal:test_proveWithdrawalTransaction_benchmark() (gas: 155610) \ No newline at end of file diff --git a/packages/contracts-bedrock/foundry.toml b/packages/contracts-bedrock/foundry.toml index cef9f85bbaeb..36e12cab585b 100644 --- a/packages/contracts-bedrock/foundry.toml +++ b/packages/contracts-bedrock/foundry.toml @@ -17,6 +17,7 @@ remappings = [ '@rari-capital/solmate/=lib/solmate', '@lib-keccak/=lib/lib-keccak/contracts/lib', '@solady/=lib/solady/src', + '@solady-v0.0.245/=lib/solady-v0.0.245/src', 'forge-std/=lib/forge-std/src', 'ds-test/=lib/forge-std/lib/ds-test/src', 'safe-contracts/=lib/safe-contracts/contracts', diff --git a/packages/contracts-bedrock/lib/forge-std b/packages/contracts-bedrock/lib/forge-std index 2d8b7b876a5b..8f24d6b04c92 160000 --- a/packages/contracts-bedrock/lib/forge-std +++ b/packages/contracts-bedrock/lib/forge-std @@ -1 +1 @@ -Subproject commit 2d8b7b876a5b328d6a73e13c4740ed7a0d72d5f4 +Subproject commit 8f24d6b04c92975e0795b5868aa0d783251cdeaa diff --git a/packages/contracts-bedrock/lib/solady-v0.0.245 b/packages/contracts-bedrock/lib/solady-v0.0.245 new file mode 160000 index 000000000000..e0ef35adb0cc --- /dev/null +++ b/packages/contracts-bedrock/lib/solady-v0.0.245 @@ -0,0 +1 @@ +Subproject commit e0ef35adb0ccd1032794731a995cb599bba7b537 diff --git a/packages/contracts-bedrock/semver-lock.json b/packages/contracts-bedrock/semver-lock.json index e794d31001a1..3e2acea24cc2 100644 --- a/packages/contracts-bedrock/semver-lock.json +++ b/packages/contracts-bedrock/semver-lock.json @@ -116,8 +116,8 @@ "sourceCodeHash": "0x4b806cc85cead74c8df34ab08f4b6c6a95a1a387a335ec8a7cb2de4ea4e1cf41" }, "src/L2/OptimismSuperchainERC20.sol": { - "initCodeHash": "0xc6452d9aef6d76bdc789f3cddac6862658a481c619e6a2e7a74f6d61147f927b", - "sourceCodeHash": "0x2502433e4b622e1697ca071f91a95b08fa40fdb03bfd958c44b2033a47df2010" + "initCodeHash": "0x9d88c802312f68a4271c6b10673ff50941b0bfad805bbb2ea1a7d6aca459b557", + "sourceCodeHash": "0xe96e39ffdbc09155a5abbc757226f2a8030f398e87937ee779f6b67b74ca95c5" }, "src/L2/OptimismSuperchainERC20Beacon.sol": { "initCodeHash": "0x99ce8095b23c124850d866cbc144fee6cee05dbc6bb5d83acadfe00b90cf42c7", @@ -133,7 +133,7 @@ }, "src/L2/SuperchainERC20.sol": { "initCodeHash": "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", - "sourceCodeHash": "0x9bc2e208774eb923894dbe391a5038a6189d7d36c202f4bf3e2c4dd332b0adf0" + "sourceCodeHash": "0xedaff59c16191469b7a4103ceb24913d98ac6c74902e6033e77f83de32819c03" }, "src/L2/SuperchainERC20Bridge.sol": { "initCodeHash": "0xa21232df1d7239fd20e7eaa320cfc91efc76343c93d833d8060a58b54ac5c8bf", diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json b/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json index 7c24b3fe0065..ab963cefe21f 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json @@ -580,6 +580,11 @@ "name": "OnlySuperchainERC20Bridge", "type": "error" }, + { + "inputs": [], + "name": "Permit2AllowanceIsFixedAtInfinity", + "type": "error" + }, { "inputs": [], "name": "PermitExpired", diff --git a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol index 6db110dd5f5f..5be585ed2f9d 100644 --- a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol @@ -50,8 +50,8 @@ contract OptimismSuperchainERC20 is SuperchainERC20, Initializable, ERC165, IOpt } /// @notice Semantic version. - /// @custom:semver 1.0.0-beta.6 - string public constant override version = "1.0.0-beta.6"; + /// @custom:semver 1.0.0-beta.7 + string public constant override version = "1.0.0-beta.7"; /// @notice Constructs the OptimismSuperchainERC20 contract. constructor() { diff --git a/packages/contracts-bedrock/src/L2/SuperchainERC20.sol b/packages/contracts-bedrock/src/L2/SuperchainERC20.sol index 6c48b231baaf..5eafe575c55a 100644 --- a/packages/contracts-bedrock/src/L2/SuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/SuperchainERC20.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.25; import { ISuperchainERC20Extension } from "src/L2/interfaces/ISuperchainERC20.sol"; import { ISemver } from "src/universal/interfaces/ISemver.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; -import { ERC20 } from "@solady/tokens/ERC20.sol"; +import { ERC20 } from "@solady-v0.0.245/tokens/ERC20.sol"; /// @title SuperchainERC20 /// @notice SuperchainERC20 is a standard extension of the base ERC20 token contract that unifies ERC20 token @@ -18,9 +18,9 @@ abstract contract SuperchainERC20 is ERC20, ISuperchainERC20Extension, ISemver { } /// @notice Semantic version. - /// @custom:semver 1.0.0-beta.1 + /// @custom:semver 1.0.0-beta.2 function version() external pure virtual returns (string memory) { - return "1.0.0-beta.1"; + return "1.0.0-beta.2"; } /// @notice Allows the SuperchainERC20Bridge to mint tokens. From 0938dd8e0afe952975b415d067ac626a7832a253 Mon Sep 17 00:00:00 2001 From: 0xDiscotech <131301107+0xDiscotech@users.noreply.github.com> Date: Wed, 2 Oct 2024 23:16:40 -0300 Subject: [PATCH 02/31] fix: undo forge std changes --- packages/contracts-bedrock/lib/forge-std | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts-bedrock/lib/forge-std b/packages/contracts-bedrock/lib/forge-std index 8f24d6b04c92..2d8b7b876a5b 160000 --- a/packages/contracts-bedrock/lib/forge-std +++ b/packages/contracts-bedrock/lib/forge-std @@ -1 +1 @@ -Subproject commit 8f24d6b04c92975e0795b5868aa0d783251cdeaa +Subproject commit 2d8b7b876a5b328d6a73e13c4740ed7a0d72d5f4 From 68dc9db644d5bc2d6df87455cb3c43251073f503 Mon Sep 17 00:00:00 2001 From: 0xDiscotech <131301107+0xDiscotech@users.noreply.github.com> Date: Wed, 2 Oct 2024 23:40:49 -0300 Subject: [PATCH 03/31] chore: re run pre pr script --- packages/contracts-bedrock/.gas-snapshot | 14 +++++++------- packages/contracts-bedrock/semver-lock.json | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/contracts-bedrock/.gas-snapshot b/packages/contracts-bedrock/.gas-snapshot index da67af9f81fc..4c8038a0ac68 100644 --- a/packages/contracts-bedrock/.gas-snapshot +++ b/packages/contracts-bedrock/.gas-snapshot @@ -4,14 +4,14 @@ GasBenchMark_L1BlockInterop_SetValuesInterop:test_setL1BlockValuesInterop_benchm GasBenchMark_L1BlockInterop_SetValuesInterop_Warm:test_setL1BlockValuesInterop_benchmark() (gas: 5099) GasBenchMark_L1Block_SetValuesEcotone:test_setL1BlockValuesEcotone_benchmark() (gas: 158531) GasBenchMark_L1Block_SetValuesEcotone_Warm:test_setL1BlockValuesEcotone_benchmark() (gas: 7597) -GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_0() (gas: 369245) -GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_1() (gas: 2967385) -GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_0() (gas: 564368) -GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_1() (gas: 4076583) +GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_0() (gas: 369242) +GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_1() (gas: 2967382) +GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_0() (gas: 564356) +GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_1() (gas: 4076571) GasBenchMark_L1StandardBridge_Deposit:test_depositETH_benchmark_0() (gas: 467019) -GasBenchMark_L1StandardBridge_Deposit:test_depositETH_benchmark_1() (gas: 3512723) +GasBenchMark_L1StandardBridge_Deposit:test_depositETH_benchmark_1() (gas: 3512701) GasBenchMark_L1StandardBridge_Finalize:test_finalizeETHWithdrawal_benchmark() (gas: 72618) GasBenchMark_L2OutputOracle:test_proposeL2Output_benchmark() (gas: 92973) -GasBenchMark_OptimismPortal:test_depositTransaction_benchmark() (gas: 68357) -GasBenchMark_OptimismPortal:test_depositTransaction_benchmark_1() (gas: 68921) +GasBenchMark_OptimismPortal:test_depositTransaction_benchmark() (gas: 68312) +GasBenchMark_OptimismPortal:test_depositTransaction_benchmark_1() (gas: 68943) GasBenchMark_OptimismPortal:test_proveWithdrawalTransaction_benchmark() (gas: 155610) \ No newline at end of file diff --git a/packages/contracts-bedrock/semver-lock.json b/packages/contracts-bedrock/semver-lock.json index 87435d4c2d72..ca9184287079 100644 --- a/packages/contracts-bedrock/semver-lock.json +++ b/packages/contracts-bedrock/semver-lock.json @@ -117,7 +117,7 @@ }, "src/L2/OptimismSuperchainERC20.sol": { "initCodeHash": "0x9d88c802312f68a4271c6b10673ff50941b0bfad805bbb2ea1a7d6aca459b557", - "sourceCodeHash": "0xe96e39ffdbc09155a5abbc757226f2a8030f398e87937ee779f6b67b74ca95c5" + "sourceCodeHash": "0xb9165f6ef40da96d7fd7a3ae68268be1653b8be6491d886fcf1696ed50a9e44f" }, "src/L2/OptimismSuperchainERC20Beacon.sol": { "initCodeHash": "0x99ce8095b23c124850d866cbc144fee6cee05dbc6bb5d83acadfe00b90cf42c7", @@ -235,4 +235,4 @@ "initCodeHash": "0x21b3059e9b13b330f76d02b61f61dcfa3abf3517a0b56afa0895c4b8291740bf", "sourceCodeHash": "0xc1ea12a87e3a7ef9c950f0a41a4e35b60d4d9c4c816ff671dbfca663861c16f4" } -} +} \ No newline at end of file From bdcd7f1d335601b1fbfee2b5fe83d8de2b38e149 Mon Sep 17 00:00:00 2001 From: 0xDiscotech <131301107+0xDiscotech@users.noreply.github.com> Date: Wed, 9 Oct 2024 16:11:50 -0300 Subject: [PATCH 04/31] chore: run pre-pr and update vendor interface --- packages/contracts-bedrock/lib/forge-std | 2 +- packages/contracts-bedrock/semver-lock.json | 8 ++++---- packages/contracts-bedrock/src/L2/SuperchainERC20.sol | 4 ++-- .../src/vendor/interfaces/IERC20Solady.sol | 3 +++ 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/contracts-bedrock/lib/forge-std b/packages/contracts-bedrock/lib/forge-std index 2d8b7b876a5b..8f24d6b04c92 160000 --- a/packages/contracts-bedrock/lib/forge-std +++ b/packages/contracts-bedrock/lib/forge-std @@ -1 +1 @@ -Subproject commit 2d8b7b876a5b328d6a73e13c4740ed7a0d72d5f4 +Subproject commit 8f24d6b04c92975e0795b5868aa0d783251cdeaa diff --git a/packages/contracts-bedrock/semver-lock.json b/packages/contracts-bedrock/semver-lock.json index c214b758a76e..df9fc62d7b67 100644 --- a/packages/contracts-bedrock/semver-lock.json +++ b/packages/contracts-bedrock/semver-lock.json @@ -108,8 +108,8 @@ "sourceCodeHash": "0xfea53344596d735eff3be945ed1300dc75a6f8b7b2c02c0043af5b0036f5f239" }, "src/L2/OptimismSuperchainERC20.sol": { - "initCodeHash": "0xadeaebb33c1d758d88d7aadd0ad654c9a1f2d59824c5dad19e1d9cf05ea3e516", - "sourceCodeHash": "0x5a40eabbdc33bd96ff0a7bc2be6699a7b6233610bf67f3da899f0efb367bb486" + "initCodeHash": "0xa44a4fb2417bcb71bde84bf2e7b839ed2ff31b73157df741edb571a5a42543d6", + "sourceCodeHash": "0x59cd1cdedae4b8582e6b8904f2b5a68822c870af3c689fdd8214a766a4685660" }, "src/L2/OptimismSuperchainERC20Beacon.sol": { "initCodeHash": "0x99ce8095b23c124850d866cbc144fee6cee05dbc6bb5d83acadfe00b90cf42c7", @@ -125,7 +125,7 @@ }, "src/L2/SuperchainERC20.sol": { "initCodeHash": "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", - "sourceCodeHash": "0x19f598d3b3e77963f9af395b0102dd9acea0e76f7a0ed6eb937d94d3c054137e" + "sourceCodeHash": "0x3e4e270f2c91f73f352d92f215abbfbcd740e9b67eb15ca93383f18832aae7f1" }, "src/L2/SuperchainTokenBridge.sol": { "initCodeHash": "0xfeba60d8e17a0c62cc56c7319da323e154ccc6c379e7b72c48c9d0ce1e5b9474", @@ -235,4 +235,4 @@ "initCodeHash": "0x06ae2c0b39c215b7fa450d382916ce6f5c6f9f2d630e572db6b72d688255b3fd", "sourceCodeHash": "0xa014d9c992f439dee8221e065828c3326ca2c4f5db0e83431c64c20f7e51ec14" } -} +} \ No newline at end of file diff --git a/packages/contracts-bedrock/src/L2/SuperchainERC20.sol b/packages/contracts-bedrock/src/L2/SuperchainERC20.sol index ab6d7dd42812..5881bf9a72a4 100644 --- a/packages/contracts-bedrock/src/L2/SuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/SuperchainERC20.sol @@ -21,9 +21,9 @@ abstract contract SuperchainERC20 is ERC20, ICrosschainERC20, ISemver { } /// @notice Semantic version. - /// @custom:semver 1.0.0-beta.1 + /// @custom:semver 1.0.0-beta.2 function version() external view virtual returns (string memory) { - return "1.0.0-beta.1"; + return "1.0.0-beta.2"; } /// @notice Allows the SuperchainTokenBridge to mint tokens. diff --git a/packages/contracts-bedrock/src/vendor/interfaces/IERC20Solady.sol b/packages/contracts-bedrock/src/vendor/interfaces/IERC20Solady.sol index 1e696ad23ac3..b05b906eec97 100644 --- a/packages/contracts-bedrock/src/vendor/interfaces/IERC20Solady.sol +++ b/packages/contracts-bedrock/src/vendor/interfaces/IERC20Solady.sol @@ -23,6 +23,9 @@ interface IERC20Solady { /// @dev The permit has expired. error PermitExpired(); + /// @dev The allowance of Permit2 is fixed at infinity. + error Permit2AllowanceIsFixedAtInfinity(); + /// @dev Emitted when `amount` tokens is transferred from `from` to `to`. event Transfer(address indexed from, address indexed to, uint256 amount); From 70e3570da04eec587d414358007b7987580a595a Mon Sep 17 00:00:00 2001 From: 0xDiscotech <131301107+0xDiscotech@users.noreply.github.com> Date: Wed, 9 Oct 2024 16:43:29 -0300 Subject: [PATCH 05/31] feat: add permit2 on optimism superchain erc20 --- packages/contracts-bedrock/semver-lock.json | 4 ++-- .../contracts-bedrock/src/L2/OptimismSuperchainERC20.sol | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/contracts-bedrock/semver-lock.json b/packages/contracts-bedrock/semver-lock.json index df9fc62d7b67..2715fce57ff3 100644 --- a/packages/contracts-bedrock/semver-lock.json +++ b/packages/contracts-bedrock/semver-lock.json @@ -108,8 +108,8 @@ "sourceCodeHash": "0xfea53344596d735eff3be945ed1300dc75a6f8b7b2c02c0043af5b0036f5f239" }, "src/L2/OptimismSuperchainERC20.sol": { - "initCodeHash": "0xa44a4fb2417bcb71bde84bf2e7b839ed2ff31b73157df741edb571a5a42543d6", - "sourceCodeHash": "0x59cd1cdedae4b8582e6b8904f2b5a68822c870af3c689fdd8214a766a4685660" + "initCodeHash": "0xff04c65ec2bf258b71ba6c3a29076914075fff72f1ae11d821b5b52ffa593746", + "sourceCodeHash": "0x45157fe5751971a8e06ebab6a84461b01e636fa929ba926301742dae39297b03" }, "src/L2/OptimismSuperchainERC20Beacon.sol": { "initCodeHash": "0x99ce8095b23c124850d866cbc144fee6cee05dbc6bb5d83acadfe00b90cf42c7", diff --git a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol index 6591afaaa3e3..575a62814a84 100644 --- a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol @@ -66,8 +66,8 @@ contract OptimismSuperchainERC20 is SuperchainERC20, Initializable, ERC165 { } /// @notice Semantic version. - /// @custom:semver 1.0.0-beta.7 - string public constant override version = "1.0.0-beta.7"; + /// @custom:semver 1.0.0-beta.8 + string public constant override version = "1.0.0-beta.8"; /// @notice Constructs the OptimismSuperchainERC20 contract. constructor() { @@ -148,4 +148,9 @@ contract OptimismSuperchainERC20 is SuperchainERC20, Initializable, ERC165 { function supportsInterface(bytes4 _interfaceId) public view virtual override returns (bool) { return _interfaceId == type(IOptimismSuperchainERC20).interfaceId || super.supportsInterface(_interfaceId); } + + /// @notice Sets Permit2 contract's allowance at infinity. + function _givePermit2InfiniteAllowance() internal view virtual override returns (bool) { + return true; + } } From 553462db0ece2b30fa23d6903cc780dede9b9297 Mon Sep 17 00:00:00 2001 From: 0xDiscotech <131301107+0xDiscotech@users.noreply.github.com> Date: Wed, 9 Oct 2024 16:51:04 -0300 Subject: [PATCH 06/31] chore: run pre-pr script --- packages/contracts-bedrock/semver-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/contracts-bedrock/semver-lock.json b/packages/contracts-bedrock/semver-lock.json index 094ac4222478..a449281871f2 100644 --- a/packages/contracts-bedrock/semver-lock.json +++ b/packages/contracts-bedrock/semver-lock.json @@ -108,8 +108,8 @@ "sourceCodeHash": "0xfea53344596d735eff3be945ed1300dc75a6f8b7b2c02c0043af5b0036f5f239" }, "src/L2/OptimismSuperchainERC20.sol": { - "initCodeHash": "0xadeaebb33c1d758d88d7aadd0ad654c9a1f2d59824c5dad19e1d9cf05ea3e516", - "sourceCodeHash": "0xacf5ca4cdebd7e1d52f691db0f873cc026c6336a9ea309af1364a46aba723180" + "initCodeHash": "0xff04c65ec2bf258b71ba6c3a29076914075fff72f1ae11d821b5b52ffa593746", + "sourceCodeHash": "0x485831aade04f7bca1a0a5f83f0d8b909a9c78c889c34372c950eb8aeb9b64bc" }, "src/L2/OptimismSuperchainERC20Beacon.sol": { "initCodeHash": "0x99ce8095b23c124850d866cbc144fee6cee05dbc6bb5d83acadfe00b90cf42c7", @@ -235,4 +235,4 @@ "initCodeHash": "0x06ae2c0b39c215b7fa450d382916ce6f5c6f9f2d630e572db6b72d688255b3fd", "sourceCodeHash": "0xa014d9c992f439dee8221e065828c3326ca2c4f5db0e83431c64c20f7e51ec14" } -} +} \ No newline at end of file From a5d0e3079fc0c8e30af15c0fe46cb040c2335d6f Mon Sep 17 00:00:00 2001 From: Chen Kai <281165273grape@gmail.com> Date: Fri, 11 Oct 2024 21:54:20 +0800 Subject: [PATCH 07/31] MT Cannon: add mips movf/movt tests (#12392) * feat:add movf/movt tests Signed-off-by: Chen Kai <281165273grape@gmail.com> * fix:fix code review nit Signed-off-by: Chen Kai <281165273grape@gmail.com> --------- Signed-off-by: Chen Kai <281165273grape@gmail.com> --- cannon/mipsevm/tests/evm_common_test.go | 78 +++++++++++++++++++++++++ cannon/mipsevm/testutil/state.go | 12 ++++ 2 files changed, 90 insertions(+) diff --git a/cannon/mipsevm/tests/evm_common_test.go b/cannon/mipsevm/tests/evm_common_test.go index f351ba4733eb..0b54a50bc28e 100644 --- a/cannon/mipsevm/tests/evm_common_test.go +++ b/cannon/mipsevm/tests/evm_common_test.go @@ -378,6 +378,84 @@ func TestEVMSingleStep_MovzMovn(t *testing.T) { } +func TestEVMSingleStep_MfhiMflo(t *testing.T) { + var tracer *tracing.Hooks + versions := GetMipsVersionTestCases(t) + cases := []struct { + name string + funct uint32 + hi Word + lo Word + }{ + {name: "mflo", funct: uint32(0x12), lo: Word(0xdeadbeef), hi: Word(0x0)}, + {name: "mfhi", funct: uint32(0x10), lo: Word(0x0), hi: Word(0xdeadbeef)}, + } + expect := Word(0xdeadbeef) + for _, v := range versions { + for i, tt := range cases { + testName := fmt.Sprintf("%v (%v)", tt.name, v.Name) + t.Run(testName, func(t *testing.T) { + goVm := v.VMFactory(nil, os.Stdout, os.Stderr, testutil.CreateLogger(), testutil.WithRandomization(int64(i)), testutil.WithLO(tt.lo), testutil.WithHI(tt.hi)) + state := goVm.GetState() + rdReg := uint32(8) + insn := rdReg<<11 | tt.funct + state.GetMemory().SetUint32(state.GetPC(), insn) + step := state.GetStep() + // Setup expectations + expected := testutil.NewExpectedState(state) + expected.ExpectStep() + expected.Registers[rdReg] = expect + stepWitness, err := goVm.Step(true) + require.NoError(t, err) + // Check expectations + expected.Validate(t, state) + testutil.ValidateEVM(t, stepWitness, step, goVm, v.StateHashFn, v.Contracts, tracer) + }) + } + } +} + +func TestEVMSingleStep_MthiMtlo(t *testing.T) { + var tracer *tracing.Hooks + versions := GetMipsVersionTestCases(t) + cases := []struct { + name string + funct uint32 + }{ + {name: "mtlo", funct: uint32(0x13)}, + {name: "mthi", funct: uint32(0x11)}, + } + val := Word(0xdeadbeef) + for _, v := range versions { + for i, tt := range cases { + testName := fmt.Sprintf("%v (%v)", tt.name, v.Name) + t.Run(testName, func(t *testing.T) { + + goVm := v.VMFactory(nil, os.Stdout, os.Stderr, testutil.CreateLogger(), testutil.WithRandomization(int64(i))) + state := goVm.GetState() + rsReg := uint32(8) + insn := rsReg<<21 | tt.funct + state.GetMemory().SetUint32(state.GetPC(), insn) + state.GetRegistersRef()[rsReg] = val + step := state.GetStep() + // Setup expectations + expected := testutil.NewExpectedState(state) + expected.ExpectStep() + if tt.funct == 0x11 { + expected.HI = state.GetRegistersRef()[rsReg] + } else { + expected.LO = state.GetRegistersRef()[rsReg] + } + stepWitness, err := goVm.Step(true) + require.NoError(t, err) + // Check expectations + expected.Validate(t, state) + testutil.ValidateEVM(t, stepWitness, step, goVm, v.StateHashFn, v.Contracts, tracer) + }) + } + } +} + func TestEVM_MMap(t *testing.T) { var tracer *tracing.Hooks diff --git a/cannon/mipsevm/testutil/state.go b/cannon/mipsevm/testutil/state.go index 0c247a7fb26d..742677bdba56 100644 --- a/cannon/mipsevm/testutil/state.go +++ b/cannon/mipsevm/testutil/state.go @@ -68,6 +68,18 @@ func WithPCAndNextPC(pc arch.Word) StateOption { } } +func WithHI(hi arch.Word) StateOption { + return func(state StateMutator) { + state.SetHI(hi) + } +} + +func WithLO(lo arch.Word) StateOption { + return func(state StateMutator) { + state.SetLO(lo) + } +} + func WithHeap(addr arch.Word) StateOption { return func(state StateMutator) { state.SetHeap(addr) From 0466b62d21403f851760bf5f50e6db3d35db7668 Mon Sep 17 00:00:00 2001 From: Maurelian Date: Fri, 11 Oct 2024 14:40:37 -0400 Subject: [PATCH 08/31] deployOpChain using OPCM (#12291) * feat: move log to top of save function Makes debugging slightly easier. * feat: Deploy system with OPCM * feat: Deploy missing Permissionless DWETH * feat: Fully OPCM based deployment At this point tests that depend on the L2OutputOracle will still fail, but those with Fault Proofs _should_ pass. * feat: Refactor _setFaultGameImplementation It no longer handles the PERMISSIONED game type deploy by OPCM. * test: Make caller explicit in unauthorized test case * test: Fix have guardian setRespectedGameType to Cannon THis is necessary because the tests assume the respecte game is cannon, but the OPCM assumes it is the permissioned game * test: Bridge_Initializer defaulst to FP on * test: setRespectedGameType to Cannon in Deploy * test: Bridge_Initializer defaulst to FP on * feat: resetInitializedProxy func to allow reinitializing contracts * feat: Delete initializeOpChain, we don't need it anymore * feat: Set batch inbox address in config using hashed method * feat: Make caller explicit in testFuzz_removeDependency_notDependencyManager_reverts * feat: lint * Revert "test: Bridge_Initializer defaulst to FP on" This reverts commit d435653b5405f35970e9663cfddd5a7214fcdb57. * feat: Consolidate useFaultProofs modifications into _run * Revert "test: Bridge_Initializer defaulst to FP on" This reverts commit af8d99b94393c3adef32b6ae5d5384e6766569e5. * feat: Some annotations in Deploy.s.ol * feat: Skip checking L2OO in Initializable.t.sol * feat: Delete unused initialize functions * fix: Remove unused imports * fix: unused import * feat: Use respectedGameType from deploy-config * use startPrank * feat: Prevent deploying legacy portal for interop * fix: incorrect var name * feat: detect when OPCM is deploying the Permissionless game * fix: error format --- .../deploy-config/devnetL1-template.json | 2 +- .../deploy-config/hardhat.json | 2 +- .../contracts-bedrock/scripts/Artifacts.s.sol | 2 +- .../scripts/deploy/Deploy.s.sol | 640 +++++------------- .../scripts/libraries/Constants.sol | 6 +- .../test/L1/SystemConfigInterop.t.sol | 4 + .../test/vendor/Initializable.t.sol | 4 +- 7 files changed, 202 insertions(+), 458 deletions(-) diff --git a/packages/contracts-bedrock/deploy-config/devnetL1-template.json b/packages/contracts-bedrock/deploy-config/devnetL1-template.json index d241c3186a08..1844d7dbd9ce 100644 --- a/packages/contracts-bedrock/deploy-config/devnetL1-template.json +++ b/packages/contracts-bedrock/deploy-config/devnetL1-template.json @@ -6,7 +6,7 @@ "sequencerWindowSize": 200, "channelTimeout": 120, "p2pSequencerAddress": "0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc", - "batchInboxAddress": "0xff00000000000000000000000000000000000901", + "batchInboxAddress": "0x00289C189bEE4E70334629f04Cd5eD602B6600eB", "batchSenderAddress": "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", "l1StartingBlockTag": "earliest", "l2OutputOracleSubmissionInterval": 10, diff --git a/packages/contracts-bedrock/deploy-config/hardhat.json b/packages/contracts-bedrock/deploy-config/hardhat.json index 6dcbb299d1de..965d403d4d82 100644 --- a/packages/contracts-bedrock/deploy-config/hardhat.json +++ b/packages/contracts-bedrock/deploy-config/hardhat.json @@ -11,7 +11,7 @@ "sequencerWindowSize": 15, "channelTimeout": 40, "p2pSequencerAddress": "0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc", - "batchInboxAddress": "0xff00000000000000000000000000000000000000", + "batchInboxAddress": "0x00289C189bEE4E70334629f04Cd5eD602B6600eB", "batchSenderAddress": "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", "l2OutputOracleSubmissionInterval": 6, "l2OutputOracleStartingTimestamp": 1, diff --git a/packages/contracts-bedrock/scripts/Artifacts.s.sol b/packages/contracts-bedrock/scripts/Artifacts.s.sol index 0069bfa30f94..d43bd6915455 100644 --- a/packages/contracts-bedrock/scripts/Artifacts.s.sol +++ b/packages/contracts-bedrock/scripts/Artifacts.s.sol @@ -183,6 +183,7 @@ abstract contract Artifacts { /// @param _name The name of the deployment. /// @param _deployed The address of the deployment. function save(string memory _name, address _deployed) public { + console.log("Saving %s: %s", _name, _deployed); if (bytes(_name).length == 0) { revert InvalidDeployment("EmptyName"); } @@ -190,7 +191,6 @@ abstract contract Artifacts { revert InvalidDeployment("AlreadyExists"); } - console.log("Saving %s: %s", _name, _deployed); Deployment memory deployment = Deployment({ name: _name, addr: payable(_deployed) }); _namedDeployments[_name] = deployment; _newDeployments.push(deployment); diff --git a/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol b/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol index 3f70d78b2049..0aff98d54409 100644 --- a/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol @@ -27,11 +27,13 @@ import { // Contracts import { AddressManager } from "src/legacy/AddressManager.sol"; import { StorageSetter } from "src/universal/StorageSetter.sol"; +import { OPContractsManager } from "src/L1/OPContractsManager.sol"; // Libraries import { Constants } from "src/libraries/Constants.sol"; import { Types } from "scripts/libraries/Types.sol"; import { Duration } from "src/dispute/lib/LibUDT.sol"; +import { StorageSlot, ForgeArtifacts } from "scripts/libraries/ForgeArtifacts.sol"; import "src/dispute/lib/Types.sol"; // Interfaces @@ -39,8 +41,6 @@ import { IProxy } from "src/universal/interfaces/IProxy.sol"; import { IProxyAdmin } from "src/universal/interfaces/IProxyAdmin.sol"; import { IOptimismPortal } from "src/L1/interfaces/IOptimismPortal.sol"; import { IOptimismPortal2 } from "src/L1/interfaces/IOptimismPortal2.sol"; -import { ICrossDomainMessenger } from "src/universal/interfaces/ICrossDomainMessenger.sol"; -import { IL1CrossDomainMessenger } from "src/L1/interfaces/IL1CrossDomainMessenger.sol"; import { IL2OutputOracle } from "src/L1/interfaces/IL2OutputOracle.sol"; import { ISuperchainConfig } from "src/L1/interfaces/ISuperchainConfig.sol"; import { ISystemConfig } from "src/L1/interfaces/ISystemConfig.sol"; @@ -52,13 +52,11 @@ import { IBigStepper } from "src/dispute/interfaces/IBigStepper.sol"; import { IDisputeGameFactory } from "src/dispute/interfaces/IDisputeGameFactory.sol"; import { IDisputeGame } from "src/dispute/interfaces/IDisputeGame.sol"; import { IFaultDisputeGame } from "src/dispute/interfaces/IFaultDisputeGame.sol"; -import { IPermissionedDisputeGame } from "src/dispute/interfaces/IPermissionedDisputeGame.sol"; import { IDelayedWETH } from "src/dispute/interfaces/IDelayedWETH.sol"; import { IAnchorStateRegistry } from "src/dispute/interfaces/IAnchorStateRegistry.sol"; import { IMIPS } from "src/cannon/interfaces/IMIPS.sol"; import { IMIPS2 } from "src/cannon/interfaces/IMIPS2.sol"; import { IPreimageOracle } from "src/cannon/interfaces/IPreimageOracle.sol"; -import { IOptimismMintableERC20Factory } from "src/universal/interfaces/IOptimismMintableERC20Factory.sol"; import { IAddressManager } from "src/legacy/interfaces/IAddressManager.sol"; import { IL1ChugSplashProxy } from "src/legacy/interfaces/IL1ChugSplashProxy.sol"; import { IResolvedDelegateProxy } from "src/legacy/interfaces/IResolvedDelegateProxy.sol"; @@ -285,6 +283,31 @@ contract Deploy is Deployer { // Deploy Current OPChain Contracts deployOpChain(); + // Apply modifications for non-standard configurations not supported by the OPCM deployment + if (cfg.useFaultProofs()) { + vm.startPrank(ISuperchainConfig(mustGetAddress("SuperchainConfigProxy")).guardian()); + IOptimismPortal2(mustGetAddress("OptimismPortalProxy")).setRespectedGameType( + GameType.wrap(uint32(cfg.respectedGameType())) + ); + vm.stopPrank(); + } else { + // The L2OutputOracle is not deployed by the OPCM, we deploy the proxy and initialize it here. + deployERC1967Proxy("L2OutputOracleProxy"); + initializeL2OutputOracle(); + + // The OptimismPortalProxy contract is used both with and without Fault Proofs enabled, and is deployed by + // deployOPChain. If Fault Proofs are disabled, then we need to reinitialize the OptimismPortalProxy + // as the legacy OptimismPortal. + resetInitializedProxy("OptimismPortal"); + initializeOptimismPortal(); + } + + if (cfg.useCustomGasToken()) { + // Reset the systemconfig then reinitialize it with the custom gas token + resetInitializedProxy("SystemConfig"); + initializeSystemConfig(); + } + if (cfg.useAltDA()) { bytes32 typeHash = keccak256(bytes(cfg.daCommitmentType())); bytes32 keccakHash = keccak256(bytes("KeccakCommitment")); @@ -421,67 +444,55 @@ contract Deploy is Deployer { /// @notice Deploy all of the OP Chain specific contracts function deployOpChain() public { console.log("Deploying OP Chain"); - deployAddressManager(); - deployProxyAdmin(); - transferAddressManagerOwnership(); // to the ProxyAdmin - // Ensure that the requisite contracts are deployed - mustGetAddress("SuperchainConfigProxy"); - mustGetAddress("AddressManager"); - mustGetAddress("ProxyAdmin"); - - deployERC1967Proxy("OptimismPortalProxy"); - deployERC1967Proxy("SystemConfigProxy"); - deployL1StandardBridgeProxy(); - deployL1CrossDomainMessengerProxy(); - deployERC1967Proxy("OptimismMintableERC20FactoryProxy"); - deployERC1967Proxy("L1ERC721BridgeProxy"); - - // Both the DisputeGameFactory and L2OutputOracle proxies are deployed regardless of whether fault proofs is - // enabled to prevent a nastier refactor to the deploy scripts. In the future, the L2OutputOracle will be - // removed. If fault proofs are not enabled, the DisputeGameFactory proxy will be unused. - deployERC1967Proxy("DisputeGameFactoryProxy"); - deployERC1967Proxy("DelayedWETHProxy"); - deployERC1967Proxy("PermissionedDelayedWETHProxy"); - deployERC1967Proxy("AnchorStateRegistryProxy"); - - deployAnchorStateRegistry(); - - // Deploy and setup the legacy (pre-faultproofs) contracts - if (!cfg.useFaultProofs()) { - deployERC1967Proxy("L2OutputOracleProxy"); - } + address superchainConfigProxy = mustGetAddress("SuperchainConfigProxy"); + OPContractsManager opcm = OPContractsManager(mustGetAddress("OPContractsManagerProxy")); + + OPContractsManager.DeployInput memory deployInput = getDeployInput(); + OPContractsManager.DeployOutput memory deployOutput = opcm.deploy(deployInput); + + // Save all deploy outputs from the OPCM, in the order they are declared in the DeployOutput struct + save("ProxyAdmin", address(deployOutput.opChainProxyAdmin)); + save("AddressManager", address(deployOutput.addressManager)); + save("L1ERC721BridgeProxy", address(deployOutput.l1ERC721BridgeProxy)); + save("SystemConfigProxy", address(deployOutput.systemConfigProxy)); + save("OptimismMintableERC20FactoryProxy", address(deployOutput.optimismMintableERC20FactoryProxy)); + save("L1StandardBridgeProxy", address(deployOutput.l1StandardBridgeProxy)); + save("L1CrossDomainMessengerProxy", address(deployOutput.l1CrossDomainMessengerProxy)); + + // Fault Proof contracts + save("DisputeGameFactoryProxy", address(deployOutput.disputeGameFactoryProxy)); + save("PermissionedDelayedWETHProxy", address(deployOutput.delayedWETHPermissionedGameProxy)); + save("AnchorStateRegistryProxy", address(deployOutput.anchorStateRegistryProxy)); + save("AnchorStateRegistry", address(deployOutput.anchorStateRegistryImpl)); + save("PermissionedDisputeGame", address(deployOutput.permissionedDisputeGame)); + save("OptimismPortalProxy", address(deployOutput.optimismPortalProxy)); + + // Check if the permissionless game implementation is already set + IDisputeGameFactory factory = IDisputeGameFactory(mustGetAddress("DisputeGameFactoryProxy")); + address permissionlessGameImpl = address(factory.gameImpls(GameTypes.CANNON)); - initializeOpChain(); + // Deploy and setup the PermissionlessDelayedWeth not provided by the OPCM. + // If the following require statement is hit, you can delete the block of code after it. + require( + permissionlessGameImpl == address(0), + "Deploy: The PermissionlessDelayedWETH is already set by the OPCM, it is no longer necessary to deploy it separately." + ); + address delayedWETHImpl = mustGetAddress("DelayedWETH"); + address delayedWETHPermissionlessGameProxy = deployERC1967ProxyWithOwner("DelayedWETHProxy", msg.sender); + vm.broadcast(msg.sender); + IProxy(payable(delayedWETHPermissionlessGameProxy)).upgradeToAndCall({ + _implementation: delayedWETHImpl, + _data: abi.encodeCall(IDelayedWETH.initialize, (msg.sender, ISuperchainConfig(superchainConfigProxy))) + }); setAlphabetFaultGameImplementation({ _allowUpgrade: false }); setFastFaultGameImplementation({ _allowUpgrade: false }); setCannonFaultGameImplementation({ _allowUpgrade: false }); - setPermissionedCannonFaultGameImplementation({ _allowUpgrade: false }); transferDisputeGameFactoryOwnership(); transferDelayedWETHOwnership(); - } - - /// @notice Initialize all of the proxies in an OP Chain by upgrading to the correct proxy and calling the - /// initialize function - function initializeOpChain() public { - console.log("Initializing Op Chain proxies"); - - initializeOptimismPortal(); - initializeSystemConfig(); - initializeL1StandardBridge(); - initializeL1ERC721Bridge(); - initializeOptimismMintableERC20Factory(); - initializeL1CrossDomainMessenger(); - initializeDisputeGameFactory(); - initializeDelayedWETH(); - initializePermissionedDelayedWETH(); - initializeAnchorStateRegistry(); - - if (!cfg.useFaultProofs()) { - initializeL2OutputOracle(); - } + transferPermissionedDelayedWETHOwnership(); } /// @notice Add AltDA setup to the OP chain @@ -638,50 +649,22 @@ contract Deploy is Deployer { /// @notice Deploy the OptimismPortal function deployOptimismPortal() public broadcast returns (address addr_) { - if (cfg.useFaultProofs()) { - // Could also verify this inside DeployConfig but doing it here is a bit more reliable. - require( - uint32(cfg.respectedGameType()) == cfg.respectedGameType(), - "Deploy: respectedGameType must fit into uint32" - ); + require(!cfg.useFaultProofs(), "Deploy: FaultProofs OptimismPortal is deployed by OPCM"); + require(!cfg.useInterop(), "Deploy: The legacy OptimismPortal does not support interop"); - addr_ = DeployUtils.create2AndSave({ - _save: this, - _salt: _implSalt(), - _name: "OptimismPortal2", - _args: DeployUtils.encodeConstructor( - abi.encodeCall( - IOptimismPortal2.__constructor__, - (cfg.proofMaturityDelaySeconds(), cfg.disputeGameFinalityDelaySeconds()) - ) - ) - }); - - // Override the `OptimismPortal2` contract to the deployed implementation. This is necessary - // to check the `OptimismPortal2` implementation alongside dependent contracts, which - // are always proxies. - Types.ContractSet memory contracts = _proxies(); - contracts.OptimismPortal2 = addr_; - ChainAssertions.checkOptimismPortal2({ _contracts: contracts, _cfg: cfg, _isProxy: false }); - } else { - if (cfg.useInterop()) { - console.log("Attempting to deploy OptimismPortal with interop, this config is a noop"); - } + addr_ = DeployUtils.create2AndSave({ + _save: this, + _salt: _implSalt(), + _name: "OptimismPortal", + _args: DeployUtils.encodeConstructor(abi.encodeCall(IOptimismPortal.__constructor__, ())) + }); - addr_ = DeployUtils.create2AndSave({ - _save: this, - _salt: _implSalt(), - _name: "OptimismPortal", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IOptimismPortal.__constructor__, ())) - }); - - // Override the `OptimismPortal` contract to the deployed implementation. This is necessary - // to check the `OptimismPortal` implementation alongside dependent contracts, which - // are always proxies. - Types.ContractSet memory contracts = _proxies(); - contracts.OptimismPortal = addr_; - ChainAssertions.checkOptimismPortal({ _contracts: contracts, _cfg: cfg, _isProxy: false }); - } + // Override the `OptimismPortal` contract to the deployed implementation. This is necessary + // to check the `OptimismPortal` implementation alongside dependent contracts, which + // are always proxies. + Types.ContractSet memory contracts = _proxies(); + contracts.OptimismPortal = addr_; + ChainAssertions.checkOptimismPortal({ _contracts: contracts, _cfg: cfg, _isProxy: false }); } /// @notice Deploy the L2OutputOracle @@ -816,127 +799,6 @@ contract Deploy is Deployer { // Initialize Functions // //////////////////////////////////////////////////////////////// - /// @notice Initialize the DisputeGameFactory - function initializeDisputeGameFactory() public broadcast { - console.log("Upgrading and initializing DisputeGameFactory proxy"); - address disputeGameFactoryProxy = mustGetAddress("DisputeGameFactoryProxy"); - address disputeGameFactory = mustGetAddress("DisputeGameFactory"); - - IProxyAdmin proxyAdmin = IProxyAdmin(payable(mustGetAddress("ProxyAdmin"))); - proxyAdmin.upgradeAndCall({ - _proxy: payable(disputeGameFactoryProxy), - _implementation: disputeGameFactory, - _data: abi.encodeCall(IDisputeGameFactory.initialize, (msg.sender)) - }); - - string memory version = IDisputeGameFactory(disputeGameFactoryProxy).version(); - console.log("DisputeGameFactory version: %s", version); - - ChainAssertions.checkDisputeGameFactory({ _contracts: _proxies(), _expectedOwner: msg.sender, _isProxy: true }); - } - - function initializeDelayedWETH() public broadcast { - console.log("Upgrading and initializing DelayedWETH proxy"); - address delayedWETHProxy = mustGetAddress("DelayedWETHProxy"); - address delayedWETH = mustGetAddress("DelayedWETH"); - address superchainConfigProxy = mustGetAddress("SuperchainConfigProxy"); - - IProxyAdmin proxyAdmin = IProxyAdmin(payable(mustGetAddress("ProxyAdmin"))); - proxyAdmin.upgradeAndCall({ - _proxy: payable(delayedWETHProxy), - _implementation: delayedWETH, - _data: abi.encodeCall(IDelayedWETH.initialize, (msg.sender, ISuperchainConfig(superchainConfigProxy))) - }); - - string memory version = IDelayedWETH(payable(delayedWETHProxy)).version(); - console.log("DelayedWETH version: %s", version); - - ChainAssertions.checkDelayedWETH({ - _contracts: _proxies(), - _cfg: cfg, - _isProxy: true, - _expectedOwner: msg.sender - }); - } - - function initializePermissionedDelayedWETH() public broadcast { - console.log("Upgrading and initializing permissioned DelayedWETH proxy"); - address delayedWETHProxy = mustGetAddress("PermissionedDelayedWETHProxy"); - address delayedWETH = mustGetAddress("DelayedWETH"); - address superchainConfigProxy = mustGetAddress("SuperchainConfigProxy"); - - IProxyAdmin proxyAdmin = IProxyAdmin(payable(mustGetAddress("ProxyAdmin"))); - proxyAdmin.upgradeAndCall({ - _proxy: payable(delayedWETHProxy), - _implementation: delayedWETH, - _data: abi.encodeCall(IDelayedWETH.initialize, (msg.sender, ISuperchainConfig(superchainConfigProxy))) - }); - - string memory version = IDelayedWETH(payable(delayedWETHProxy)).version(); - console.log("DelayedWETH version: %s", version); - - ChainAssertions.checkPermissionedDelayedWETH({ - _contracts: _proxies(), - _cfg: cfg, - _isProxy: true, - _expectedOwner: msg.sender - }); - } - - function initializeAnchorStateRegistry() public broadcast { - console.log("Upgrading and initializing AnchorStateRegistry proxy"); - address anchorStateRegistryProxy = mustGetAddress("AnchorStateRegistryProxy"); - address anchorStateRegistry = mustGetAddress("AnchorStateRegistry"); - ISuperchainConfig superchainConfig = ISuperchainConfig(mustGetAddress("SuperchainConfigProxy")); - - IAnchorStateRegistry.StartingAnchorRoot[] memory roots = new IAnchorStateRegistry.StartingAnchorRoot[](5); - roots[0] = IAnchorStateRegistry.StartingAnchorRoot({ - gameType: GameTypes.CANNON, - outputRoot: OutputRoot({ - root: Hash.wrap(cfg.faultGameGenesisOutputRoot()), - l2BlockNumber: cfg.faultGameGenesisBlock() - }) - }); - roots[1] = IAnchorStateRegistry.StartingAnchorRoot({ - gameType: GameTypes.PERMISSIONED_CANNON, - outputRoot: OutputRoot({ - root: Hash.wrap(cfg.faultGameGenesisOutputRoot()), - l2BlockNumber: cfg.faultGameGenesisBlock() - }) - }); - roots[2] = IAnchorStateRegistry.StartingAnchorRoot({ - gameType: GameTypes.ALPHABET, - outputRoot: OutputRoot({ - root: Hash.wrap(cfg.faultGameGenesisOutputRoot()), - l2BlockNumber: cfg.faultGameGenesisBlock() - }) - }); - roots[3] = IAnchorStateRegistry.StartingAnchorRoot({ - gameType: GameTypes.ASTERISC, - outputRoot: OutputRoot({ - root: Hash.wrap(cfg.faultGameGenesisOutputRoot()), - l2BlockNumber: cfg.faultGameGenesisBlock() - }) - }); - roots[4] = IAnchorStateRegistry.StartingAnchorRoot({ - gameType: GameTypes.FAST, - outputRoot: OutputRoot({ - root: Hash.wrap(cfg.faultGameGenesisOutputRoot()), - l2BlockNumber: cfg.faultGameGenesisBlock() - }) - }); - - IProxyAdmin proxyAdmin = IProxyAdmin(payable(mustGetAddress("ProxyAdmin"))); - proxyAdmin.upgradeAndCall({ - _proxy: payable(anchorStateRegistryProxy), - _implementation: anchorStateRegistry, - _data: abi.encodeCall(IAnchorStateRegistry.initialize, (roots, superchainConfig)) - }); - - string memory version = IAnchorStateRegistry(payable(anchorStateRegistryProxy)).version(); - console.log("AnchorStateRegistry version: %s", version); - } - /// @notice Initialize the SystemConfig function initializeSystemConfig() public broadcast { console.log("Upgrading and initializing SystemConfig proxy"); @@ -985,133 +847,6 @@ contract Deploy is Deployer { ChainAssertions.checkSystemConfig({ _contracts: _proxies(), _cfg: cfg, _isProxy: true }); } - /// @notice Initialize the L1StandardBridge - function initializeL1StandardBridge() public broadcast { - console.log("Upgrading and initializing L1StandardBridge proxy"); - IProxyAdmin proxyAdmin = IProxyAdmin(mustGetAddress("ProxyAdmin")); - address l1StandardBridgeProxy = mustGetAddress("L1StandardBridgeProxy"); - address l1StandardBridge = mustGetAddress("L1StandardBridge"); - address l1CrossDomainMessengerProxy = mustGetAddress("L1CrossDomainMessengerProxy"); - address superchainConfigProxy = mustGetAddress("SuperchainConfigProxy"); - address systemConfigProxy = mustGetAddress("SystemConfigProxy"); - - uint256 proxyType = uint256(proxyAdmin.proxyType(l1StandardBridgeProxy)); - if (proxyType != uint256(IProxyAdmin.ProxyType.CHUGSPLASH)) { - proxyAdmin.setProxyType(l1StandardBridgeProxy, IProxyAdmin.ProxyType.CHUGSPLASH); - } - require(uint256(proxyAdmin.proxyType(l1StandardBridgeProxy)) == uint256(IProxyAdmin.ProxyType.CHUGSPLASH)); - - proxyAdmin.upgradeAndCall({ - _proxy: payable(l1StandardBridgeProxy), - _implementation: l1StandardBridge, - _data: abi.encodeCall( - IL1StandardBridge.initialize, - ( - ICrossDomainMessenger(l1CrossDomainMessengerProxy), - ISuperchainConfig(superchainConfigProxy), - ISystemConfig(systemConfigProxy) - ) - ) - }); - - string memory version = IL1StandardBridge(payable(l1StandardBridgeProxy)).version(); - console.log("L1StandardBridge version: %s", version); - - ChainAssertions.checkL1StandardBridge({ _contracts: _proxies(), _isProxy: true }); - } - - /// @notice Initialize the L1ERC721Bridge - function initializeL1ERC721Bridge() public broadcast { - console.log("Upgrading and initializing L1ERC721Bridge proxy"); - address l1ERC721BridgeProxy = mustGetAddress("L1ERC721BridgeProxy"); - address l1ERC721Bridge = mustGetAddress("L1ERC721Bridge"); - address l1CrossDomainMessengerProxy = mustGetAddress("L1CrossDomainMessengerProxy"); - address superchainConfigProxy = mustGetAddress("SuperchainConfigProxy"); - - IProxyAdmin proxyAdmin = IProxyAdmin(payable(mustGetAddress("ProxyAdmin"))); - proxyAdmin.upgradeAndCall({ - _proxy: payable(l1ERC721BridgeProxy), - _implementation: l1ERC721Bridge, - _data: abi.encodeCall( - IL1ERC721Bridge.initialize, - (ICrossDomainMessenger(payable(l1CrossDomainMessengerProxy)), ISuperchainConfig(superchainConfigProxy)) - ) - }); - - IL1ERC721Bridge bridge = IL1ERC721Bridge(l1ERC721BridgeProxy); - string memory version = bridge.version(); - console.log("L1ERC721Bridge version: %s", version); - - ChainAssertions.checkL1ERC721Bridge({ _contracts: _proxies(), _isProxy: true }); - } - - /// @notice Initialize the OptimismMintableERC20Factory - function initializeOptimismMintableERC20Factory() public broadcast { - console.log("Upgrading and initializing OptimismMintableERC20Factory proxy"); - address optimismMintableERC20FactoryProxy = mustGetAddress("OptimismMintableERC20FactoryProxy"); - address optimismMintableERC20Factory = mustGetAddress("OptimismMintableERC20Factory"); - address l1StandardBridgeProxy = mustGetAddress("L1StandardBridgeProxy"); - - IProxyAdmin proxyAdmin = IProxyAdmin(payable(mustGetAddress("ProxyAdmin"))); - proxyAdmin.upgradeAndCall({ - _proxy: payable(optimismMintableERC20FactoryProxy), - _implementation: optimismMintableERC20Factory, - _data: abi.encodeCall(IOptimismMintableERC20Factory.initialize, (l1StandardBridgeProxy)) - }); - - IOptimismMintableERC20Factory factory = IOptimismMintableERC20Factory(optimismMintableERC20FactoryProxy); - string memory version = factory.version(); - console.log("OptimismMintableERC20Factory version: %s", version); - - ChainAssertions.checkOptimismMintableERC20Factory({ _contracts: _proxies(), _isProxy: true }); - } - - /// @notice initializeL1CrossDomainMessenger - function initializeL1CrossDomainMessenger() public broadcast { - console.log("Upgrading and initializing L1CrossDomainMessenger proxy"); - IProxyAdmin proxyAdmin = IProxyAdmin(mustGetAddress("ProxyAdmin")); - address l1CrossDomainMessengerProxy = mustGetAddress("L1CrossDomainMessengerProxy"); - address l1CrossDomainMessenger = mustGetAddress("L1CrossDomainMessenger"); - address superchainConfigProxy = mustGetAddress("SuperchainConfigProxy"); - address optimismPortalProxy = mustGetAddress("OptimismPortalProxy"); - address systemConfigProxy = mustGetAddress("SystemConfigProxy"); - - uint256 proxyType = uint256(proxyAdmin.proxyType(l1CrossDomainMessengerProxy)); - if (proxyType != uint256(IProxyAdmin.ProxyType.RESOLVED)) { - proxyAdmin.setProxyType(l1CrossDomainMessengerProxy, IProxyAdmin.ProxyType.RESOLVED); - } - require(uint256(proxyAdmin.proxyType(l1CrossDomainMessengerProxy)) == uint256(IProxyAdmin.ProxyType.RESOLVED)); - - string memory contractName = "OVM_L1CrossDomainMessenger"; - string memory implName = proxyAdmin.implementationName(l1CrossDomainMessenger); - if (keccak256(bytes(contractName)) != keccak256(bytes(implName))) { - proxyAdmin.setImplementationName(l1CrossDomainMessengerProxy, contractName); - } - require( - keccak256(bytes(proxyAdmin.implementationName(l1CrossDomainMessengerProxy))) - == keccak256(bytes(contractName)) - ); - - proxyAdmin.upgradeAndCall({ - _proxy: payable(l1CrossDomainMessengerProxy), - _implementation: l1CrossDomainMessenger, - _data: abi.encodeCall( - IL1CrossDomainMessenger.initialize, - ( - ISuperchainConfig(superchainConfigProxy), - IOptimismPortal(payable(optimismPortalProxy)), - ISystemConfig(systemConfigProxy) - ) - ) - }); - - IL1CrossDomainMessenger messenger = IL1CrossDomainMessenger(l1CrossDomainMessengerProxy); - string memory version = messenger.version(); - console.log("L1CrossDomainMessenger version: %s", version); - - ChainAssertions.checkL1CrossDomainMessenger({ _contracts: _proxies(), _vm: vm, _isProxy: true }); - } - /// @notice Initialize the L2OutputOracle function initializeL2OutputOracle() public broadcast { console.log("Upgrading and initializing L2OutputOracle proxy"); @@ -1150,59 +885,33 @@ contract Deploy is Deployer { /// @notice Initialize the OptimismPortal function initializeOptimismPortal() public broadcast { + console.log("Upgrading and initializing OptimismPortal proxy"); + require(!cfg.useFaultProofs(), "Deploy: FaultProofs OptimismPortal is initialized by OPCM"); address optimismPortalProxy = mustGetAddress("OptimismPortalProxy"); address systemConfigProxy = mustGetAddress("SystemConfigProxy"); address superchainConfigProxy = mustGetAddress("SuperchainConfigProxy"); - if (cfg.useFaultProofs()) { - console.log("Upgrading and initializing OptimismPortal2 proxy"); - address optimismPortal2 = mustGetAddress("OptimismPortal2"); - address disputeGameFactoryProxy = mustGetAddress("DisputeGameFactoryProxy"); - - IProxyAdmin proxyAdmin = IProxyAdmin(payable(mustGetAddress("ProxyAdmin"))); - proxyAdmin.upgradeAndCall({ - _proxy: payable(optimismPortalProxy), - _implementation: optimismPortal2, - _data: abi.encodeCall( - IOptimismPortal2.initialize, - ( - IDisputeGameFactory(disputeGameFactoryProxy), - ISystemConfig(systemConfigProxy), - ISuperchainConfig(superchainConfigProxy), - GameType.wrap(uint32(cfg.respectedGameType())) - ) - ) - }); - - IOptimismPortal2 portal = IOptimismPortal2(payable(optimismPortalProxy)); - string memory version = portal.version(); - console.log("OptimismPortal2 version: %s", version); + address optimismPortal = mustGetAddress("OptimismPortal"); + address l2OutputOracleProxy = mustGetAddress("L2OutputOracleProxy"); - ChainAssertions.checkOptimismPortal2({ _contracts: _proxies(), _cfg: cfg, _isProxy: true }); - } else { - console.log("Upgrading and initializing OptimismPortal proxy"); - address optimismPortal = mustGetAddress("OptimismPortal"); - address l2OutputOracleProxy = mustGetAddress("L2OutputOracleProxy"); - - IProxyAdmin proxyAdmin = IProxyAdmin(payable(mustGetAddress("ProxyAdmin"))); - proxyAdmin.upgradeAndCall({ - _proxy: payable(optimismPortalProxy), - _implementation: optimismPortal, - _data: abi.encodeCall( - IOptimismPortal.initialize, - ( - IL2OutputOracle(l2OutputOracleProxy), - ISystemConfig(systemConfigProxy), - ISuperchainConfig(superchainConfigProxy) - ) + IProxyAdmin proxyAdmin = IProxyAdmin(payable(mustGetAddress("ProxyAdmin"))); + proxyAdmin.upgradeAndCall({ + _proxy: payable(optimismPortalProxy), + _implementation: optimismPortal, + _data: abi.encodeCall( + IOptimismPortal.initialize, + ( + IL2OutputOracle(l2OutputOracleProxy), + ISystemConfig(systemConfigProxy), + ISuperchainConfig(superchainConfigProxy) ) - }); + ) + }); - IOptimismPortal portal = IOptimismPortal(payable(optimismPortalProxy)); - string memory version = portal.version(); - console.log("OptimismPortal version: %s", version); + IOptimismPortal portal = IOptimismPortal(payable(optimismPortalProxy)); + string memory version = portal.version(); + console.log("OptimismPortal version: %s", version); - ChainAssertions.checkOptimismPortal({ _contracts: _proxies(), _cfg: cfg, _isProxy: true }); - } + ChainAssertions.checkOptimismPortal({ _contracts: _proxies(), _cfg: cfg, _isProxy: true }); } /// @notice Transfer ownership of the DisputeGameFactory contract to the final system owner @@ -1349,7 +1058,7 @@ contract Deploy is Deployer { IDisputeGameFactory factory = IDisputeGameFactory(mustGetAddress("DisputeGameFactoryProxy")); IDelayedWETH weth = IDelayedWETH(mustGetAddress("PermissionedDelayedWETHProxy")); - // Set the Cannon FaultDisputeGame implementation in the factory. + // Deploys and sets the Permissioned FaultDisputeGame implementation in the factory. _setFaultGameImplementation({ _factory: factory, _allowUpgrade: _allowUpgrade, @@ -1442,72 +1151,41 @@ contract Deploy is Deployer { // Redefine _param variable to avoid stack too deep error during compilation FaultDisputeGameParams memory _params_ = _params; - if (rawGameType != GameTypes.PERMISSIONED_CANNON.raw()) { - _factory.setImplementation( - _params_.gameType, - IDisputeGame( - DeployUtils.create2AndSave({ - _save: this, - _salt: _implSalt(), - _name: "FaultDisputeGame", - _nick: string.concat("FaultDisputeGame_", vm.toString(rawGameType)), - _args: DeployUtils.encodeConstructor( - abi.encodeCall( - IFaultDisputeGame.__constructor__, - ( - _params_.gameType, - _params_.absolutePrestate, - _params_.maxGameDepth, - cfg.faultGameSplitDepth(), - Duration.wrap(uint64(cfg.faultGameClockExtension())), - _params_.maxClockDuration, - _params_.faultVm, - _params_.weth, - IAnchorStateRegistry(mustGetAddress("AnchorStateRegistryProxy")), - cfg.l2ChainID() - ) - ) - ) - }) - ) - ); - } else { - _factory.setImplementation( - _params_.gameType, - IDisputeGame( - DeployUtils.create2AndSave({ - _save: this, - _salt: _implSalt(), - _name: "PermissionedDisputeGame", - _args: DeployUtils.encodeConstructor( - abi.encodeCall( - IPermissionedDisputeGame.__constructor__, - ( - _params_.gameType, - _params_.absolutePrestate, - _params_.maxGameDepth, - cfg.faultGameSplitDepth(), - Duration.wrap(uint64(cfg.faultGameClockExtension())), - _params_.maxClockDuration, - _params_.faultVm, - _params_.weth, - _params_.anchorStateRegistry, - cfg.l2ChainID(), - cfg.l2OutputOracleProposer(), - cfg.l2OutputOracleChallenger() - ) + require( + rawGameType != GameTypes.PERMISSIONED_CANNON.raw(), "Deploy: Permissioned Game should be deployed by OPCM" + ); + _factory.setImplementation( + _params_.gameType, + IDisputeGame( + DeployUtils.create2AndSave({ + _save: this, + _salt: _implSalt(), + _name: "FaultDisputeGame", + _nick: string.concat("FaultDisputeGame_", vm.toString(rawGameType)), + _args: DeployUtils.encodeConstructor( + abi.encodeCall( + IFaultDisputeGame.__constructor__, + ( + _params_.gameType, + _params_.absolutePrestate, + _params_.maxGameDepth, + cfg.faultGameSplitDepth(), + Duration.wrap(uint64(cfg.faultGameClockExtension())), + _params_.maxClockDuration, + _params_.faultVm, + _params_.weth, + IAnchorStateRegistry(mustGetAddress("AnchorStateRegistryProxy")), + cfg.l2ChainID() ) ) - }) - ) - ); - } + ) + }) + ) + ); string memory gameTypeString; if (rawGameType == GameTypes.CANNON.raw()) { gameTypeString = "Cannon"; - } else if (rawGameType == GameTypes.PERMISSIONED_CANNON.raw()) { - gameTypeString = "PermissionedCannon"; } else if (rawGameType == GameTypes.ALPHABET.raw()) { gameTypeString = "Alphabet"; } else { @@ -1553,4 +1231,60 @@ contract Deploy is Deployer { require(dac.bondSize() == daBondSize); require(dac.resolverRefundPercentage() == daResolverRefundPercentage); } + + /// @notice Get the DeployInput struct to use for testing + function getDeployInput() public view returns (OPContractsManager.DeployInput memory) { + OutputRoot memory testOutputRoot = OutputRoot({ + root: Hash.wrap(cfg.faultGameGenesisOutputRoot()), + l2BlockNumber: cfg.faultGameGenesisBlock() + }); + IAnchorStateRegistry.StartingAnchorRoot[] memory startingAnchorRoots = + new IAnchorStateRegistry.StartingAnchorRoot[](5); + startingAnchorRoots[0] = + IAnchorStateRegistry.StartingAnchorRoot({ gameType: GameTypes.CANNON, outputRoot: testOutputRoot }); + startingAnchorRoots[1] = IAnchorStateRegistry.StartingAnchorRoot({ + gameType: GameTypes.PERMISSIONED_CANNON, + outputRoot: testOutputRoot + }); + startingAnchorRoots[2] = + IAnchorStateRegistry.StartingAnchorRoot({ gameType: GameTypes.ASTERISC, outputRoot: testOutputRoot }); + startingAnchorRoots[3] = + IAnchorStateRegistry.StartingAnchorRoot({ gameType: GameTypes.FAST, outputRoot: testOutputRoot }); + startingAnchorRoots[4] = + IAnchorStateRegistry.StartingAnchorRoot({ gameType: GameTypes.ALPHABET, outputRoot: testOutputRoot }); + string memory saltMixer = "salt mixer"; + return OPContractsManager.DeployInput({ + roles: OPContractsManager.Roles({ + opChainProxyAdminOwner: msg.sender, + systemConfigOwner: cfg.finalSystemOwner(), + batcher: cfg.batchSenderAddress(), + unsafeBlockSigner: cfg.p2pSequencerAddress(), + proposer: cfg.l2OutputOracleProposer(), + challenger: cfg.l2OutputOracleChallenger() + }), + basefeeScalar: cfg.basefeeScalar(), + blobBasefeeScalar: cfg.blobbasefeeScalar(), + l2ChainId: cfg.l2ChainID(), + startingAnchorRoots: abi.encode(startingAnchorRoots), + saltMixer: saltMixer, + gasLimit: uint64(cfg.l2GenesisBlockGasLimit()), + disputeGameType: GameTypes.PERMISSIONED_CANNON, + disputeAbsolutePrestate: Claim.wrap(bytes32(cfg.faultGameAbsolutePrestate())), + disputeMaxGameDepth: cfg.faultGameMaxDepth(), + disputeSplitDepth: cfg.faultGameSplitDepth(), + disputeClockExtension: Duration.wrap(uint64(cfg.faultGameClockExtension())), + disputeMaxClockDuration: Duration.wrap(uint64(cfg.faultGameMaxClockDuration())) + }); + } + + function resetInitializedProxy(string memory _contractName) internal { + console.log("resetting initialized value on %s Proxy", _contractName); + address proxy = mustGetAddress(string.concat(_contractName, "Proxy")); + StorageSlot memory slot = ForgeArtifacts.getInitializedSlot(_contractName); + bytes32 slotVal = vm.load(proxy, bytes32(vm.parseUint(slot.slot))); + uint256 value = uint256(slotVal); + value = value & ~(0xFF << (slot.offset * 8)); + slotVal = bytes32(value); + vm.store(proxy, bytes32(vm.parseUint(slot.slot)), slotVal); + } } diff --git a/packages/contracts-bedrock/scripts/libraries/Constants.sol b/packages/contracts-bedrock/scripts/libraries/Constants.sol index 093bb04369a9..603084ac6eef 100644 --- a/packages/contracts-bedrock/scripts/libraries/Constants.sol +++ b/packages/contracts-bedrock/scripts/libraries/Constants.sol @@ -10,12 +10,16 @@ import { GameTypes, OutputRoot, Hash } from "src/dispute/lib/Types.sol"; /// should be defined in that contract instead. library Constants { /// @notice Returns the default starting anchor roots value to be used in a new dispute game. + function DEFAULT_OUTPUT_ROOT() internal pure returns (OutputRoot memory) { + return OutputRoot({ root: Hash.wrap(bytes32(hex"dead")), l2BlockNumber: 0 }); + } + function DEFAULT_STARTING_ANCHOR_ROOTS() internal pure returns (IAnchorStateRegistry.StartingAnchorRoot[] memory) { IAnchorStateRegistry.StartingAnchorRoot[] memory defaultStartingAnchorRoots = new IAnchorStateRegistry.StartingAnchorRoot[](1); defaultStartingAnchorRoots[0] = IAnchorStateRegistry.StartingAnchorRoot({ gameType: GameTypes.PERMISSIONED_CANNON, - outputRoot: OutputRoot({ root: Hash.wrap(bytes32(hex"dead")), l2BlockNumber: 0 }) + outputRoot: DEFAULT_OUTPUT_ROOT() }); return defaultStartingAnchorRoots; } diff --git a/packages/contracts-bedrock/test/L1/SystemConfigInterop.t.sol b/packages/contracts-bedrock/test/L1/SystemConfigInterop.t.sol index 0e47529c760c..0f2c51b4bf3b 100644 --- a/packages/contracts-bedrock/test/L1/SystemConfigInterop.t.sol +++ b/packages/contracts-bedrock/test/L1/SystemConfigInterop.t.sol @@ -80,7 +80,9 @@ contract SystemConfigInterop_Test is CommonTest { /// @dev Tests that adding a dependency as not the dependency manager reverts. function testFuzz_addDependency_notDependencyManager_reverts(uint256 _chainId) public { + require(alice != _systemConfigInterop().dependencyManager(), "SystemConfigInterop_Test-100"); vm.expectRevert("SystemConfig: caller is not the dependency manager"); + vm.prank(alice); _systemConfigInterop().addDependency(_chainId); } @@ -100,7 +102,9 @@ contract SystemConfigInterop_Test is CommonTest { /// @dev Tests that removing a dependency as not the dependency manager reverts. function testFuzz_removeDependency_notDependencyManager_reverts(uint256 _chainId) public { + require(alice != _systemConfigInterop().dependencyManager(), "SystemConfigInterop_Test-100"); vm.expectRevert("SystemConfig: caller is not the dependency manager"); + vm.prank(alice); _systemConfigInterop().removeDependency(_chainId); } diff --git a/packages/contracts-bedrock/test/vendor/Initializable.t.sol b/packages/contracts-bedrock/test/vendor/Initializable.t.sol index eaa0c420915a..3d1465704aff 100644 --- a/packages/contracts-bedrock/test/vendor/Initializable.t.sol +++ b/packages/contracts-bedrock/test/vendor/Initializable.t.sol @@ -393,7 +393,7 @@ contract Initializer_Test is Bridge_Initializer { /// 3. The `initialize()` function of each contract cannot be called again. function test_cannotReinitialize_succeeds() public { // Collect exclusions. - string[] memory excludes = new string[](8); + string[] memory excludes = new string[](9); // TODO: Neither of these contracts are labeled properly in the deployment script. Both are // currently being labeled as their non-interop versions. Remove these exclusions once // the deployment script is fixed. @@ -412,6 +412,8 @@ contract Initializer_Test is Bridge_Initializer { // TODO: Eventually remove this exclusion. Same reason as above dispute contracts. excludes[6] = "src/L1/OPContractsManager.sol"; excludes[7] = "src/L1/OPContractsManagerInterop.sol"; + // The L2OutputOracle is not always deployed (and is no longer being modified) + excludes[8] = "src/L1/L2OutputOracle.sol"; // Get all contract names in the src directory, minus the excluded contracts. string[] memory contractNames = ForgeArtifacts.getContractNames("src/*", excludes); From 0f4b1e331431ff052841e97b501573909faec93d Mon Sep 17 00:00:00 2001 From: Delweng Date: Sat, 12 Oct 2024 02:56:26 +0800 Subject: [PATCH 09/31] feat(contracts-bedrock): remove old forge-std code (#12378) * feat(contracts-bedrock): rm assume on precompile Signed-off-by: jsvisa * feat(contracts-bedrock): replace vm.keyExists with vm.keyExistsJson Signed-off-by: jsvisa * feat(contracts-bedrock): replace _readOr implement Signed-off-by: jsvisa --------- Signed-off-by: jsvisa --- .../contracts-bedrock/scripts/deploy/DeployConfig.s.sol | 6 +++--- packages/contracts-bedrock/test/libraries/SafeCall.t.sol | 2 -- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol index 9288111b6e6b..3542f16bc422 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol @@ -268,7 +268,7 @@ contract DeployConfig is Script { } function _readOr(string memory _jsonInp, string memory _key, bool _defaultValue) internal view returns (bool) { - return vm.keyExistsJson(_jsonInp, _key) ? _jsonInp.readBool(_key) : _defaultValue; + return _jsonInp.readBoolOr(_key, _defaultValue); } function _readOr( @@ -292,7 +292,7 @@ contract DeployConfig is Script { view returns (address) { - return vm.keyExistsJson(_jsonInp, _key) ? _jsonInp.readAddress(_key) : _defaultValue; + return _jsonInp.readAddressOr(_key, _defaultValue); } function _isNull(string memory _jsonInp, string memory _key) internal pure returns (bool) { @@ -309,6 +309,6 @@ contract DeployConfig is Script { view returns (string memory) { - return vm.keyExists(_jsonInp, _key) ? _jsonInp.readString(_key) : _defaultValue; + return _jsonInp.readStringOr(_key, _defaultValue); } } diff --git a/packages/contracts-bedrock/test/libraries/SafeCall.t.sol b/packages/contracts-bedrock/test/libraries/SafeCall.t.sol index 5bd3fb3a4ab7..fcb9d3832a12 100644 --- a/packages/contracts-bedrock/test/libraries/SafeCall.t.sol +++ b/packages/contracts-bedrock/test/libraries/SafeCall.t.sol @@ -13,8 +13,6 @@ contract SafeCall_Test is Test { function assumeNot(address _addr) internal { vm.assume(_addr.balance == 0); vm.assume(_addr != address(this)); - vm.assume(uint256(uint160(_addr)) > uint256(256)); // TODO temp fix until new forge-std release with modern - // precompiles: https://github.com/foundry-rs/forge-std/pull/594 assumeAddressIsNot(_addr, StdCheatsSafe.AddressType.ForgeAddress, StdCheatsSafe.AddressType.Precompile); } From f259ee0da71eb9c16bbdab376cee7b1c4f4988e6 Mon Sep 17 00:00:00 2001 From: Matt Solomon Date: Fri, 11 Oct 2024 12:00:19 -0700 Subject: [PATCH 10/31] doc: add more security review info (#12429) --- docs/security-reviews/README.md | 44 ++++++++++++++++----------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/docs/security-reviews/README.md b/docs/security-reviews/README.md index fd41977b9f10..e0cab31d0eb4 100644 --- a/docs/security-reviews/README.md +++ b/docs/security-reviews/README.md @@ -5,27 +5,27 @@ The following is a list of past security reviews. Each review is focused on a different part of the codebase, and at a different point in time. Please see the report for the specific details. -| Date | Reviewer | Focus | Report Link | -| ------- | -------------------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 2020-10 | Trail of Bits | Rollup | [2020_10-TrailOfBits.pdf](./2020_10-Rollup-TrailOfBits.pdf) | -| 2020-11 | Dapphub | ECDSA Wallet | [2020_11-Dapphub-ECDSA_Wallet.pdf](./2020_11-Dapphub-ECDSA_Wallet.pdf) | -| 2021-03 | OpenZeppelin | OVM and Rollup | [2021_03-OVM_and_Rollup-OpenZeppelin.pdf](./2021_03-OVM_and_Rollup-OpenZeppelin.pdf) | -| 2021-03 | ConsenSys Diligence | Safety Checker | [2021_03-SafetyChecker-ConsenSysDiligence.pdf](./2021_03-SafetyChecker-ConsenSysDiligence.pdf) | -| 2022-05 | Zeppelin | Bedrock Contracts | [2022_05-Bedrock_Contracts-Zeppelin.pdf](./2022_05-Bedrock_Contracts-Zeppelin.pdf) | -| 2022-05 | Trail of Bits | OpNode | [2022_05-OpNode-TrailOfBits.pdf](./2022_05-OpNode-TrailOfBits.pdf) | -| 2022-08 | Sigma Prime | Bedrock GoLang | [2022_08-Bedrock_GoLang-SigmaPrime.pdf](./2022_08-Bedrock_GoLang-SigmaPrime.pdf) | -| 2022-09 | Zeppelin | Bedrock and Periphery | [2022_09-Bedrock_and_Periphery-Zeppelin.pdf](./2022_09-Bedrock_and_Periphery-Zeppelin.pdf) | -| 2022-10 | Spearbit | Drippie | [2022_10-Drippie-Spearbit.pdf](./2022_10-Drippie-Spearbit.pdf) | -| 2022-11 | Trail of Bits | Invariant Testing | [2022_11-Invariant_Testing-TrailOfBits.pdf](./2022_11-Invariant_Testing-TrailOfBits.pdf) | -| 2022-12 | Runtime Verification | Deposit Transaction | [2022_12-DepositTransaction-RuntimeVerification.pdf](./2022_12-DepositTransaction-RuntimeVerification.pdf) | -| 2023-01 | Trail of Bits | Bedrock Updates | [2023_01-Bedrock_Updates-TrailOfBits.pdf](./2023_01-Bedrock_Updates-TrailOfBits.pdf) | -| 2023-01 | Sherlock | Bedrock | [Sherlock Bedrock Contest](https://audits.sherlock.xyz/contests/38) | -| 2023-03 | Sherlock | Bedrock Fixes | [Sherlock Bedrock Contest - Fix Review](https://audits.sherlock.xyz/contests/63) | -| 2023-12 | Trust | Superchain Config Upgrade | [2023_12_SuperchainConfigUpgrade_Trust.pdf](./2023_12_SuperchainConfigUpgrade_Trust.pdf) | -| 2024-02 | Runtime Verification | Pausability | [Kontrol Verification][kontrol] | -| 2024-02 | Cantina | MCP L1 | [2024_02-MCP_L1-Cantina.pdf](./2024_02-MCP_L1-Cantina.pdf) | -| 2024-03 | Sherlock | MCP L1 | [Sherlock Optimism Fault Proofs Contest](https://audits.sherlock.xyz/contests/205) | -| 2024-08 | Cantina | Fault proof MIPS | [Base Fault Proof MIPS](./2024_08_report-cantinacode-coinbase-fault-proofs-mips.pdf) -| 2024-08 | Spearbit | Fault proof no-MIPS | [Base Fault Proof No MIPS](./2024_08_report-cb-fault-proofs-non-mips.pdf) +| Date | Reviewer | Focus and Scope | Report Link | Commit | Subsequent Release | +| ------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | -------------------------------------------- | ------------------- | +| 2020-10 | Trail of Bits | Rollup | [2020_10-TrailOfBits.pdf](./2020_10-Rollup-TrailOfBits.pdf) | | | +| 2020-11 | Dapphub | ECDSA Wallet | [2020_11-Dapphub-ECDSA_Wallet.pdf](./2020_11-Dapphub-ECDSA_Wallet.pdf) | | | +| 2021-03 | OpenZeppelin | OVM and Rollup | [2021_03-OVM_and_Rollup-OpenZeppelin.pdf](./2021_03-OVM_and_Rollup-OpenZeppelin.pdf) | | | +| 2021-03 | ConsenSys Diligence | Safety Checker | [2021_03-SafetyChecker-ConsenSysDiligence.pdf](./2021_03-SafetyChecker-ConsenSysDiligence.pdf) | | | +| 2022-05 | Zeppelin | Bedrock Contracts | [2022_05-Bedrock_Contracts-Zeppelin.pdf](./2022_05-Bedrock_Contracts-Zeppelin.pdf) | | | +| 2022-05 | Trail of Bits | OpNode | [2022_05-OpNode-TrailOfBits.pdf](./2022_05-OpNode-TrailOfBits.pdf) | | | +| 2022-08 | Sigma Prime | Bedrock GoLang | [2022_08-Bedrock_GoLang-SigmaPrime.pdf](./2022_08-Bedrock_GoLang-SigmaPrime.pdf) | | | +| 2022-09 | Zeppelin | Bedrock and Periphery: All contracts in `packages/contracts-bedrock/contracts` | [2022_09-Bedrock_and_Periphery-Zeppelin.pdf](./2022_09-Bedrock_and_Periphery-Zeppelin.pdf) | 93d3bd411a8ae75702539ac9c5fe00bad21d4104 | op-contracts/v1.0.0 | +| 2022-10 | Spearbit | Drippie: `Drippie.sol` | [2022_10-Drippie-Spearbit.pdf](./2022_10-Drippie-Spearbit.pdf) | 2a7be367634f147736f960eb2f38a77291cdfcad | op-contracts/v1.0.0 | +| 2022-11 | Trail of Bits | Invariant Testing: `OptimismPortal.sol` | [2022_11-Invariant_Testing-TrailOfBits.pdf](./2022_11-Invariant_Testing-TrailOfBits.pdf) | b31d35b67755479645dd150e7cc8c6710f0b4a56 | op-contracts/v1.0.0 | +| 2022-12 | Runtime Verification | Deposit Transaction: `OptimismPortal.sol` | [2022_12-DepositTransaction-RuntimeVerification.pdf](./2022_12-DepositTransaction-RuntimeVerification.pdf) | | op-contracts/v1.0.0 | +| 2023-01 | Trail of Bits | Bedrock Updates: `SystemConfig.sol` | [2023_01-Bedrock_Updates-TrailOfBits.pdf](./2023_01-Bedrock_Updates-TrailOfBits.pdf) | ee96ff8585699b054c95c6ff4a2411ee9fedcc87 | op-contracts/v1.0.0 | +| 2023-01 | Sherlock | Bedrock: All contracts in `packages/contracts-bedrock/src` | [Sherlock Bedrock Contest](https://audits.sherlock.xyz/contests/38) | 3f4b3c328153a8aa03611158b6984d624b17c1d9 | op-contracts/v1.0.0 | +| 2023-03 | Sherlock | Bedrock Fixes: All contracts in `packages/contracts-bedrock/src` | [Sherlock Bedrock Contest: Fix Review](https://audits.sherlock.xyz/contests/63) | 20229b9f78c6613c6ee53b93ca43c71bb74479f4b975 | op-contracts/v1.0.0 | +| 2023-12 | Trust | Superchain Config Upgrade: `SuperchainConfig.sol`, `L1CrossDomainMessenger.sol`, `L1ERC721Bridge.sol`, `L1StandardBridge.sol`, `OptimismPortal.sol`, `CrossDomainMessenger.sol`, `ERC721Bridge.sol`, `StandardBridge.sol` | [2023_12_SuperchainConfigUpgrade_Trust.pdf](./2023_12_SuperchainConfigUpgrade_Trust.pdf) | d1651bb22645ebd41ac4bb2ab4786f9a56fc1003 | op-contracts/v1.2.0 | +| 2024-02 | Runtime Verification | Pausability | [Kontrol Verification][kontrol] | | | +| 2024-02 | Cantina | MCP L1: `OptimismPortal.sol`, `L1CrossDomainMessenger.sol`, `L1StandardBridge.sol`, `L1ERC721Bridge.sol`, `OptimismMintableERC20Factory.sol`, `L2OutputOracle.sol`, `SystemConfig.sol` | [2024_02-MCP_L1-Cantina.pdf](./2024_02-MCP_L1-Cantina.pdf) | e6ef3a900c42c8722e72c2e2314027f85d12ced5 | op-contracts/v1.3.0 | +| 2024-03 | Sherlock | MCP L1 | [Sherlock Optimism Fault Proofs Contest](https://audits.sherlock.xyz/contests/205) | | | +| 2024-08 | Spearbit | Fault proof no-MIPS: All contracts in the `packages/contracts-bedrock/src/dispute` directory | [Base Fault Proof No MIPS](./2024_08_report-cb-fault-proofs-non-mips.pdf) | 1f7081798ce2d49b8643514663d10681cb853a3d | op-contracts/v1.4.0 | +| 2024-08 | Cantina | Fault proof MIPS: `MIPS.sol` | [Base Fault Proof MIPS](./2024_08_report-cantinacode-coinbase-fault-proofs-mips.pdf) | 71b93116738ee98c9f8713b1a5dfe626ce06c1b2 | op-contracts/v1.6.0 | [kontrol]: https://github.com/ethereum-optimism/optimism/blob/876e16ad04968f0bb641eb76f98eb77e7e1a3e16/packages/contracts-bedrock/test/kontrol/README.md From 39f5caf8ef3649692a69d0e080c25868f4dba37a Mon Sep 17 00:00:00 2001 From: 0xDiscotech <131301107+0xDiscotech@users.noreply.github.com> Date: Fri, 11 Oct 2024 16:14:30 -0300 Subject: [PATCH 11/31] chore: run pre-pr --- packages/contracts-bedrock/semver-lock.json | 8 ++++---- .../snapshots/abi/OptimismSuperchainERC20.json | 7 ++++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/contracts-bedrock/semver-lock.json b/packages/contracts-bedrock/semver-lock.json index a53310f87bd2..926bbcf50689 100644 --- a/packages/contracts-bedrock/semver-lock.json +++ b/packages/contracts-bedrock/semver-lock.json @@ -108,8 +108,8 @@ "sourceCodeHash": "0xfea53344596d735eff3be945ed1300dc75a6f8b7b2c02c0043af5b0036f5f239" }, "src/L2/OptimismSuperchainERC20.sol": { - "initCodeHash": "0xd5c84e45746fd741d541a917ddc1cc0c7043c6b21d5c18040d4bc999d6a7b2db", - "sourceCodeHash": "0xf32130f0b46333daba062c50ff6dcfadce1f177ff753bed2374d499ea9c2d98a" + "initCodeHash": "0xa1d74ae6bc6a0fcc851d18e4147adc4ee151efc0bacfea54aeaae22c72ef3e26", + "sourceCodeHash": "0xcb705d26e63e733051c8bd442ea69ce637a00c16d646ccc37b687b20941366fe" }, "src/L2/OptimismSuperchainERC20Beacon.sol": { "initCodeHash": "0x99ce8095b23c124850d866cbc144fee6cee05dbc6bb5d83acadfe00b90cf42c7", @@ -125,7 +125,7 @@ }, "src/L2/SuperchainERC20.sol": { "initCodeHash": "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", - "sourceCodeHash": "0x75d061633a141af11a19b86e599a1725dfae8d245dcddfb6bb244a50d5e53f96" + "sourceCodeHash": "0x6a384ccfb6f2f7316c1b33873a1630b5179e52475951d31771656e06d2b11519" }, "src/L2/SuperchainTokenBridge.sol": { "initCodeHash": "0x07fc1d495928d9c13bd945a049d17e1d105d01c2082a7719e5d18cbc0e1c7d9e", @@ -235,4 +235,4 @@ "initCodeHash": "0x06ae2c0b39c215b7fa450d382916ce6f5c6f9f2d630e572db6b72d688255b3fd", "sourceCodeHash": "0xa014d9c992f439dee8221e065828c3326ca2c4f5db0e83431c64c20f7e51ec14" } -} +} \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json b/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json index 2885fc120873..d6ad63fad9c3 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json @@ -570,6 +570,11 @@ "name": "NotInitializing", "type": "error" }, + { + "inputs": [], + "name": "Permit2AllowanceIsFixedAtInfinity", + "type": "error" + }, { "inputs": [], "name": "PermitExpired", @@ -590,4 +595,4 @@ "name": "ZeroAddress", "type": "error" } -] +] \ No newline at end of file From 86c37b94c3cd12bedaebace07f8a2172cd768c60 Mon Sep 17 00:00:00 2001 From: Blaine Malone Date: Fri, 11 Oct 2024 15:26:28 -0400 Subject: [PATCH 12/31] op-deployer: Fee Recipients and Gas Params added to intent (#12404) * op-deployer: Fee Recipients and Gas Params added to intent * fix: retrieved fee recipients from docs. * fix: added a loose test for checking fee vault recipients and gas params. * fix: programmatically retrieving the proxy impl address to check the immutable. * fix: fee recipient per chain --- op-chain-ops/deployer/init.go | 7 ++- .../deployer/integration_test/apply_test.go | 54 ++++++++++++++++--- op-chain-ops/deployer/state/deploy_config.go | 12 +++-- op-chain-ops/deployer/state/intent.go | 10 ++++ 4 files changed, 70 insertions(+), 13 deletions(-) diff --git a/op-chain-ops/deployer/init.go b/op-chain-ops/deployer/init.go index 239e1b2da415..fa61f32f0025 100644 --- a/op-chain-ops/deployer/init.go +++ b/op-chain-ops/deployer/init.go @@ -105,7 +105,12 @@ func Init(cfg InitConfig) error { for _, l2ChainID := range cfg.L2ChainIDs { l2ChainIDBig := l2ChainID.Big() intent.Chains = append(intent.Chains, &state.ChainIntent{ - ID: l2ChainID, + ID: l2ChainID, + BaseFeeVaultRecipient: common.Address{}, + L1FeeVaultRecipient: common.Address{}, + SequencerFeeVaultRecipient: common.Address{}, + Eip1559Denominator: 50, + Eip1559Elasticity: 6, Roles: state.ChainRoles{ ProxyAdminOwner: addrFor(devkeys.L2ProxyAdminOwnerRole.Key(l2ChainIDBig)), SystemConfigOwner: addrFor(devkeys.SystemConfigOwner.Key(l2ChainIDBig)), diff --git a/op-chain-ops/deployer/integration_test/apply_test.go b/op-chain-ops/deployer/integration_test/apply_test.go index bddefe53d411..784637fe5b98 100644 --- a/op-chain-ops/deployer/integration_test/apply_test.go +++ b/op-chain-ops/deployer/integration_test/apply_test.go @@ -9,22 +9,25 @@ import ( "os" "path" "runtime" + "strings" "testing" "time" - "github.com/ethereum-optimism/optimism/op-service/testutils/anvil" - crypto "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum-optimism/optimism/op-chain-ops/deployer" "github.com/holiman/uint256" "github.com/ethereum-optimism/optimism/op-chain-ops/deployer/pipeline" "github.com/ethereum-optimism/optimism/op-chain-ops/deployer/state" "github.com/ethereum-optimism/optimism/op-chain-ops/devkeys" + "github.com/ethereum-optimism/optimism/op-chain-ops/genesis" opcrypto "github.com/ethereum-optimism/optimism/op-service/crypto" + "github.com/ethereum-optimism/optimism/op-service/predeploys" "github.com/ethereum-optimism/optimism/op-service/testlog" + "github.com/ethereum-optimism/optimism/op-service/testutils/anvil" "github.com/ethereum-optimism/optimism/op-service/testutils/kurtosisutil" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient" "github.com/stretchr/testify/require" ) @@ -141,7 +144,7 @@ func TestEndToEndApply(t *testing.T) { }) } - validateOPChainDeployment(t, ctx, l1Client, st) + validateOPChainDeployment(t, ctx, l1Client, st, intent) }) t.Run("subsequent chain", func(t *testing.T) { @@ -172,7 +175,7 @@ func TestEndToEndApply(t *testing.T) { }) } - validateOPChainDeployment(t, ctx, l1Client, st) + validateOPChainDeployment(t, ctx, l1Client, st, intent) }) } @@ -207,7 +210,12 @@ func makeIntent( ContractsRelease: "dev", Chains: []*state.ChainIntent{ { - ID: l2ChainID.Bytes32(), + ID: l2ChainID.Bytes32(), + BaseFeeVaultRecipient: addrFor(devkeys.BaseFeeVaultRecipientRole.Key(l1ChainID)), + L1FeeVaultRecipient: addrFor(devkeys.L1FeeVaultRecipientRole.Key(l1ChainID)), + SequencerFeeVaultRecipient: addrFor(devkeys.SequencerFeeVaultRecipientRole.Key(l1ChainID)), + Eip1559Denominator: 50, + Eip1559Elasticity: 6, Roles: state.ChainRoles{ ProxyAdminOwner: addrFor(devkeys.L2ProxyAdminOwnerRole.Key(l1ChainID)), SystemConfigOwner: addrFor(devkeys.SystemConfigOwner.Key(l1ChainID)), @@ -226,7 +234,7 @@ func makeIntent( return intent, st } -func validateOPChainDeployment(t *testing.T, ctx context.Context, l1Client *ethclient.Client, st *state.State) { +func validateOPChainDeployment(t *testing.T, ctx context.Context, l1Client *ethclient.Client, st *state.State, intent *state.Intent) { for _, chainState := range st.Chains { chainAddrs := []struct { name string @@ -261,10 +269,40 @@ func validateOPChainDeployment(t *testing.T, ctx context.Context, l1Client *ethc t.Run("l2 genesis", func(t *testing.T) { require.Greater(t, len(chainState.Allocs), 0) + l2Allocs, _ := chainState.UnmarshalAllocs() + alloc := l2Allocs.Copy().Accounts + + firstChainIntent := intent.Chains[0] + checkImmutable(t, alloc, predeploys.BaseFeeVaultAddr, firstChainIntent.BaseFeeVaultRecipient) + checkImmutable(t, alloc, predeploys.L1FeeVaultAddr, firstChainIntent.L1FeeVaultRecipient) + checkImmutable(t, alloc, predeploys.SequencerFeeVaultAddr, firstChainIntent.SequencerFeeVaultRecipient) + + require.Equal(t, int(firstChainIntent.Eip1559Denominator), 50, "EIP1559Denominator should be set") + require.Equal(t, int(firstChainIntent.Eip1559Elasticity), 6, "EIP1559Elasticity should be set") }) } } +func getEIP1967ImplementationAddress(t *testing.T, allocations types.GenesisAlloc, proxyAddress common.Address) common.Address { + storage := allocations[proxyAddress].Storage + storageValue := storage[genesis.ImplementationSlot] + require.NotEmpty(t, storageValue, "Implementation address for %s should be set", proxyAddress) + return common.HexToAddress(storageValue.Hex()) +} + +func checkImmutable(t *testing.T, allocations types.GenesisAlloc, proxyContract common.Address, feeRecipient common.Address) { + implementationAddress := getEIP1967ImplementationAddress(t, allocations, proxyContract) + account, ok := allocations[implementationAddress] + require.True(t, ok, "%s not found in allocations", implementationAddress.Hex()) + require.NotEmpty(t, account.Code, "%s should have code", implementationAddress.Hex()) + require.Contains( + t, + strings.ToLower(common.Bytes2Hex(account.Code)), + strings.ToLower(strings.TrimPrefix(feeRecipient.Hex(), "0x")), + "%s code should contain %s immutable", implementationAddress.Hex(), feeRecipient.Hex(), + ) +} + func TestApplyExistingOPCM(t *testing.T) { anvil.Test(t) @@ -321,7 +359,7 @@ func TestApplyExistingOPCM(t *testing.T) { st, )) - validateOPChainDeployment(t, ctx, l1Client, st) + validateOPChainDeployment(t, ctx, l1Client, st, intent) } func TestL2BlockTimeOverride(t *testing.T) { diff --git a/op-chain-ops/deployer/state/deploy_config.go b/op-chain-ops/deployer/state/deploy_config.go index 5ea8590f537f..4e6be6251b94 100644 --- a/op-chain-ops/deployer/state/deploy_config.go +++ b/op-chain-ops/deployer/state/deploy_config.go @@ -18,7 +18,7 @@ var ( vaultMinWithdrawalAmount = mustHexBigFromHex("0x8ac7230489e80000") ) -func DefaultDeployConfig() genesis.DeployConfig { +func DefaultDeployConfig(chainIntent *ChainIntent) genesis.DeployConfig { return genesis.DeployConfig{ L2InitializationConfig: genesis.L2InitializationConfig{ L2GenesisBlockDeployConfig: genesis.L2GenesisBlockDeployConfig{ @@ -32,6 +32,9 @@ func DefaultDeployConfig() genesis.DeployConfig { SequencerFeeVaultMinimumWithdrawalAmount: vaultMinWithdrawalAmount, BaseFeeVaultMinimumWithdrawalAmount: vaultMinWithdrawalAmount, L1FeeVaultMinimumWithdrawalAmount: vaultMinWithdrawalAmount, + BaseFeeVaultRecipient: chainIntent.BaseFeeVaultRecipient, + L1FeeVaultRecipient: chainIntent.L1FeeVaultRecipient, + SequencerFeeVaultRecipient: chainIntent.SequencerFeeVaultRecipient, }, GovernanceDeployConfig: genesis.GovernanceDeployConfig{ EnableGovernance: true, @@ -43,9 +46,9 @@ func DefaultDeployConfig() genesis.DeployConfig { GasPriceOracleBlobBaseFeeScalar: 810949, }, EIP1559DeployConfig: genesis.EIP1559DeployConfig{ - EIP1559Denominator: 50, + EIP1559Denominator: chainIntent.Eip1559Denominator, EIP1559DenominatorCanyon: 250, - EIP1559Elasticity: 6, + EIP1559Elasticity: chainIntent.Eip1559Elasticity, }, UpgradeScheduleDeployConfig: genesis.UpgradeScheduleDeployConfig{ L2GenesisRegolithTimeOffset: u64UtilPtr(0), @@ -76,7 +79,8 @@ func DefaultDeployConfig() genesis.DeployConfig { } func CombineDeployConfig(intent *Intent, chainIntent *ChainIntent, state *State, chainState *ChainState) (genesis.DeployConfig, error) { - cfg := DefaultDeployConfig() + firstChainIntent := intent.Chains[0] + cfg := DefaultDeployConfig(firstChainIntent) var err error if len(intent.GlobalDeployOverrides) > 0 { diff --git a/op-chain-ops/deployer/state/intent.go b/op-chain-ops/deployer/state/intent.go index b365711b0deb..bdb7cb847105 100644 --- a/op-chain-ops/deployer/state/intent.go +++ b/op-chain-ops/deployer/state/intent.go @@ -97,6 +97,16 @@ type SuperchainRoles struct { type ChainIntent struct { ID common.Hash `json:"id" toml:"id"` + BaseFeeVaultRecipient common.Address `json:"baseFeeVaultRecipient" toml:"baseFeeVaultRecipient"` + + L1FeeVaultRecipient common.Address `json:"l1FeeVaultRecipient" toml:"l1FeeVaultRecipient"` + + SequencerFeeVaultRecipient common.Address `json:"sequencerFeeVaultRecipient" toml:"sequencerFeeVaultRecipient"` + + Eip1559Denominator uint64 `json:"eip1559Denominator" toml:"eip1559Denominator"` + + Eip1559Elasticity uint64 `json:"eip1559Elasticity" toml:"eip1559Elasticity"` + Roles ChainRoles `json:"roles" toml:"roles"` DeployOverrides map[string]any `json:"deployOverrides" toml:"deployOverrides"` From 1495f6d0f93d4190bdef4b3006358cdd20b35e85 Mon Sep 17 00:00:00 2001 From: smartcontracts Date: Fri, 11 Oct 2024 15:38:58 -0400 Subject: [PATCH 13/31] feat(ci): add semgrep to contracts checks (#12395) Adds the semgrep step to contracts-bedrock checks now that semgrep is added to the latest version of ci-builder. --- .circleci/config.yml | 4 +++- packages/contracts-bedrock/justfile | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 08faefd24f71..b15c6bec8452 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,7 +3,7 @@ version: 2.1 parameters: ci_builder_image: type: string - default: us-docker.pkg.dev/oplabs-tools-artifacts/images/ci-builder:v0.53.0 + default: us-docker.pkg.dev/oplabs-tools-artifacts/images/ci-builder:v0.54.0 ci_builder_rust_image: type: string default: us-docker.pkg.dev/oplabs-tools-artifacts/images/ci-builder-rust:latest @@ -701,6 +701,8 @@ jobs: - run: name: print forge version command: forge --version + - run-contracts-check: + command: semgrep - run-contracts-check: command: semver-lock - run-contracts-check: diff --git a/packages/contracts-bedrock/justfile b/packages/contracts-bedrock/justfile index 06228b3630d8..c80f02886353 100644 --- a/packages/contracts-bedrock/justfile +++ b/packages/contracts-bedrock/justfile @@ -193,6 +193,10 @@ validate-spacers-no-build: # Checks that spacer variables are correctly inserted. validate-spacers: build validate-spacers-no-build +# Runs semgrep on the contracts. +semgrep: + cd ../../ && semgrep scan --config=.semgrep ./packages/contracts-bedrock + # TODO: Also run lint-forge-tests-check but we need to fix the test names first. # Runs all checks. check: From 5c1e1983d1a0af9e57878e49d8d19d6c1f46786a Mon Sep 17 00:00:00 2001 From: Michael de Hoog Date: Fri, 11 Oct 2024 10:55:07 -1000 Subject: [PATCH 14/31] [batcher] derive.ChannelOut factory (#12344) * Add support for a derive.ChannelOut factory * Add DriverSetupOption for injecting custom options into the DriverSetup * Remove factory from NewChannelManager and NewChannelBuilder * Add ChannelOut factory test * Add comment about why we use a wrapper --- op-batcher/batcher/channel.go | 11 +++------- op-batcher/batcher/channel_builder.go | 14 +++++++----- op-batcher/batcher/channel_manager.go | 16 ++++++++++++-- op-batcher/batcher/channel_manager_test.go | 22 +++++++++++++++++++ op-batcher/batcher/channel_test.go | 13 +++++++++-- op-batcher/batcher/driver.go | 25 +++++++++++++--------- op-batcher/batcher/service.go | 20 +++++++++++------ 7 files changed, 87 insertions(+), 34 deletions(-) diff --git a/op-batcher/batcher/channel.go b/op-batcher/batcher/channel.go index de68fa588a0a..399122ecaf0d 100644 --- a/op-batcher/batcher/channel.go +++ b/op-batcher/batcher/channel.go @@ -1,7 +1,6 @@ package batcher import ( - "fmt" "math" "github.com/ethereum-optimism/optimism/op-batcher/metrics" @@ -34,12 +33,8 @@ type channel struct { maxInclusionBlock uint64 } -func newChannel(log log.Logger, metr metrics.Metricer, cfg ChannelConfig, rollupCfg *rollup.Config, latestL1OriginBlockNum uint64) (*channel, error) { - cb, err := NewChannelBuilder(cfg, rollupCfg, latestL1OriginBlockNum) - if err != nil { - return nil, fmt.Errorf("creating new channel: %w", err) - } - +func newChannel(log log.Logger, metr metrics.Metricer, cfg ChannelConfig, rollupCfg *rollup.Config, latestL1OriginBlockNum uint64, channelOut derive.ChannelOut) *channel { + cb := NewChannelBuilderWithChannelOut(cfg, rollupCfg, latestL1OriginBlockNum, channelOut) return &channel{ log: log, metr: metr, @@ -47,7 +42,7 @@ func newChannel(log log.Logger, metr metrics.Metricer, cfg ChannelConfig, rollup channelBuilder: cb, pendingTransactions: make(map[string]txData), confirmedTransactions: make(map[string]eth.BlockID), - }, nil + } } // TxFailed records a transaction as failed. It will attempt to resubmit the data diff --git a/op-batcher/batcher/channel_builder.go b/op-batcher/batcher/channel_builder.go index 35afb2cbd267..ae1fb03d2841 100644 --- a/op-batcher/batcher/channel_builder.go +++ b/op-batcher/batcher/channel_builder.go @@ -86,24 +86,28 @@ type ChannelBuilder struct { // channel out could not be created. // it acts as a factory for either a span or singular channel out func NewChannelBuilder(cfg ChannelConfig, rollupCfg *rollup.Config, latestL1OriginBlockNum uint64) (*ChannelBuilder, error) { - co, err := newChannelOut(cfg, rollupCfg) + co, err := NewChannelOut(cfg, rollupCfg) if err != nil { return nil, fmt.Errorf("creating channel out: %w", err) } + return NewChannelBuilderWithChannelOut(cfg, rollupCfg, latestL1OriginBlockNum, co), nil +} + +func NewChannelBuilderWithChannelOut(cfg ChannelConfig, rollupCfg *rollup.Config, latestL1OriginBlockNum uint64, channelOut derive.ChannelOut) *ChannelBuilder { cb := &ChannelBuilder{ cfg: cfg, rollupCfg: rollupCfg, - co: co, + co: channelOut, } cb.updateDurationTimeout(latestL1OriginBlockNum) - return cb, nil + return cb } -// newChannelOut creates a new channel out based on the given configuration. -func newChannelOut(cfg ChannelConfig, rollupCfg *rollup.Config) (derive.ChannelOut, error) { +// NewChannelOut creates a new channel out based on the given configuration. +func NewChannelOut(cfg ChannelConfig, rollupCfg *rollup.Config) (derive.ChannelOut, error) { spec := rollup.NewChainSpec(rollupCfg) if cfg.BatchType == derive.SpanBatchType { return derive.NewSpanChannelOut( diff --git a/op-batcher/batcher/channel_manager.go b/op-batcher/batcher/channel_manager.go index a275fdc1a0a1..887c51f4ebf2 100644 --- a/op-batcher/batcher/channel_manager.go +++ b/op-batcher/batcher/channel_manager.go @@ -18,6 +18,8 @@ import ( var ErrReorg = errors.New("block does not extend existing chain") +type ChannelOutFactory func(cfg ChannelConfig, rollupCfg *rollup.Config) (derive.ChannelOut, error) + // channelManager stores a contiguous set of blocks & turns them into channels. // Upon receiving tx confirmation (or a tx failure), it does channel error handling. // @@ -32,6 +34,8 @@ type channelManager struct { cfgProvider ChannelConfigProvider rollupCfg *rollup.Config + outFactory ChannelOutFactory + // All blocks since the last request for new tx data. blocks queue.Queue[*types.Block] // The latest L1 block from all the L2 blocks in the most recently closed channel @@ -59,10 +63,15 @@ func NewChannelManager(log log.Logger, metr metrics.Metricer, cfgProvider Channe cfgProvider: cfgProvider, defaultCfg: cfgProvider.ChannelConfig(), rollupCfg: rollupCfg, + outFactory: NewChannelOut, txChannels: make(map[string]*channel), } } +func (s *channelManager) SetChannelOutFactory(outFactory ChannelOutFactory) { + s.outFactory = outFactory +} + // Clear clears the entire state of the channel manager. // It is intended to be used before launching op-batcher and after an L2 reorg. func (s *channelManager) Clear(l1OriginLastClosedChannel eth.BlockID) { @@ -265,11 +274,14 @@ func (s *channelManager) ensureChannelWithSpace(l1Head eth.BlockID) error { // This will be reassessed at channel submission-time, // but this is our best guess at the appropriate values for now. cfg := s.defaultCfg - pc, err := newChannel(s.log, s.metr, cfg, s.rollupCfg, s.l1OriginLastClosedChannel.Number) + + channelOut, err := s.outFactory(cfg, s.rollupCfg) if err != nil { - return fmt.Errorf("creating new channel: %w", err) + return fmt.Errorf("creating channel out: %w", err) } + pc := newChannel(s.log, s.metr, cfg, s.rollupCfg, s.l1OriginLastClosedChannel.Number, channelOut) + s.currentChannel = pc s.channelQueue = append(s.channelQueue, pc) diff --git a/op-batcher/batcher/channel_manager_test.go b/op-batcher/batcher/channel_manager_test.go index fac34f8c931e..8dcb0745c164 100644 --- a/op-batcher/batcher/channel_manager_test.go +++ b/op-batcher/batcher/channel_manager_test.go @@ -668,3 +668,25 @@ func TestChannelManager_Requeue(t *testing.T) { require.NotContains(t, m.blocks, blockA) } +func TestChannelManager_ChannelOutFactory(t *testing.T) { + type ChannelOutWrapper struct { + derive.ChannelOut + } + + l := testlog.Logger(t, log.LevelCrit) + cfg := channelManagerTestConfig(100, derive.SingularBatchType) + m := NewChannelManager(l, metrics.NoopMetrics, cfg, defaultTestRollupConfig) + m.SetChannelOutFactory(func(cfg ChannelConfig, rollupCfg *rollup.Config) (derive.ChannelOut, error) { + co, err := NewChannelOut(cfg, rollupCfg) + if err != nil { + return nil, err + } + // return a wrapper type, to validate that the factory was correctly used by checking the type below + return &ChannelOutWrapper{ + ChannelOut: co, + }, nil + }) + require.NoError(t, m.ensureChannelWithSpace(eth.BlockID{})) + + require.IsType(t, &ChannelOutWrapper{}, m.currentChannel.channelBuilder.co) +} diff --git a/op-batcher/batcher/channel_test.go b/op-batcher/batcher/channel_test.go index 3585ea8b99f6..2bd2cfa73990 100644 --- a/op-batcher/batcher/channel_test.go +++ b/op-batcher/batcher/channel_test.go @@ -1,6 +1,7 @@ package batcher import ( + "fmt" "io" "testing" @@ -23,6 +24,14 @@ func zeroFrameTxID(fn uint16) txID { return txID{frameID{frameNumber: fn}} } +func newChannelWithChannelOut(log log.Logger, metr metrics.Metricer, cfg ChannelConfig, rollupCfg *rollup.Config, latestL1OriginBlockNum uint64) (*channel, error) { + channelOut, err := NewChannelOut(cfg, rollupCfg) + if err != nil { + return nil, fmt.Errorf("creating channel out: %w", err) + } + return newChannel(log, metr, cfg, rollupCfg, latestL1OriginBlockNum, channelOut), nil +} + // TestChannelTimeout tests that the channel manager // correctly identifies when a pending channel is timed out. func TestChannelTimeout(t *testing.T) { @@ -121,7 +130,7 @@ func TestChannel_NextTxData_singleFrameTx(t *testing.T) { require := require.New(t) const n = 6 lgr := testlog.Logger(t, log.LevelWarn) - ch, err := newChannel(lgr, metrics.NoopMetrics, ChannelConfig{ + ch, err := newChannelWithChannelOut(lgr, metrics.NoopMetrics, ChannelConfig{ UseBlobs: false, TargetNumFrames: n, CompressorConfig: compressor.Config{ @@ -162,7 +171,7 @@ func TestChannel_NextTxData_multiFrameTx(t *testing.T) { require := require.New(t) const n = eth.MaxBlobsPerBlobTx lgr := testlog.Logger(t, log.LevelWarn) - ch, err := newChannel(lgr, metrics.NoopMetrics, ChannelConfig{ + ch, err := newChannelWithChannelOut(lgr, metrics.NoopMetrics, ChannelConfig{ UseBlobs: true, TargetNumFrames: n, CompressorConfig: compressor.Config{ diff --git a/op-batcher/batcher/driver.go b/op-batcher/batcher/driver.go index 968e6de3e71a..3eb687287e60 100644 --- a/op-batcher/batcher/driver.go +++ b/op-batcher/batcher/driver.go @@ -76,15 +76,16 @@ type RollupClient interface { // DriverSetup is the collection of input/output interfaces and configuration that the driver operates on. type DriverSetup struct { - Log log.Logger - Metr metrics.Metricer - RollupConfig *rollup.Config - Config BatcherConfig - Txmgr txmgr.TxManager - L1Client L1Client - EndpointProvider dial.L2EndpointProvider - ChannelConfig ChannelConfigProvider - AltDA *altda.DAClient + Log log.Logger + Metr metrics.Metricer + RollupConfig *rollup.Config + Config BatcherConfig + Txmgr txmgr.TxManager + L1Client L1Client + EndpointProvider dial.L2EndpointProvider + ChannelConfig ChannelConfigProvider + AltDA *altda.DAClient + ChannelOutFactory ChannelOutFactory } // BatchSubmitter encapsulates a service responsible for submitting L2 tx @@ -115,9 +116,13 @@ type BatchSubmitter struct { // NewBatchSubmitter initializes the BatchSubmitter driver from a preconfigured DriverSetup func NewBatchSubmitter(setup DriverSetup) *BatchSubmitter { + state := NewChannelManager(setup.Log, setup.Metr, setup.ChannelConfig, setup.RollupConfig) + if setup.ChannelOutFactory != nil { + state.SetChannelOutFactory(setup.ChannelOutFactory) + } return &BatchSubmitter{ DriverSetup: setup, - state: NewChannelManager(setup.Log, setup.Metr, setup.ChannelConfig, setup.RollupConfig), + state: state, } } diff --git a/op-batcher/batcher/service.go b/op-batcher/batcher/service.go index 6ed906af15aa..90a85cc4ee48 100644 --- a/op-batcher/batcher/service.go +++ b/op-batcher/batcher/service.go @@ -75,18 +75,20 @@ type BatcherService struct { NotSubmittingOnStart bool } +type DriverSetupOption func(setup *DriverSetup) + // BatcherServiceFromCLIConfig creates a new BatcherService from a CLIConfig. // The service components are fully started, except for the driver, // which will not be submitting batches (if it was configured to) until the Start part of the lifecycle. -func BatcherServiceFromCLIConfig(ctx context.Context, version string, cfg *CLIConfig, log log.Logger) (*BatcherService, error) { +func BatcherServiceFromCLIConfig(ctx context.Context, version string, cfg *CLIConfig, log log.Logger, opts ...DriverSetupOption) (*BatcherService, error) { var bs BatcherService - if err := bs.initFromCLIConfig(ctx, version, cfg, log); err != nil { + if err := bs.initFromCLIConfig(ctx, version, cfg, log, opts...); err != nil { return nil, errors.Join(err, bs.Stop(ctx)) // try to clean up our failed initialization attempt } return &bs, nil } -func (bs *BatcherService) initFromCLIConfig(ctx context.Context, version string, cfg *CLIConfig, log log.Logger) error { +func (bs *BatcherService) initFromCLIConfig(ctx context.Context, version string, cfg *CLIConfig, log log.Logger, opts ...DriverSetupOption) error { bs.Version = version bs.Log = log bs.NotSubmittingOnStart = cfg.Stopped @@ -122,7 +124,7 @@ func (bs *BatcherService) initFromCLIConfig(ctx context.Context, version string, if err := bs.initPProf(cfg); err != nil { return fmt.Errorf("failed to init profiling: %w", err) } - bs.initDriver() + bs.initDriver(opts...) if err := bs.initRPCServer(cfg); err != nil { return fmt.Errorf("failed to start RPC server: %w", err) } @@ -315,8 +317,8 @@ func (bs *BatcherService) initMetricsServer(cfg *CLIConfig) error { return nil } -func (bs *BatcherService) initDriver() { - bs.driver = NewBatchSubmitter(DriverSetup{ +func (bs *BatcherService) initDriver(opts ...DriverSetupOption) { + ds := DriverSetup{ Log: bs.Log, Metr: bs.Metrics, RollupConfig: bs.RollupConfig, @@ -326,7 +328,11 @@ func (bs *BatcherService) initDriver() { EndpointProvider: bs.EndpointProvider, ChannelConfig: bs.ChannelConfig, AltDA: bs.AltDA, - }) + } + for _, opt := range opts { + opt(&ds) + } + bs.driver = NewBatchSubmitter(ds) } func (bs *BatcherService) initRPCServer(cfg *CLIConfig) error { From bb2c99c8a931e53195ea2588d3f9258156968306 Mon Sep 17 00:00:00 2001 From: John Chase <68833933+joohhnnn@users.noreply.github.com> Date: Sat, 12 Oct 2024 05:23:08 +0800 Subject: [PATCH 15/31] MTCannon: improve consistency & add EmptyThreadStack test (#12389) * Add EmptyThreadStacks test * add go evm check * improve consistency and update test * delete emptyThreadedProofGenerator * forge lint update * fix solidity versioning and hash & add proof variations in test --- .../mipsevm/tests/evm_multithreaded_test.go | 33 +++++++++++++++++++ cannon/mipsevm/tests/helpers.go | 17 ++++++++++ packages/contracts-bedrock/semver-lock.json | 4 +-- .../contracts-bedrock/src/cannon/MIPS2.sol | 11 ++++--- 4 files changed, 59 insertions(+), 6 deletions(-) diff --git a/cannon/mipsevm/tests/evm_multithreaded_test.go b/cannon/mipsevm/tests/evm_multithreaded_test.go index 21807c59f3f1..3da9e6593747 100644 --- a/cannon/mipsevm/tests/evm_multithreaded_test.go +++ b/cannon/mipsevm/tests/evm_multithreaded_test.go @@ -1193,6 +1193,39 @@ func TestEVM_UnsupportedSyscall(t *testing.T) { } } +func TestEVM_EmptyThreadStacks(t *testing.T) { + t.Parallel() + var tracer *tracing.Hooks + + cases := []struct { + name string + otherStackSize int + traverseRight bool + }{ + {name: "Traverse right with empty stacks", otherStackSize: 0, traverseRight: true}, + {name: "Traverse left with empty stacks", otherStackSize: 0, traverseRight: false}, + {name: "Traverse right with one non-empty stack on the other side", otherStackSize: 1, traverseRight: true}, + {name: "Traverse left with one non-empty stack on the other side", otherStackSize: 1, traverseRight: false}, + } + // Generate proof variations + proofVariations := GenerateEmptyThreadProofVariations(t) + + for i, c := range cases { + for _, proofCase := range proofVariations { + testName := fmt.Sprintf("%v (proofCase=%v)", c.name, proofCase.Name) + t.Run(testName, func(t *testing.T) { + goVm, state, contracts := setup(t, i*123, nil) + mttestutil.SetupThreads(int64(i*123), state, c.traverseRight, 0, c.otherStackSize) + + require.PanicsWithValue(t, "Active thread stack is empty", func() { _, _ = goVm.Step(false) }) + + errorMessage := "MIPS2: active thread stack is empty" + testutil.AssertEVMReverts(t, state, contracts, tracer, proofCase.Proof, errorMessage) + }) + } + } +} + func TestEVM_NormalTraversalStep_HandleWaitingThread(t *testing.T) { var tracer *tracing.Hooks cases := []struct { diff --git a/cannon/mipsevm/tests/helpers.go b/cannon/mipsevm/tests/helpers.go index 3cbbcccefea7..c4f857f34b83 100644 --- a/cannon/mipsevm/tests/helpers.go +++ b/cannon/mipsevm/tests/helpers.go @@ -129,3 +129,20 @@ func GetMipsVersionTestCases(t require.TestingT) []VersionedVMTestCase { } } } + +type threadProofTestcase struct { + Name string + Proof []byte +} + +func GenerateEmptyThreadProofVariations(t require.TestingT) []threadProofTestcase { + defaultThreadProof := multiThreadedProofGenerator(t, multithreaded.CreateEmptyState()) + zeroBytesThreadProof := make([]byte, multithreaded.THREAD_WITNESS_SIZE) + copy(zeroBytesThreadProof[multithreaded.SERIALIZED_THREAD_SIZE:], defaultThreadProof[multithreaded.SERIALIZED_THREAD_SIZE:]) + nilBytesThreadProof := defaultThreadProof[multithreaded.SERIALIZED_THREAD_SIZE:] + return []threadProofTestcase{ + {Name: "default thread proof", Proof: defaultThreadProof}, + {Name: "zeroed thread bytes proof", Proof: zeroBytesThreadProof}, + {Name: "nil thread bytes proof", Proof: nilBytesThreadProof}, + } +} diff --git a/packages/contracts-bedrock/semver-lock.json b/packages/contracts-bedrock/semver-lock.json index 724d889f5326..f058f3b4758f 100644 --- a/packages/contracts-bedrock/semver-lock.json +++ b/packages/contracts-bedrock/semver-lock.json @@ -136,8 +136,8 @@ "sourceCodeHash": "0xaf7416f27db1b393092f51d290a29293184105bc5f0d89cd6048f687cebc7d69" }, "src/cannon/MIPS2.sol": { - "initCodeHash": "0xbb203b0d83efddfa0f664dbc63ec55844318b48fe8133758307f64e87c892a47", - "sourceCodeHash": "0x16614cc0e6abf7e81e1e5dc2c0773ee7101cb38af40e0907a8800ca7eddd3b5a" + "initCodeHash": "0x9ba94a69090a8c89786cdb2a5980deba4b5b16bbf5909f8275e090dbcd65e5c3", + "sourceCodeHash": "0x3859b4bf63f485800b0eb6ffb83a79c8d134f7e4cbbe93fbc72cc2ccd4f91b82" }, "src/cannon/PreimageOracle.sol": { "initCodeHash": "0x64ea814bf9769257c91da57928675d3f8462374b0c23bdf860ccfc79f41f7801", diff --git a/packages/contracts-bedrock/src/cannon/MIPS2.sol b/packages/contracts-bedrock/src/cannon/MIPS2.sol index 77d3530e0001..6d7ca94fa8b1 100644 --- a/packages/contracts-bedrock/src/cannon/MIPS2.sol +++ b/packages/contracts-bedrock/src/cannon/MIPS2.sol @@ -57,8 +57,8 @@ contract MIPS2 is ISemver { } /// @notice The semantic version of the MIPS2 contract. - /// @custom:semver 1.0.0-beta.14 - string public constant version = "1.0.0-beta.14"; + /// @custom:semver 1.0.0-beta.15 + string public constant version = "1.0.0-beta.15"; /// @notice The preimage oracle contract. IPreimageOracle internal immutable ORACLE; @@ -162,8 +162,11 @@ contract MIPS2 is ISemver { return outputState(); } - if (state.leftThreadStack == EMPTY_THREAD_ROOT && state.rightThreadStack == EMPTY_THREAD_ROOT) { - revert("MIPS2: illegal vm state"); + if ( + (state.leftThreadStack == EMPTY_THREAD_ROOT && !state.traverseRight) + || (state.rightThreadStack == EMPTY_THREAD_ROOT && state.traverseRight) + ) { + revert("MIPS2: active thread stack is empty"); } state.step += 1; From 12225341d979f957017b8b0f9ccc8ae96ce465c6 Mon Sep 17 00:00:00 2001 From: Joshua Gutow Date: Fri, 11 Oct 2024 15:01:40 -0700 Subject: [PATCH 16/31] op-service: Add optional headers to the signer client (#12407) * op-service: Add optional headers to the signer client * Explain flag usage --- op-service/signer/cli.go | 21 +++++++++++++++++++++ op-service/signer/cli_test.go | 35 +++++++++++++++++++++++++++++++++++ op-service/signer/client.go | 8 ++++---- op-service/tls/cli.go | 6 +++++- op-service/tls/cli_test.go | 1 + 5 files changed, 66 insertions(+), 5 deletions(-) diff --git a/op-service/signer/cli.go b/op-service/signer/cli.go index 1da5330dc4d6..0c1df648286d 100644 --- a/op-service/signer/cli.go +++ b/op-service/signer/cli.go @@ -2,6 +2,8 @@ package signer import ( "errors" + "net/http" + "strings" "github.com/urfave/cli/v2" @@ -12,6 +14,7 @@ import ( const ( EndpointFlagName = "signer.endpoint" AddressFlagName = "signer.address" + HeadersFlagName = "signer.header" ) func CLIFlags(envPrefix string) []cli.Flag { @@ -27,6 +30,11 @@ func CLIFlags(envPrefix string) []cli.Flag { Usage: "Address the signer is signing transactions for", EnvVars: opservice.PrefixEnvVar(envPrefix, "ADDRESS"), }, + &cli.StringSliceFlag{ + Name: HeadersFlagName, + Usage: "Headers to pass to the remote signer. Format `key=value`. Value can contain any character allowed in a HTTP header. When using env vars, split with commas. When using flags one key value pair per flag.", + EnvVars: opservice.PrefixEnvVar(envPrefix, "HEADER"), + }, } flags = append(flags, optls.CLIFlagsWithFlagPrefix(envPrefix, "signer")...) return flags @@ -35,11 +43,13 @@ func CLIFlags(envPrefix string) []cli.Flag { type CLIConfig struct { Endpoint string Address string + Headers http.Header TLSConfig optls.CLIConfig } func NewCLIConfig() CLIConfig { return CLIConfig{ + Headers: http.Header{}, TLSConfig: optls.NewCLIConfig(), } } @@ -62,9 +72,20 @@ func (c CLIConfig) Enabled() bool { } func ReadCLIConfig(ctx *cli.Context) CLIConfig { + var headers = http.Header{} + if ctx.StringSlice(HeadersFlagName) != nil { + for _, header := range ctx.StringSlice(HeadersFlagName) { + args := strings.SplitN(header, "=", 2) + if len(args) == 2 { + headers.Set(args[0], args[1]) + } + } + } + cfg := CLIConfig{ Endpoint: ctx.String(EndpointFlagName), Address: ctx.String(AddressFlagName), + Headers: headers, TLSConfig: optls.ReadCLIConfigWithPrefix(ctx, "signer"), } return cfg diff --git a/op-service/signer/cli_test.go b/op-service/signer/cli_test.go index a7fd7385a30e..056ed4815601 100644 --- a/op-service/signer/cli_test.go +++ b/op-service/signer/cli_test.go @@ -1,6 +1,7 @@ package signer import ( + "net/http" "testing" "github.com/stretchr/testify/require" @@ -18,6 +19,37 @@ func TestDefaultConfigIsValid(t *testing.T) { require.NoError(t, err) } +func TestHeaderParsing(t *testing.T) { + testHeaders := []string{ + "test-key=this:is:a:value", + "b64-test-key=value:dGVzdCBkYXRhIDE=$", + } + + args := []string{"app", "--signer.header", testHeaders[0], "--signer.header", testHeaders[1]} + cfg := configForArgs(args...) + + expectedHeaders := http.Header{} + expectedHeaders.Set("test-key", "this:is:a:value") + expectedHeaders.Set("b64-test-key", "value:dGVzdCBkYXRhIDE=$") + + require.Equal(t, expectedHeaders, cfg.Headers) +} + +func TestHeaderParsingWithComma(t *testing.T) { + testHeaders := []string{ + "test-key=this:is:a:value,b64-test-key=value:dGVzdCBkYXRhIDE=$", + } + + args := []string{"app", "--signer.header", testHeaders[0]} + cfg := configForArgs(args...) + + expectedHeaders := http.Header{} + expectedHeaders.Set("test-key", "this:is:a:value") + expectedHeaders.Set("b64-test-key", "value:dGVzdCBkYXRhIDE=$") + + require.Equal(t, expectedHeaders, cfg.Headers) +} + func TestInvalidConfig(t *testing.T) { tests := []struct { name string @@ -29,6 +61,7 @@ func TestInvalidConfig(t *testing.T) { expected: "signer endpoint and address must both be set or not set", configChange: func(config *CLIConfig) { config.Address = "0x1234" + config.TLSConfig.Enabled = true }, }, { @@ -36,6 +69,7 @@ func TestInvalidConfig(t *testing.T) { expected: "signer endpoint and address must both be set or not set", configChange: func(config *CLIConfig) { config.Endpoint = "http://localhost" + config.TLSConfig.Enabled = true }, }, { @@ -43,6 +77,7 @@ func TestInvalidConfig(t *testing.T) { expected: "all tls flags must be set if at least one is set", configChange: func(config *CLIConfig) { config.TLSConfig.TLSKey = "" + config.TLSConfig.Enabled = true }, }, } diff --git a/op-service/signer/client.go b/op-service/signer/client.go index 9822a1a7409f..cdb9094dfe6b 100644 --- a/op-service/signer/client.go +++ b/op-service/signer/client.go @@ -25,9 +25,9 @@ type SignerClient struct { logger log.Logger } -func NewSignerClient(logger log.Logger, endpoint string, tlsConfig optls.CLIConfig) (*SignerClient, error) { +func NewSignerClient(logger log.Logger, endpoint string, headers http.Header, tlsConfig optls.CLIConfig) (*SignerClient, error) { var httpClient *http.Client - if tlsConfig.TLSCaCert != "" { + if tlsConfig.Enabled { logger.Info("tlsConfig specified, loading tls config") caCert, err := os.ReadFile(tlsConfig.TLSCaCert) if err != nil { @@ -63,7 +63,7 @@ func NewSignerClient(logger log.Logger, endpoint string, tlsConfig optls.CLIConf httpClient = http.DefaultClient } - rpcClient, err := rpc.DialOptions(context.Background(), endpoint, rpc.WithHTTPClient(httpClient)) + rpcClient, err := rpc.DialOptions(context.Background(), endpoint, rpc.WithHTTPClient(httpClient), rpc.WithHeaders(headers)) if err != nil { return nil, err } @@ -79,7 +79,7 @@ func NewSignerClient(logger log.Logger, endpoint string, tlsConfig optls.CLIConf } func NewSignerClientFromConfig(logger log.Logger, config CLIConfig) (*SignerClient, error) { - return NewSignerClient(logger, config.Endpoint, config.TLSConfig) + return NewSignerClient(logger, config.Endpoint, config.Headers, config.TLSConfig) } func (s *SignerClient) pingVersion() (string, error) { diff --git a/op-service/tls/cli.go b/op-service/tls/cli.go index 1d5f59ae3616..b00b084cd339 100644 --- a/op-service/tls/cli.go +++ b/op-service/tls/cli.go @@ -64,6 +64,7 @@ type CLIConfig struct { TLSCaCert string TLSCert string TLSKey string + Enabled bool } func NewCLIConfig() CLIConfig { @@ -71,6 +72,7 @@ func NewCLIConfig() CLIConfig { TLSCaCert: defaultTLSCaCert, TLSCert: defaultTLSCert, TLSKey: defaultTLSKey, + Enabled: false, } } @@ -83,7 +85,7 @@ func (c CLIConfig) Check() error { } func (c CLIConfig) TLSEnabled() bool { - return !(c.TLSCaCert == "" && c.TLSCert == "" && c.TLSKey == "") + return c.Enabled } // ReadCLIConfig reads tls cli configs @@ -93,6 +95,7 @@ func ReadCLIConfig(ctx *cli.Context) CLIConfig { TLSCaCert: ctx.String(TLSCaCertFlagName), TLSCert: ctx.String(TLSCertFlagName), TLSKey: ctx.String(TLSKeyFlagName), + Enabled: ctx.IsSet(TLSCaCertFlagName) || ctx.IsSet(TLSCertFlagName) || ctx.IsSet(TLSKeyFlagName), } } @@ -106,5 +109,6 @@ func ReadCLIConfigWithPrefix(ctx *cli.Context, flagPrefix string) CLIConfig { TLSCaCert: ctx.String(prefixFunc(TLSCaCertFlagName)), TLSCert: ctx.String(prefixFunc(TLSCertFlagName)), TLSKey: ctx.String(prefixFunc(TLSKeyFlagName)), + Enabled: ctx.IsSet(TLSCaCertFlagName) || ctx.IsSet(TLSCertFlagName) || ctx.IsSet(TLSKeyFlagName), } } diff --git a/op-service/tls/cli_test.go b/op-service/tls/cli_test.go index c2d39dc3daab..f926a19fabdd 100644 --- a/op-service/tls/cli_test.go +++ b/op-service/tls/cli_test.go @@ -36,6 +36,7 @@ func TestInvalidConfig(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { cfg := NewCLIConfig() + cfg.Enabled = true test.configChange(&cfg) err := cfg.Check() require.ErrorContains(t, err, "all tls flags must be set if at least one is set") From 1f910540b10492ecacd237a90535bdcaccfcf762 Mon Sep 17 00:00:00 2001 From: Afanti <127061691+threewebcode@users.noreply.github.com> Date: Sat, 12 Oct 2024 06:07:28 +0800 Subject: [PATCH 17/31] fix: L1 blobs fetcher interface implementation assertion (#12403) --- op-program/client/l1/blob_fetcher.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/op-program/client/l1/blob_fetcher.go b/op-program/client/l1/blob_fetcher.go index b41082e4d4d8..dd704bf8dbd0 100644 --- a/op-program/client/l1/blob_fetcher.go +++ b/op-program/client/l1/blob_fetcher.go @@ -16,7 +16,7 @@ type BlobFetcher struct { oracle Oracle } -var _ = (*derive.L1BlobsFetcher)(nil) +var _ derive.L1BlobsFetcher = (*BlobFetcher)(nil) func NewBlobFetcher(logger log.Logger, oracle Oracle) *BlobFetcher { return &BlobFetcher{ From 9d9dc32977d5cda01628530b6f54047e4d4e0f9b Mon Sep 17 00:00:00 2001 From: mbaxter Date: Fri, 11 Oct 2024 15:44:23 -0700 Subject: [PATCH 18/31] cannon: Add more load / store tests (#12432) * cannon: Add a few more load/store test cases * cannon: Add some more load test cases with negative numbers * cannon: Specify test names more precisely --- cannon/mipsevm/tests/evm_common_test.go | 85 +++++++++++++++---------- 1 file changed, 53 insertions(+), 32 deletions(-) diff --git a/cannon/mipsevm/tests/evm_common_test.go b/cannon/mipsevm/tests/evm_common_test.go index 0b54a50bc28e..3900d0216fd8 100644 --- a/cannon/mipsevm/tests/evm_common_test.go +++ b/cannon/mipsevm/tests/evm_common_test.go @@ -245,69 +245,90 @@ func TestEVMSingleStep_Operators(t *testing.T) { func TestEVMSingleStep_LoadStore(t *testing.T) { var tracer *tracing.Hooks + loadMemVal := Word(0x11_22_33_44) + loadMemValNeg := Word(0xF1_F2_F3_F4) + rtVal := Word(0xaa_bb_cc_dd) versions := GetMipsVersionTestCases(t) cases := []struct { name string - rs Word rt Word - isUnAligned bool + base Word + imm uint32 opcode uint32 memVal Word expectMemVal Word expectRes Word }{ - {name: "lb", opcode: uint32(0x20), memVal: Word(0x12_00_00_00), expectRes: Word(0x12)}, // lb $t0, 4($t1) - {name: "lh", opcode: uint32(0x21), memVal: Word(0x12_23_00_00), expectRes: Word(0x12_23)}, // lh $t0, 4($t1) - {name: "lw", opcode: uint32(0x23), memVal: Word(0x12_23_45_67), expectRes: Word(0x12_23_45_67)}, // lw $t0, 4($t1) - {name: "lbu", opcode: uint32(0x24), memVal: Word(0x12_23_00_00), expectRes: Word(0x12)}, // lbu $t0, 4($t1) - {name: "lhu", opcode: uint32(0x25), memVal: Word(0x12_23_00_00), expectRes: Word(0x12_23)}, // lhu $t0, 4($t1) - {name: "lwl", opcode: uint32(0x22), rt: Word(0xaa_bb_cc_dd), memVal: Word(0x12_34_56_78), expectRes: Word(0x12_34_56_78)}, // lwl $t0, 4($t1) - {name: "lwl unaligned address", opcode: uint32(0x22), rt: Word(0xaa_bb_cc_dd), isUnAligned: true, memVal: Word(0x12_34_56_78), expectRes: Word(0x34_56_78_dd)}, // lwl $t0, 5($t1) - {name: "lwr", opcode: uint32(0x26), rt: Word(0xaa_bb_cc_dd), memVal: Word(0x12_34_56_78), expectRes: Word(0xaa_bb_cc_12)}, // lwr $t0, 4($t1) - {name: "lwr unaligned address", opcode: uint32(0x26), rt: Word(0xaa_bb_cc_dd), isUnAligned: true, memVal: Word(0x12_34_56_78), expectRes: Word(0xaa_bb_12_34)}, // lwr $t0, 5($t1) - {name: "sb", opcode: uint32(0x28), rt: Word(0xaa_bb_cc_dd), expectMemVal: Word(0xdd_00_00_00)}, // sb $t0, 4($t1) - {name: "sh", opcode: uint32(0x29), rt: Word(0xaa_bb_cc_dd), expectMemVal: Word(0xcc_dd_00_00)}, // sh $t0, 4($t1) - {name: "swl", opcode: uint32(0x2a), rt: Word(0xaa_bb_cc_dd), expectMemVal: Word(0xaa_bb_cc_dd)}, // swl $t0, 4($t1) - {name: "sw", opcode: uint32(0x2b), rt: Word(0xaa_bb_cc_dd), expectMemVal: Word(0xaa_bb_cc_dd)}, // sw $t0, 4($t1) - {name: "swr unaligned address", opcode: uint32(0x2e), rt: Word(0xaa_bb_cc_dd), isUnAligned: true, expectMemVal: Word(0xcc_dd_00_00)}, // swr $t0, 5($t1) + {name: "lb, offset=0", opcode: uint32(0x20), base: 0x100, imm: 0x20, memVal: loadMemVal, expectRes: 0x11}, + {name: "lb, offset=1", opcode: uint32(0x20), base: 0x100, imm: 0x1, memVal: loadMemVal, expectRes: 0x22}, + {name: "lb, offset=2", opcode: uint32(0x20), base: 0x100, imm: 0x2, memVal: loadMemVal, expectRes: 0x33}, + {name: "lb, offset=2, variation", opcode: uint32(0x20), base: 0x102, imm: 0x20, memVal: loadMemVal, expectRes: 0x33}, + {name: "lb, offset=4", opcode: uint32(0x20), base: 0x103, imm: 0x0, memVal: loadMemVal, expectRes: 0x44}, + {name: "lb, negative, offset=0", opcode: uint32(0x20), base: 0x100, imm: 0x0, memVal: loadMemValNeg, expectRes: 0xFF_FF_FF_F1}, + {name: "lb, negative, offset=1", opcode: uint32(0x20), base: 0x101, imm: 0x0, memVal: loadMemValNeg, expectRes: 0xFF_FF_FF_F2}, + {name: "lb, negative, offset=2", opcode: uint32(0x20), base: 0x102, imm: 0x0, memVal: loadMemValNeg, expectRes: 0xFF_FF_FF_F3}, + {name: "lb, negative, offset=3", opcode: uint32(0x20), base: 0x103, imm: 0x0, memVal: loadMemValNeg, expectRes: 0xFF_FF_FF_F4}, + {name: "lh, offset=0", opcode: uint32(0x21), base: 0x100, imm: 0x20, memVal: loadMemVal, expectRes: 0x11_22}, + {name: "lh, offset=1", opcode: uint32(0x21), base: 0x101, imm: 0x20, memVal: loadMemVal, expectRes: 0x11_22}, + {name: "lh, offset=2", opcode: uint32(0x21), base: 0x102, imm: 0x20, memVal: loadMemVal, expectRes: 0x33_44}, + {name: "lh, offset=3", opcode: uint32(0x21), base: 0x102, imm: 0x1, memVal: loadMemVal, expectRes: 0x33_44}, + {name: "lh, negative, offset=0", opcode: uint32(0x21), base: 0x100, imm: 0x20, memVal: loadMemValNeg, expectRes: 0xFF_FF_F1_F2}, + {name: "lh, negative, offset=3", opcode: uint32(0x21), base: 0x102, imm: 0x1, memVal: loadMemValNeg, expectRes: 0xFF_FF_F3_F4}, + {name: "lw", opcode: uint32(0x23), base: 0x100, imm: 0x20, memVal: loadMemVal, expectRes: 0x11_22_33_44}, + {name: "lbu", opcode: uint32(0x24), base: 0x100, imm: 0x20, memVal: loadMemVal, expectRes: 0x11}, + {name: "lbu, negative", opcode: uint32(0x24), base: 0x100, imm: 0x20, memVal: loadMemValNeg, expectRes: 0xF1}, + {name: "lhu", opcode: uint32(0x25), base: 0x100, imm: 0x20, memVal: loadMemVal, expectRes: 0x11_22}, + {name: "lhu, negative", opcode: uint32(0x25), base: 0x100, imm: 0x20, memVal: loadMemValNeg, expectRes: 0xF1_F2}, + {name: "lwl", opcode: uint32(0x22), base: 0x100, imm: 0x20, rt: rtVal, memVal: loadMemVal, expectRes: loadMemVal}, + {name: "lwl unaligned", opcode: uint32(0x22), base: 0x100, imm: 0x1, rt: rtVal, memVal: loadMemVal, expectRes: 0x22_33_44_dd}, + {name: "lwr", opcode: uint32(0x26), base: 0x100, imm: 0x20, rt: rtVal, memVal: loadMemVal, expectRes: 0xaa_bb_cc_11}, + {name: "lwr unaligned", opcode: uint32(0x26), base: 0x100, imm: 0x1, rt: rtVal, memVal: loadMemVal, expectRes: 0xaa_bb_11_22}, + {name: "sb, offset=0", opcode: uint32(0x28), base: 0x100, imm: 0x20, rt: rtVal, expectMemVal: 0xdd_00_00_00}, + {name: "sb, offset=1", opcode: uint32(0x28), base: 0x100, imm: 0x21, rt: rtVal, expectMemVal: 0x00_dd_00_00}, + {name: "sb, offset=2", opcode: uint32(0x28), base: 0x102, imm: 0x20, rt: rtVal, expectMemVal: 0x00_00_dd_00}, + {name: "sb, offset=3", opcode: uint32(0x28), base: 0x103, imm: 0x20, rt: rtVal, expectMemVal: 0x00_00_00_dd}, + {name: "sh, offset=0", opcode: uint32(0x29), base: 0x100, imm: 0x20, rt: rtVal, expectMemVal: 0xcc_dd_00_00}, + {name: "sh, offset=1", opcode: uint32(0x29), base: 0x100, imm: 0x21, rt: rtVal, expectMemVal: 0xcc_dd_00_00}, + {name: "sh, offset=2", opcode: uint32(0x29), base: 0x102, imm: 0x20, rt: rtVal, expectMemVal: 0x00_00_cc_dd}, + {name: "sh, offset=3", opcode: uint32(0x29), base: 0x102, imm: 0x21, rt: rtVal, expectMemVal: 0x00_00_cc_dd}, + {name: "swl", opcode: uint32(0x2a), base: 0x100, imm: 0x20, rt: rtVal, expectMemVal: 0xaa_bb_cc_dd}, + {name: "sw", opcode: uint32(0x2b), base: 0x100, imm: 0x20, rt: rtVal, expectMemVal: 0xaa_bb_cc_dd}, + {name: "swr unaligned", opcode: uint32(0x2e), base: 0x100, imm: 0x1, rt: rtVal, expectMemVal: 0xcc_dd_00_00}, } - var t1 Word = 0x100 var baseReg uint32 = 9 var rtReg uint32 = 8 - for _, v := range versions { - for i, tt := range cases { + for i, tt := range cases { + for _, v := range versions { testName := fmt.Sprintf("%v (%v)", tt.name, v.Name) t.Run(testName, func(t *testing.T) { + addr := tt.base + Word(tt.imm) + effAddr := arch.AddressMask & addr + + // Setup goVm := v.VMFactory(nil, os.Stdout, os.Stderr, testutil.CreateLogger(), testutil.WithRandomization(int64(i)), testutil.WithPC(0), testutil.WithNextPC(4)) state := goVm.GetState() - var insn uint32 - imm := uint32(0x4) - if tt.isUnAligned { - imm = uint32(0x5) - } - - insn = tt.opcode<<26 | baseReg<<21 | rtReg<<16 | imm + insn := tt.opcode<<26 | baseReg<<21 | rtReg<<16 | tt.imm state.GetRegistersRef()[rtReg] = tt.rt - state.GetRegistersRef()[baseReg] = t1 - + state.GetRegistersRef()[baseReg] = tt.base state.GetMemory().SetUint32(0, insn) - state.GetMemory().SetWord(t1+4, tt.memVal) + state.GetMemory().SetWord(effAddr, tt.memVal) step := state.GetStep() // Setup expectations expected := testutil.NewExpectedState(state) expected.ExpectStep() - if tt.expectMemVal != 0 { - expected.ExpectMemoryWriteWord(t1+4, tt.expectMemVal) + expected.ExpectMemoryWriteWord(effAddr, tt.expectMemVal) } else { expected.Registers[rtReg] = tt.expectRes } + + // Run vm stepWitness, err := goVm.Step(true) require.NoError(t, err) - // Check expectations + // Validate expected.Validate(t, state) testutil.ValidateEVM(t, stepWitness, step, goVm, v.StateHashFn, v.Contracts, tracer) }) From 14b437b010511401af102f40626102dcb5cc4fb8 Mon Sep 17 00:00:00 2001 From: Adrian Sutton Date: Tue, 15 Oct 2024 05:07:17 +1000 Subject: [PATCH 19/31] op-challenger: Set op-program log level based on the challenger level (#12379) * op-challenger: Set op-program log level based on the challenger level. * op-challenger: Fix tests to actually assert values. --- op-challenger/game/fault/register.go | 6 +- .../game/fault/trace/asterisc/provider.go | 2 +- .../game/fault/trace/cannon/provider.go | 2 +- .../game/fault/trace/vm/executor_test.go | 2 +- .../trace/vm/op_program_server_executor.go | 23 ++- .../vm/op_program_server_executor_test.go | 152 ++++++++++-------- op-challenger/runner/factory.go | 6 +- .../disputegame/output_cannon_helper.go | 2 +- op-e2e/faultproofs/precompile_test.go | 2 +- 9 files changed, 118 insertions(+), 79 deletions(-) diff --git a/op-challenger/game/fault/register.go b/op-challenger/game/fault/register.go index 38957be0ce95..17874e7ddde8 100644 --- a/op-challenger/game/fault/register.go +++ b/op-challenger/game/fault/register.go @@ -68,13 +68,13 @@ func RegisterGameTypes( var registerTasks []*RegisterTask if cfg.TraceTypeEnabled(faultTypes.TraceTypeCannon) { - registerTasks = append(registerTasks, NewCannonRegisterTask(faultTypes.CannonGameType, cfg, m, vm.NewOpProgramServerExecutor())) + registerTasks = append(registerTasks, NewCannonRegisterTask(faultTypes.CannonGameType, cfg, m, vm.NewOpProgramServerExecutor(logger))) } if cfg.TraceTypeEnabled(faultTypes.TraceTypePermissioned) { - registerTasks = append(registerTasks, NewCannonRegisterTask(faultTypes.PermissionedGameType, cfg, m, vm.NewOpProgramServerExecutor())) + registerTasks = append(registerTasks, NewCannonRegisterTask(faultTypes.PermissionedGameType, cfg, m, vm.NewOpProgramServerExecutor(logger))) } if cfg.TraceTypeEnabled(faultTypes.TraceTypeAsterisc) { - registerTasks = append(registerTasks, NewAsteriscRegisterTask(faultTypes.AsteriscGameType, cfg, m, vm.NewOpProgramServerExecutor())) + registerTasks = append(registerTasks, NewAsteriscRegisterTask(faultTypes.AsteriscGameType, cfg, m, vm.NewOpProgramServerExecutor(logger))) } if cfg.TraceTypeEnabled(faultTypes.TraceTypeAsteriscKona) { registerTasks = append(registerTasks, NewAsteriscKonaRegisterTask(faultTypes.AsteriscKonaGameType, cfg, m, vm.NewKonaExecutor())) diff --git a/op-challenger/game/fault/trace/asterisc/provider.go b/op-challenger/game/fault/trace/asterisc/provider.go index 0916cd29397d..c622b6ed9dd1 100644 --- a/op-challenger/game/fault/trace/asterisc/provider.go +++ b/op-challenger/game/fault/trace/asterisc/provider.go @@ -168,7 +168,7 @@ func NewTraceProviderForTest(logger log.Logger, m vm.Metricer, cfg *config.Confi logger: logger, dir: dir, prestate: cfg.AsteriscAbsolutePreState, - generator: vm.NewExecutor(logger, m, cfg.Asterisc, vm.NewOpProgramServerExecutor(), cfg.AsteriscAbsolutePreState, localInputs), + generator: vm.NewExecutor(logger, m, cfg.Asterisc, vm.NewOpProgramServerExecutor(logger), cfg.AsteriscAbsolutePreState, localInputs), gameDepth: gameDepth, preimageLoader: utils.NewPreimageLoader(func() (utils.PreimageSource, error) { return kvstore.NewDiskKV(logger, vm.PreimageDir(dir), kvtypes.DataFormatFile) diff --git a/op-challenger/game/fault/trace/cannon/provider.go b/op-challenger/game/fault/trace/cannon/provider.go index cca2cf0e484e..7cffccfe8433 100644 --- a/op-challenger/game/fault/trace/cannon/provider.go +++ b/op-challenger/game/fault/trace/cannon/provider.go @@ -167,7 +167,7 @@ func NewTraceProviderForTest(logger log.Logger, m vm.Metricer, cfg *config.Confi logger: logger, dir: dir, prestate: cfg.CannonAbsolutePreState, - generator: vm.NewExecutor(logger, m, cfg.Cannon, vm.NewOpProgramServerExecutor(), cfg.CannonAbsolutePreState, localInputs), + generator: vm.NewExecutor(logger, m, cfg.Cannon, vm.NewOpProgramServerExecutor(logger), cfg.CannonAbsolutePreState, localInputs), gameDepth: gameDepth, preimageLoader: utils.NewPreimageLoader(func() (utils.PreimageSource, error) { return kvstore.NewDiskKV(logger, vm.PreimageDir(dir), kvtypes.DataFormatFile) diff --git a/op-challenger/game/fault/trace/vm/executor_test.go b/op-challenger/game/fault/trace/vm/executor_test.go index ff4a8ba7e1ab..13146d8f263b 100644 --- a/op-challenger/game/fault/trace/vm/executor_test.go +++ b/op-challenger/game/fault/trace/vm/executor_test.go @@ -41,7 +41,7 @@ func TestGenerateProof(t *testing.T) { } captureExec := func(t *testing.T, cfg Config, proofAt uint64) (string, string, map[string]string) { m := &stubVmMetrics{} - executor := NewExecutor(testlog.Logger(t, log.LevelInfo), m, cfg, NewOpProgramServerExecutor(), prestate, inputs) + executor := NewExecutor(testlog.Logger(t, log.LevelInfo), m, cfg, NewOpProgramServerExecutor(testlog.Logger(t, log.LvlInfo)), prestate, inputs) executor.selectSnapshot = func(logger log.Logger, dir string, absolutePreState string, i uint64, binary bool) (string, error) { return input, nil } diff --git a/op-challenger/game/fault/trace/vm/op_program_server_executor.go b/op-challenger/game/fault/trace/vm/op_program_server_executor.go index 11044307e9af..1b62e42938e7 100644 --- a/op-challenger/game/fault/trace/vm/op_program_server_executor.go +++ b/op-challenger/game/fault/trace/vm/op_program_server_executor.go @@ -1,16 +1,20 @@ package vm import ( + "context" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/utils" + "github.com/ethereum/go-ethereum/log" ) type OpProgramServerExecutor struct { + logger log.Logger } var _ OracleServerExecutor = (*OpProgramServerExecutor)(nil) -func NewOpProgramServerExecutor() *OpProgramServerExecutor { - return &OpProgramServerExecutor{} +func NewOpProgramServerExecutor(logger log.Logger) *OpProgramServerExecutor { + return &OpProgramServerExecutor{logger: logger} } func (s *OpProgramServerExecutor) OracleCommand(cfg Config, dataDir string, inputs utils.LocalGameInputs) ([]string, error) { @@ -35,5 +39,20 @@ func (s *OpProgramServerExecutor) OracleCommand(cfg Config, dataDir string, inpu if cfg.L2GenesisPath != "" { args = append(args, "--l2.genesis", cfg.L2GenesisPath) } + var logLevel string + if s.logger.Enabled(context.Background(), log.LevelTrace) { + logLevel = "TRACE" + } else if s.logger.Enabled(context.Background(), log.LevelDebug) { + logLevel = "DEBUG" + } else if s.logger.Enabled(context.Background(), log.LevelInfo) { + logLevel = "INFO" + } else if s.logger.Enabled(context.Background(), log.LevelWarn) { + logLevel = "WARN" + } else if s.logger.Enabled(context.Background(), log.LevelError) { + logLevel = "ERROR" + } else { + logLevel = "CRIT" + } + args = append(args, "--log.level", logLevel) return args, nil } diff --git a/op-challenger/game/fault/trace/vm/op_program_server_executor_test.go b/op-challenger/game/fault/trace/vm/op_program_server_executor_test.go index ff17209d6324..90fad9507b90 100644 --- a/op-challenger/game/fault/trace/vm/op_program_server_executor_test.go +++ b/op-challenger/game/fault/trace/vm/op_program_server_executor_test.go @@ -1,98 +1,118 @@ package vm import ( + "fmt" + "log/slog" "math/big" - "slices" "testing" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/utils" + "github.com/ethereum-optimism/optimism/op-service/testlog" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" "github.com/stretchr/testify/require" ) func TestOpProgramFillHostCommand(t *testing.T) { dir := "mockdir" - cfg := Config{ - L1: "http://localhost:8888", - L1Beacon: "http://localhost:9000", - L2: "http://localhost:9999", - Server: "./bin/mockserver", - } - inputs := utils.LocalGameInputs{ - L1Head: common.Hash{0x11}, - L2Head: common.Hash{0x22}, - L2OutputRoot: common.Hash{0x33}, - L2Claim: common.Hash{0x44}, - L2BlockNumber: big.NewInt(3333), - } - validateStandard := func(t *testing.T, args []string) { - require.True(t, slices.Contains(args, "--server")) - require.True(t, slices.Contains(args, "--l1")) - require.True(t, slices.Contains(args, "--l1.beacon")) - require.True(t, slices.Contains(args, "--l2")) - require.True(t, slices.Contains(args, "--datadir")) - require.True(t, slices.Contains(args, "--l1.head")) - require.True(t, slices.Contains(args, "--l2.head")) - require.True(t, slices.Contains(args, "--l2.outputroot")) - require.True(t, slices.Contains(args, "--l2.claim")) - require.True(t, slices.Contains(args, "--l2.blocknumber")) + toPairs := func(args []string) map[string]string { + pairs := make(map[string]string, len(args)/2) + for i := 0; i < len(args); i += 2 { + pairs[args[i]] = args[i+1] + } + return pairs } - t.Run("NoExtras", func(t *testing.T) { - vmConfig := NewOpProgramServerExecutor() - - args, err := vmConfig.OracleCommand(cfg, dir, inputs) + oracleCommand := func(t *testing.T, lvl slog.Level, configModifier func(c *Config)) map[string]string { + cfg := Config{ + L1: "http://localhost:8888", + L1Beacon: "http://localhost:9000", + L2: "http://localhost:9999", + Server: "./bin/mockserver", + } + inputs := utils.LocalGameInputs{ + L1Head: common.Hash{0x11}, + L2Head: common.Hash{0x22}, + L2OutputRoot: common.Hash{0x33}, + L2Claim: common.Hash{0x44}, + L2BlockNumber: big.NewInt(3333), + } + configModifier(&cfg) + executor := NewOpProgramServerExecutor(testlog.Logger(t, lvl)) + + args, err := executor.OracleCommand(cfg, dir, inputs) require.NoError(t, err) + pairs := toPairs(args) + // Validate standard options + require.Equal(t, "--server", pairs[cfg.Server]) + require.Equal(t, cfg.L1, pairs["--l1"]) + require.Equal(t, cfg.L1Beacon, pairs["--l1.beacon"]) + require.Equal(t, cfg.L2, pairs["--l2"]) + require.Equal(t, dir, pairs["--datadir"]) + require.Equal(t, inputs.L1Head.Hex(), pairs["--l1.head"]) + require.Equal(t, inputs.L2Head.Hex(), pairs["--l2.head"]) + require.Equal(t, inputs.L2OutputRoot.Hex(), pairs["--l2.outputroot"]) + require.Equal(t, inputs.L2Claim.Hex(), pairs["--l2.claim"]) + require.Equal(t, inputs.L2BlockNumber.String(), pairs["--l2.blocknumber"]) + return pairs + } - validateStandard(t, args) + t.Run("NoExtras", func(t *testing.T) { + pairs := oracleCommand(t, log.LvlInfo, func(c *Config) {}) + require.NotContains(t, pairs, "--network") + require.NotContains(t, pairs, "--rollup.config") + require.NotContains(t, pairs, "--l2.genesis") }) t.Run("WithNetwork", func(t *testing.T) { - cfg.Network = "op-test" - vmConfig := NewOpProgramServerExecutor() - - args, err := vmConfig.OracleCommand(cfg, dir, inputs) - require.NoError(t, err) - - validateStandard(t, args) - require.True(t, slices.Contains(args, "--network")) + pairs := oracleCommand(t, log.LvlInfo, func(c *Config) { + c.Network = "op-test" + }) + require.Equal(t, "op-test", pairs["--network"]) }) t.Run("WithRollupConfigPath", func(t *testing.T) { - cfg.RollupConfigPath = "rollup.config" - vmConfig := NewOpProgramServerExecutor() - - args, err := vmConfig.OracleCommand(cfg, dir, inputs) - require.NoError(t, err) - - validateStandard(t, args) - require.True(t, slices.Contains(args, "--rollup.config")) + pairs := oracleCommand(t, log.LvlInfo, func(c *Config) { + c.RollupConfigPath = "rollup.config.json" + }) + require.Equal(t, "rollup.config.json", pairs["--rollup.config"]) }) t.Run("WithL2GenesisPath", func(t *testing.T) { - cfg.L2GenesisPath = "l2.genesis" - vmConfig := NewOpProgramServerExecutor() - - args, err := vmConfig.OracleCommand(cfg, dir, inputs) - require.NoError(t, err) - - validateStandard(t, args) - require.True(t, slices.Contains(args, "--l2.genesis")) + pairs := oracleCommand(t, log.LvlInfo, func(c *Config) { + c.L2GenesisPath = "genesis.json" + }) + require.Equal(t, "genesis.json", pairs["--l2.genesis"]) }) t.Run("WithAllExtras", func(t *testing.T) { - cfg.Network = "op-test" - cfg.RollupConfigPath = "rollup.config" - cfg.L2GenesisPath = "l2.genesis" - vmConfig := NewOpProgramServerExecutor() - - args, err := vmConfig.OracleCommand(cfg, dir, inputs) - require.NoError(t, err) - - validateStandard(t, args) - require.True(t, slices.Contains(args, "--network")) - require.True(t, slices.Contains(args, "--rollup.config")) - require.True(t, slices.Contains(args, "--l2.genesis")) + pairs := oracleCommand(t, log.LvlInfo, func(c *Config) { + c.Network = "op-test" + c.RollupConfigPath = "rollup.config.json" + c.L2GenesisPath = "genesis.json" + }) + require.Equal(t, "op-test", pairs["--network"]) + require.Equal(t, "rollup.config.json", pairs["--rollup.config"]) + require.Equal(t, "genesis.json", pairs["--l2.genesis"]) }) + + logTests := []struct { + level slog.Level + arg string + }{ + {log.LevelTrace, "TRACE"}, + {log.LevelDebug, "DEBUG"}, + {log.LevelInfo, "INFO"}, + {log.LevelWarn, "WARN"}, + {log.LevelError, "ERROR"}, + {log.LevelCrit, "CRIT"}, + } + for _, logTest := range logTests { + logTest := logTest + t.Run(fmt.Sprintf("LogLevel-%v", logTest.arg), func(t *testing.T) { + pairs := oracleCommand(t, logTest.level, func(c *Config) {}) + require.Equal(t, pairs["--log.level"], logTest.arg) + }) + } } diff --git a/op-challenger/runner/factory.go b/op-challenger/runner/factory.go index 6aab9642778e..d6bfbb0f5be8 100644 --- a/op-challenger/runner/factory.go +++ b/op-challenger/runner/factory.go @@ -30,7 +30,7 @@ func createTraceProvider( ) (types.TraceProvider, error) { switch traceType { case types.TraceTypeCannon: - serverExecutor := vm.NewOpProgramServerExecutor() + serverExecutor := vm.NewOpProgramServerExecutor(logger) stateConverter := cannon.NewStateConverter(cfg.Cannon) prestate, err := getPrestate(ctx, prestateHash, cfg.CannonAbsolutePreStateBaseURL, cfg.CannonAbsolutePreState, dir, stateConverter) if err != nil { @@ -39,7 +39,7 @@ func createTraceProvider( prestateProvider := vm.NewPrestateProvider(prestate, stateConverter) return cannon.NewTraceProvider(logger, m, cfg.Cannon, serverExecutor, prestateProvider, prestate, localInputs, dir, 42), nil case types.TraceTypeAsterisc: - serverExecutor := vm.NewOpProgramServerExecutor() + serverExecutor := vm.NewOpProgramServerExecutor(logger) stateConverter := asterisc.NewStateConverter(cfg.Asterisc) prestate, err := getPrestate(ctx, prestateHash, cfg.AsteriscAbsolutePreStateBaseURL, cfg.AsteriscAbsolutePreState, dir, stateConverter) if err != nil { @@ -70,7 +70,7 @@ func createMTTraceProvider( localInputs utils.LocalGameInputs, dir string, ) (types.TraceProvider, error) { - executor := vm.NewOpProgramServerExecutor() + executor := vm.NewOpProgramServerExecutor(logger) stateConverter := cannon.NewStateConverter(vmConfig) prestateSource := prestates.NewMultiPrestateProvider(absolutePrestateBaseURL, filepath.Join(dir, "prestates"), stateConverter) diff --git a/op-e2e/e2eutils/disputegame/output_cannon_helper.go b/op-e2e/e2eutils/disputegame/output_cannon_helper.go index 264742be194b..43316a185fd3 100644 --- a/op-e2e/e2eutils/disputegame/output_cannon_helper.go +++ b/op-e2e/e2eutils/disputegame/output_cannon_helper.go @@ -89,7 +89,7 @@ func (g *OutputCannonGameHelper) CreateHonestActor(ctx context.Context, l2Node s prestateProvider := outputs.NewPrestateProvider(rollupClient, actorCfg.PrestateBlock) l1Head := g.GetL1Head(ctx) accessor, err := outputs.NewOutputCannonTraceAccessor( - logger, metrics.NoopMetrics, cfg.Cannon, vm.NewOpProgramServerExecutor(), l2Client, prestateProvider, cfg.CannonAbsolutePreState, rollupClient, dir, l1Head, splitDepth, actorCfg.PrestateBlock, actorCfg.PoststateBlock) + logger, metrics.NoopMetrics, cfg.Cannon, vm.NewOpProgramServerExecutor(logger), l2Client, prestateProvider, cfg.CannonAbsolutePreState, rollupClient, dir, l1Head, splitDepth, actorCfg.PrestateBlock, actorCfg.PoststateBlock) g.Require.NoError(err, "Failed to create output cannon trace accessor") return NewOutputHonestHelper(g.T, g.Require, &g.OutputGameHelper, g.Game, accessor) } diff --git a/op-e2e/faultproofs/precompile_test.go b/op-e2e/faultproofs/precompile_test.go index 7fa37158fd16..d055c4e8b27a 100644 --- a/op-e2e/faultproofs/precompile_test.go +++ b/op-e2e/faultproofs/precompile_test.go @@ -276,7 +276,7 @@ func runCannon(t *testing.T, ctx context.Context, sys *e2esys.System, inputs uti cannonOpts(&cfg) logger := testlog.Logger(t, log.LevelInfo).New("role", "cannon") - executor := vm.NewExecutor(logger, metrics.NoopMetrics.VmMetrics("cannon"), cfg.Cannon, vm.NewOpProgramServerExecutor(), cfg.CannonAbsolutePreState, inputs) + executor := vm.NewExecutor(logger, metrics.NoopMetrics.VmMetrics("cannon"), cfg.Cannon, vm.NewOpProgramServerExecutor(logger), cfg.CannonAbsolutePreState, inputs) t.Log("Running cannon") err := executor.DoGenerateProof(ctx, proofsDir, math.MaxUint, math.MaxUint, extraVmArgs...) From 33628f53da585fbda77a771673b0e31606181c92 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 19:42:15 +0000 Subject: [PATCH 20/31] dependabot(gomod): bump github.com/urfave/cli/v2 from 2.27.4 to 2.27.5 (#12447) Bumps [github.com/urfave/cli/v2](https://github.com/urfave/cli) from 2.27.4 to 2.27.5. - [Release notes](https://github.com/urfave/cli/releases) - [Changelog](https://github.com/urfave/cli/blob/main/docs/CHANGELOG.md) - [Commits](https://github.com/urfave/cli/compare/v2.27.4...v2.27.5) --- updated-dependencies: - dependency-name: github.com/urfave/cli/v2 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 | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 66079a8d5daa..24b80b2fe71f 100644 --- a/go.mod +++ b/go.mod @@ -45,7 +45,7 @@ require ( github.com/prometheus/client_golang v1.20.4 github.com/protolambda/ctxlock v0.1.0 github.com/stretchr/testify v1.9.0 - github.com/urfave/cli/v2 v2.27.4 + github.com/urfave/cli/v2 v2.27.5 golang.org/x/crypto v0.28.0 golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa golang.org/x/sync v0.8.0 @@ -78,7 +78,7 @@ require ( github.com/consensys/bavard v0.1.13 // indirect github.com/containerd/cgroups v1.1.0 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect diff --git a/go.sum b/go.sum index 5982cfeae140..3b723b70affb 100644 --- a/go.sum +++ b/go.sum @@ -126,8 +126,8 @@ github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8 github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c h1:uQYC5Z1mdLRPrZhHjHxufI8+2UG/i25QG92j0Er9p6I= github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c/go.mod h1:geZJZH3SzKCqnz5VT0q/DyIG/tvu/dZk+VIfXicupJs= github.com/crate-crypto/go-kzg-4844 v1.0.0 h1:TsSgHwrkTKecKJ4kadtHi4b3xHW5dCFUDFnUp1TsawI= @@ -782,8 +782,8 @@ github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oW github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8= -github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ= +github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= +github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= From 1ac85caa8a2bb30b30bf65df3ee897ec12002da2 Mon Sep 17 00:00:00 2001 From: Michael de Hoog Date: Mon, 14 Oct 2024 10:04:08 -1000 Subject: [PATCH 21/31] [batcher] Cleanup batcher channel inclusion block logic (#12363) * Cleanup batcher channel inclusion block logic * Add comment to isTimedOut * Remove updateInclusionBlocks altogether * Revert receiver variable rename * Fix tests * Fix isTimedOut for ChannelTimeouts of 1 (ensure that some txs have been confirmed) * Added comment about confirmed txs to isFullySubmitted --- op-batcher/batcher/channel.go | 39 ++++++------------------------ op-batcher/batcher/channel_test.go | 18 +++++++------- 2 files changed, 17 insertions(+), 40 deletions(-) diff --git a/op-batcher/batcher/channel.go b/op-batcher/batcher/channel.go index 399122ecaf0d..638fa098059e 100644 --- a/op-batcher/batcher/channel.go +++ b/op-batcher/batcher/channel.go @@ -25,8 +25,6 @@ type channel struct { // Set of confirmed txID -> inclusion block. For determining if the channel is timed out confirmedTransactions map[string]eth.BlockID - // True if confirmed TX list is updated. Set to false after updated min/max inclusion blocks. - confirmedTxUpdated bool // Inclusion block number of first confirmed TX minInclusionBlock uint64 // Inclusion block number of last confirmed TX @@ -42,6 +40,7 @@ func newChannel(log log.Logger, metr metrics.Metricer, cfg ChannelConfig, rollup channelBuilder: cb, pendingTransactions: make(map[string]txData), confirmedTransactions: make(map[string]eth.BlockID), + minInclusionBlock: math.MaxUint64, } } @@ -77,9 +76,12 @@ func (s *channel) TxConfirmed(id string, inclusionBlock eth.BlockID) (bool, []*t } delete(s.pendingTransactions, id) s.confirmedTransactions[id] = inclusionBlock - s.confirmedTxUpdated = true s.channelBuilder.FramePublished(inclusionBlock.Number) + // Update min/max inclusion blocks for timeout check + s.minInclusionBlock = min(s.minInclusionBlock, inclusionBlock.Number) + s.maxInclusionBlock = max(s.maxInclusionBlock, inclusionBlock.Number) + // If this channel timed out, put the pending blocks back into the local saved blocks // and then reset this state so it can try to build a new channel. if s.isTimedOut() { @@ -102,43 +104,18 @@ func (s *channel) Timeout() uint64 { return s.channelBuilder.Timeout() } -// updateInclusionBlocks finds the first & last confirmed tx and saves its inclusion numbers -func (s *channel) updateInclusionBlocks() { - if len(s.confirmedTransactions) == 0 || !s.confirmedTxUpdated { - return - } - // If there are confirmed transactions, find the first + last confirmed block numbers - min := uint64(math.MaxUint64) - max := uint64(0) - for _, inclusionBlock := range s.confirmedTransactions { - if inclusionBlock.Number < min { - min = inclusionBlock.Number - } - if inclusionBlock.Number > max { - max = inclusionBlock.Number - } - } - s.minInclusionBlock = min - s.maxInclusionBlock = max - s.confirmedTxUpdated = false -} - -// pendingChannelIsTimedOut returns true if submitted channel has timed out. +// isTimedOut returns true if submitted channel has timed out. // A channel has timed out if the difference in L1 Inclusion blocks between // the first & last included block is greater than or equal to the channel timeout. func (s *channel) isTimedOut() bool { - // Update min/max inclusion blocks for timeout check - s.updateInclusionBlocks() // Prior to the granite hard fork activating, the use of the shorter ChannelTimeout here may cause the batcher // to believe the channel timed out when it was valid. It would then resubmit the blocks needlessly. // This wastes batcher funds but doesn't cause any problems for the chain progressing safe head. - return s.maxInclusionBlock-s.minInclusionBlock >= s.cfg.ChannelTimeout + return len(s.confirmedTransactions) > 0 && s.maxInclusionBlock-s.minInclusionBlock >= s.cfg.ChannelTimeout } -// pendingChannelIsFullySubmitted returns true if the channel has been fully submitted. +// isFullySubmitted returns true if the channel has been fully submitted (all transactions are confirmed). func (s *channel) isFullySubmitted() bool { - // Update min/max inclusion blocks for timeout check - s.updateInclusionBlocks() return s.IsFull() && len(s.pendingTransactions)+s.PendingFrames() == 0 } diff --git a/op-batcher/batcher/channel_test.go b/op-batcher/batcher/channel_test.go index 2bd2cfa73990..0aad780131c7 100644 --- a/op-batcher/batcher/channel_test.go +++ b/op-batcher/batcher/channel_test.go @@ -53,16 +53,19 @@ func TestChannelTimeout(t *testing.T) { channel := m.currentChannel require.NotNil(t, channel) + // add some pending txs, to be confirmed below + channel.pendingTransactions[zeroFrameTxID(0).String()] = txData{} + channel.pendingTransactions[zeroFrameTxID(1).String()] = txData{} + channel.pendingTransactions[zeroFrameTxID(2).String()] = txData{} + // There are no confirmed transactions so // the pending channel cannot be timed out timeout := channel.isTimedOut() require.False(t, timeout) - // Manually set a confirmed transactions - // To avoid other methods clearing state - channel.confirmedTransactions[zeroFrameTxID(0).String()] = eth.BlockID{Number: 0} - channel.confirmedTransactions[zeroFrameTxID(1).String()] = eth.BlockID{Number: 99} - channel.confirmedTxUpdated = true + // Manually confirm transactions + channel.TxConfirmed(zeroFrameTxID(0).String(), eth.BlockID{Number: 0}) + channel.TxConfirmed(zeroFrameTxID(1).String(), eth.BlockID{Number: 99}) // Since the ChannelTimeout is 100, the // pending channel should not be timed out @@ -71,10 +74,7 @@ func TestChannelTimeout(t *testing.T) { // Add a confirmed transaction with a higher number // than the ChannelTimeout - channel.confirmedTransactions[zeroFrameTxID(2).String()] = eth.BlockID{ - Number: 101, - } - channel.confirmedTxUpdated = true + channel.TxConfirmed(zeroFrameTxID(2).String(), eth.BlockID{Number: 101}) // Now the pending channel should be timed out timeout = channel.isTimedOut() From d41e588681d1d0ee61c962f4d89b25ffe2084065 Mon Sep 17 00:00:00 2001 From: Adrian Sutton Date: Tue, 15 Oct 2024 06:26:53 +1000 Subject: [PATCH 22/31] op-challenger: Support running multiple prestates the same game type in run-trace (#12443) --- op-challenger/cmd/run_trace.go | 97 ++++++++++++++--------- op-challenger/cmd/run_trace_test.go | 35 +++++++++ op-challenger/runner/factory.go | 22 ------ op-challenger/runner/runner.go | 118 +++++++++++----------------- 4 files changed, 141 insertions(+), 131 deletions(-) create mode 100644 op-challenger/cmd/run_trace_test.go diff --git a/op-challenger/cmd/run_trace.go b/op-challenger/cmd/run_trace.go index c1d2261230f9..7c52d0de13b1 100644 --- a/op-challenger/cmd/run_trace.go +++ b/op-challenger/cmd/run_trace.go @@ -4,11 +4,11 @@ import ( "context" "errors" "fmt" - "net/url" + "slices" + "strings" - "github.com/ethereum-optimism/optimism/op-challenger/config" "github.com/ethereum-optimism/optimism/op-challenger/flags" - "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/vm" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" "github.com/ethereum-optimism/optimism/op-challenger/runner" opservice "github.com/ethereum-optimism/optimism/op-service" "github.com/ethereum-optimism/optimism/op-service/cliapp" @@ -16,8 +16,12 @@ import ( "github.com/urfave/cli/v2" ) -func RunTrace(ctx *cli.Context, _ context.CancelCauseFunc) (cliapp.Lifecycle, error) { +var ( + ErrUnknownTraceType = errors.New("unknown trace type") + ErrInvalidPrestateHash = errors.New("invalid prestate hash") +) +func RunTrace(ctx *cli.Context, _ context.CancelCauseFunc) (cliapp.Lifecycle, error) { logger, err := setupLogging(ctx) if err != nil { return nil, err @@ -31,36 +35,21 @@ func RunTrace(ctx *cli.Context, _ context.CancelCauseFunc) (cliapp.Lifecycle, er if err := cfg.Check(); err != nil { return nil, err } - if err := checkMTCannonFlags(ctx, cfg); err != nil { + runConfigs, err := parseRunArgs(ctx.StringSlice(RunTraceRunFlag.Name)) + if err != nil { return nil, err } - - var mtPrestate common.Hash - var mtPrestateURL *url.URL - if ctx.IsSet(addMTCannonPrestateFlag.Name) { - mtPrestate = common.HexToHash(ctx.String(addMTCannonPrestateFlag.Name)) - mtPrestateURL, err = url.Parse(ctx.String(addMTCannonPrestateURLFlag.Name)) - if err != nil { - return nil, fmt.Errorf("invalid mt-cannon prestate url (%v): %w", ctx.String(addMTCannonPrestateFlag.Name), err) + if len(runConfigs) == 0 { + // Default to running on-chain version of each enabled trace type + for _, traceType := range cfg.TraceTypes { + runConfigs = append(runConfigs, runner.RunConfig{TraceType: traceType}) } } - return runner.NewRunner(logger, cfg, mtPrestate, mtPrestateURL), nil -} - -func checkMTCannonFlags(ctx *cli.Context, cfg *config.Config) error { - if ctx.IsSet(addMTCannonPrestateFlag.Name) || ctx.IsSet(addMTCannonPrestateURLFlag.Name) { - if ctx.IsSet(addMTCannonPrestateFlag.Name) != ctx.IsSet(addMTCannonPrestateURLFlag.Name) { - return fmt.Errorf("both flag %v and %v must be set when running MT-Cannon traces", addMTCannonPrestateURLFlag.Name, addMTCannonPrestateFlag.Name) - } - if cfg.Cannon == (vm.Config{}) { - return errors.New("required Cannon vm configuration for mt-cannon traces is missing") - } - } - return nil + return runner.NewRunner(logger, cfg, runConfigs), nil } func runTraceFlags() []cli.Flag { - return append(flags.Flags, addMTCannonPrestateFlag, addMTCannonPrestateURLFlag) + return append(flags.Flags, RunTraceRunFlag) } var RunTraceCommand = &cli.Command{ @@ -72,14 +61,50 @@ var RunTraceCommand = &cli.Command{ } var ( - addMTCannonPrestateFlag = &cli.StringFlag{ - Name: "add-mt-cannon-prestate", - Usage: "Use this prestate to run MT-Cannon compatibility tests", - EnvVars: opservice.PrefixEnvVar(flags.EnvVarPrefix, "ADD_MT_CANNON_PRESTATE"), - } - addMTCannonPrestateURLFlag = &cli.StringFlag{ - Name: "add-mt-cannon-prestate-url", - Usage: "Use this prestate URL to run MT-Cannon compatibility tests", - EnvVars: opservice.PrefixEnvVar(flags.EnvVarPrefix, "ADD_MT_CANNON_PRESTATE_URL"), + RunTraceRunFlag = &cli.StringSliceFlag{ + Name: "run", + Usage: "Specify a trace to run. Format is traceType/name/prestateHash where " + + "traceType is the trace type to use with the prestate (e.g cannon or asterisc-kona), " + + "name is an arbitrary name for the prestate to use when reporting metrics and" + + "prestateHash is the hex encoded absolute prestate commitment to use. " + + "If name is omitted the trace type name is used." + + "If the prestateHash is omitted, the absolute prestate hash used for new games on-chain.", + EnvVars: opservice.PrefixEnvVar(flags.EnvVarPrefix, "RUN"), } ) + +func parseRunArgs(args []string) ([]runner.RunConfig, error) { + cfgs := make([]runner.RunConfig, len(args)) + for i, arg := range args { + cfg, err := parseRunArg(arg) + if err != nil { + return nil, err + } + cfgs[i] = cfg + } + return cfgs, nil +} + +func parseRunArg(arg string) (runner.RunConfig, error) { + cfg := runner.RunConfig{} + opts := strings.SplitN(arg, "/", 3) + if len(opts) == 0 { + return runner.RunConfig{}, fmt.Errorf("invalid run config %q", arg) + } + cfg.TraceType = types.TraceType(opts[0]) + if !slices.Contains(types.TraceTypes, cfg.TraceType) { + return runner.RunConfig{}, fmt.Errorf("%w %q for run config %q", ErrUnknownTraceType, opts[0], arg) + } + if len(opts) > 1 { + cfg.Name = opts[1] + } else { + cfg.Name = cfg.TraceType.String() + } + if len(opts) > 2 { + cfg.Prestate = common.HexToHash(opts[2]) + if cfg.Prestate == (common.Hash{}) { + return runner.RunConfig{}, fmt.Errorf("%w %q for run config %q", ErrInvalidPrestateHash, opts[2], arg) + } + } + return cfg, nil +} diff --git a/op-challenger/cmd/run_trace_test.go b/op-challenger/cmd/run_trace_test.go new file mode 100644 index 000000000000..78e047827dd2 --- /dev/null +++ b/op-challenger/cmd/run_trace_test.go @@ -0,0 +1,35 @@ +package main + +import ( + "strings" + "testing" + + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" + "github.com/ethereum-optimism/optimism/op-challenger/runner" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +func TestParseRunArg(t *testing.T) { + tests := []struct { + arg string + expected runner.RunConfig + err error + }{ + {arg: "unknown/test1/0x1234", err: ErrUnknownTraceType}, + {arg: "cannon", expected: runner.RunConfig{TraceType: types.TraceTypeCannon, Name: types.TraceTypeCannon.String()}}, + {arg: "asterisc", expected: runner.RunConfig{TraceType: types.TraceTypeAsterisc, Name: types.TraceTypeAsterisc.String()}}, + {arg: "cannon/test1", expected: runner.RunConfig{TraceType: types.TraceTypeCannon, Name: "test1"}}, + {arg: "cannon/test1/0x1234", expected: runner.RunConfig{TraceType: types.TraceTypeCannon, Name: "test1", Prestate: common.HexToHash("0x1234")}}, + {arg: "cannon/test1/invalid", err: ErrInvalidPrestateHash}, + } + for _, test := range tests { + test := test + // Slash characters in test names confuse some things that parse the output as it looks like a subtest + t.Run(strings.ReplaceAll(test.arg, "/", "_"), func(t *testing.T) { + actual, err := parseRunArg(test.arg) + require.ErrorIs(t, err, test.err) + require.Equal(t, test.expected, actual) + }) + } +} diff --git a/op-challenger/runner/factory.go b/op-challenger/runner/factory.go index d6bfbb0f5be8..8a9929bd0a40 100644 --- a/op-challenger/runner/factory.go +++ b/op-challenger/runner/factory.go @@ -60,28 +60,6 @@ func createTraceProvider( return nil, errors.New("invalid trace type") } -func createMTTraceProvider( - ctx context.Context, - logger log.Logger, - m vm.Metricer, - vmConfig vm.Config, - prestateHash common.Hash, - absolutePrestateBaseURL *url.URL, - localInputs utils.LocalGameInputs, - dir string, -) (types.TraceProvider, error) { - executor := vm.NewOpProgramServerExecutor(logger) - stateConverter := cannon.NewStateConverter(vmConfig) - - prestateSource := prestates.NewMultiPrestateProvider(absolutePrestateBaseURL, filepath.Join(dir, "prestates"), stateConverter) - prestatePath, err := prestateSource.PrestatePath(ctx, prestateHash) - if err != nil { - return nil, fmt.Errorf("failed to get prestate %v: %w", prestateHash, err) - } - prestateProvider := vm.NewPrestateProvider(prestatePath, stateConverter) - return cannon.NewTraceProvider(logger, m, vmConfig, executor, prestateProvider, prestatePath, localInputs, dir, 42), nil -} - func getPrestate(ctx context.Context, prestateHash common.Hash, prestateBaseUrl *url.URL, prestatePath string, dataDir string, stateConverter vm.StateConverter) (string, error) { prestateSource := prestates.NewPrestateSource( prestateBaseUrl, diff --git a/op-challenger/runner/runner.go b/op-challenger/runner/runner.go index 8e12d6ae0553..289bb0cfe105 100644 --- a/op-challenger/runner/runner.go +++ b/op-challenger/runner/runner.go @@ -5,9 +5,9 @@ import ( "errors" "fmt" "math/big" - "net/url" "os" "path/filepath" + "regexp" "sync" "sync/atomic" "time" @@ -29,8 +29,6 @@ import ( "github.com/ethereum/go-ethereum/log" ) -const mtCannonType = "mt-cannon" - var ( ErrUnexpectedStatusCode = errors.New("unexpected status code") ) @@ -45,12 +43,17 @@ type Metricer interface { RecordSuccess(vmType string) } +type RunConfig struct { + TraceType types.TraceType + Name string + Prestate common.Hash +} + type Runner struct { - log log.Logger - cfg *config.Config - addMTCannonPrestate common.Hash - addMTCannonPrestateURL *url.URL - m Metricer + log log.Logger + cfg *config.Config + runConfigs []RunConfig + m Metricer running atomic.Bool ctx context.Context @@ -59,13 +62,12 @@ type Runner struct { metricsSrv *httputil.HTTPServer } -func NewRunner(logger log.Logger, cfg *config.Config, mtCannonPrestate common.Hash, mtCannonPrestateURL *url.URL) *Runner { +func NewRunner(logger log.Logger, cfg *config.Config, runConfigs []RunConfig) *Runner { return &Runner{ - log: logger, - cfg: cfg, - addMTCannonPrestate: mtCannonPrestate, - addMTCannonPrestateURL: mtCannonPrestateURL, - m: NewMetrics(), + log: logger, + cfg: cfg, + runConfigs: runConfigs, + m: NewMetrics(), } } @@ -91,21 +93,21 @@ func (r *Runner) Start(ctx context.Context) error { } caller := batching.NewMultiCaller(l1Client, batching.DefaultBatchSize) - for _, traceType := range r.cfg.TraceTypes { + for _, runConfig := range r.runConfigs { r.wg.Add(1) - go r.loop(ctx, traceType, rollupClient, caller) + go r.loop(ctx, runConfig, rollupClient, caller) } - r.log.Info("Runners started") + r.log.Info("Runners started", "num", len(r.runConfigs)) return nil } -func (r *Runner) loop(ctx context.Context, traceType types.TraceType, client *sources.RollupClient, caller *batching.MultiCaller) { +func (r *Runner) loop(ctx context.Context, runConfig RunConfig, client *sources.RollupClient, caller *batching.MultiCaller) { defer r.wg.Done() t := time.NewTicker(1 * time.Minute) defer t.Stop() for { - r.runAndRecordOnce(ctx, traceType, client, caller) + r.runAndRecordOnce(ctx, runConfig, client, caller) select { case <-t.C: case <-ctx.Done(): @@ -114,80 +116,50 @@ func (r *Runner) loop(ctx context.Context, traceType types.TraceType, client *so } } -func (r *Runner) runAndRecordOnce(ctx context.Context, traceType types.TraceType, client *sources.RollupClient, caller *batching.MultiCaller) { +func (r *Runner) runAndRecordOnce(ctx context.Context, runConfig RunConfig, client *sources.RollupClient, caller *batching.MultiCaller) { recordError := func(err error, traceType string, m Metricer, log log.Logger) { if errors.Is(err, ErrUnexpectedStatusCode) { - log.Error("Incorrect status code", "type", traceType, "err", err) + log.Error("Incorrect status code", "type", runConfig.Name, "err", err) m.RecordInvalid(traceType) } else if err != nil { - log.Error("Failed to run", "type", traceType, "err", err) + log.Error("Failed to run", "type", runConfig.Name, "err", err) m.RecordFailure(traceType) } else { - log.Info("Successfully verified output root", "type", traceType) + log.Info("Successfully verified output root", "type", runConfig.Name) m.RecordSuccess(traceType) } } - prestateHash, err := r.getPrestateHash(ctx, traceType, caller) - if err != nil { - recordError(err, traceType.String(), r.m, r.log) - return + prestateHash := runConfig.Prestate + if prestateHash == (common.Hash{}) { + hash, err := r.getPrestateHash(ctx, runConfig.TraceType, caller) + if err != nil { + recordError(err, runConfig.Name, r.m, r.log) + return + } + prestateHash = hash } localInputs, err := r.createGameInputs(ctx, client) if err != nil { - recordError(err, traceType.String(), r.m, r.log) + recordError(err, runConfig.Name, r.m, r.log) return } inputsLogger := r.log.New("l1", localInputs.L1Head, "l2", localInputs.L2Head, "l2Block", localInputs.L2BlockNumber, "claim", localInputs.L2Claim) - var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() - dir, err := r.prepDatadir(traceType.String()) - if err != nil { - recordError(err, traceType.String(), r.m, r.log) - return - } - err = r.runOnce(ctx, inputsLogger.With("type", traceType), traceType, prestateHash, localInputs, dir) - recordError(err, traceType.String(), r.m, r.log) - }() - - if traceType == types.TraceTypeCannon && r.addMTCannonPrestate != (common.Hash{}) && r.addMTCannonPrestateURL != nil { - wg.Add(1) - go func() { - defer wg.Done() - dir, err := r.prepDatadir(mtCannonType) - if err != nil { - recordError(err, mtCannonType, r.m, r.log) - return - } - logger := inputsLogger.With("type", mtCannonType) - err = r.runMTOnce(ctx, logger, localInputs, dir) - recordError(err, mtCannonType, r.m, r.log.With(mtCannonType, true)) - }() - } - wg.Wait() -} - -func (r *Runner) runOnce(ctx context.Context, logger log.Logger, traceType types.TraceType, prestateHash common.Hash, localInputs utils.LocalGameInputs, dir string) error { - provider, err := createTraceProvider(ctx, logger, metrics.NewVmMetrics(r.m, traceType.String()), r.cfg, prestateHash, traceType, localInputs, dir) - if err != nil { - return fmt.Errorf("failed to create trace provider: %w", err) - } - hash, err := provider.Get(ctx, types.RootPosition) + // Sanitize the directory name. + safeName := regexp.MustCompile("[^a-zA-Z0-9_-]").ReplaceAllString(runConfig.Name, "") + dir, err := r.prepDatadir(safeName) if err != nil { - return fmt.Errorf("failed to execute trace provider: %w", err) - } - if hash[0] != mipsevm.VMStatusValid { - return fmt.Errorf("%w: %v", ErrUnexpectedStatusCode, hash) + recordError(err, runConfig.Name, r.m, r.log) + return } - return nil + err = r.runOnce(ctx, inputsLogger.With("type", runConfig.Name), runConfig.Name, runConfig.TraceType, prestateHash, localInputs, dir) + recordError(err, runConfig.Name, r.m, r.log) } -func (r *Runner) runMTOnce(ctx context.Context, logger log.Logger, localInputs utils.LocalGameInputs, dir string) error { - provider, err := createMTTraceProvider(ctx, logger, metrics.NewVmMetrics(r.m, mtCannonType), r.cfg.Cannon, r.addMTCannonPrestate, r.addMTCannonPrestateURL, localInputs, dir) +func (r *Runner) runOnce(ctx context.Context, logger log.Logger, name string, traceType types.TraceType, prestateHash common.Hash, localInputs utils.LocalGameInputs, dir string) error { + provider, err := createTraceProvider(ctx, logger, metrics.NewVmMetrics(r.m, name), r.cfg, prestateHash, traceType, localInputs, dir) if err != nil { return fmt.Errorf("failed to create trace provider: %w", err) } @@ -201,8 +173,8 @@ func (r *Runner) runMTOnce(ctx context.Context, logger log.Logger, localInputs u return nil } -func (r *Runner) prepDatadir(traceType string) (string, error) { - dir := filepath.Join(r.cfg.Datadir, traceType) +func (r *Runner) prepDatadir(name string) (string, error) { + dir := filepath.Join(r.cfg.Datadir, name) if err := os.RemoveAll(dir); err != nil { return "", fmt.Errorf("failed to remove old dir: %w", err) } From e7085e537b4a0c95d41b048cfcfdd7ad24808337 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 13:42:33 -0700 Subject: [PATCH 23/31] dependabot(gomod): bump github.com/minio/minio-go/v7 (#12446) Bumps [github.com/minio/minio-go/v7](https://github.com/minio/minio-go) from 7.0.77 to 7.0.78. - [Release notes](https://github.com/minio/minio-go/releases) - [Commits](https://github.com/minio/minio-go/compare/v7.0.77...v7.0.78) --- updated-dependencies: - dependency-name: github.com/minio/minio-go/v7 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 | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 24b80b2fe71f..02fb7ad85610 100644 --- a/go.mod +++ b/go.mod @@ -27,14 +27,14 @@ require ( github.com/holiman/uint256 v1.3.1 github.com/ipfs/go-datastore v0.6.0 github.com/ipfs/go-ds-leveldb v0.5.0 - github.com/klauspost/compress v1.17.9 + github.com/klauspost/compress v1.17.11 github.com/kurtosis-tech/kurtosis/api/golang v1.3.0 github.com/libp2p/go-libp2p v0.36.2 github.com/libp2p/go-libp2p-mplex v0.9.0 github.com/libp2p/go-libp2p-pubsub v0.12.0 github.com/libp2p/go-libp2p-testing v0.12.0 github.com/mattn/go-isatty v0.0.20 - github.com/minio/minio-go/v7 v7.0.77 + github.com/minio/minio-go/v7 v7.0.78 github.com/multiformats/go-base32 v0.1.0 github.com/multiformats/go-multiaddr v0.13.0 github.com/multiformats/go-multiaddr-dns v0.4.0 @@ -235,7 +235,7 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/mod v0.20.0 // indirect - golang.org/x/net v0.28.0 // indirect + golang.org/x/net v0.30.0 // indirect golang.org/x/sys v0.26.0 // indirect golang.org/x/text v0.19.0 // indirect golang.org/x/tools v0.24.0 // indirect diff --git a/go.sum b/go.sum index 3b723b70affb..90d22916553a 100644 --- a/go.sum +++ b/go.sum @@ -404,8 +404,8 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= -github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= @@ -509,8 +509,8 @@ github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc/go.mod h1:cGKTAVKx4S github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= -github.com/minio/minio-go/v7 v7.0.77 h1:GaGghJRg9nwDVlNbwYjSDJT1rqltQkBFDsypWX1v3Bw= -github.com/minio/minio-go/v7 v7.0.77/go.mod h1:AVM3IUN6WwKzmwBxVdjzhH8xq+f57JSbbvzqvUzR6eg= +github.com/minio/minio-go/v7 v7.0.78 h1:LqW2zy52fxnI4gg8C2oZviTaKHcBV36scS+RzJnxUFs= +github.com/minio/minio-go/v7 v7.0.78/go.mod h1:84gmIilaX4zcvAWWzJ5Z1WI5axN+hAbM5w25xf8xvC0= github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= @@ -894,8 +894,8 @@ golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= From 6f41bac618826b7669ba584876586fb17ac5e9b0 Mon Sep 17 00:00:00 2001 From: Adrian Sutton Date: Tue, 15 Oct 2024 13:44:53 +1000 Subject: [PATCH 24/31] proofs-tools: Update challenger to include new vm runner options. (#12452) --- docker-bake.hcl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-bake.hcl b/docker-bake.hcl index 5740590a95f2..5ba3d6ed24f9 100644 --- a/docker-bake.hcl +++ b/docker-bake.hcl @@ -206,7 +206,7 @@ target "proofs-tools" { dockerfile = "./ops/docker/proofs-tools/Dockerfile" context = "." args = { - CHALLENGER_VERSION="v1.1.2-rc.1" + CHALLENGER_VERSION="e7085e537b4a0c95d41b048cfcfdd7ad24808337" KONA_VERSION="kona-client-v0.1.0-alpha.3" ASTERISC_VERSION="v1.0.2" } From ed4a80c46c20c43cd2781ac62643d0ba2f319e8d Mon Sep 17 00:00:00 2001 From: Matthew Slipper Date: Mon, 14 Oct 2024 23:28:53 -0600 Subject: [PATCH 25/31] ci: Port some CI jobs to self-hosted runners (#12199) * ci: Port some CI jobs to self-hosted runners This PR ports the following CI jobs to use self-hosted runners: - cannon-go-lint-and-test - contracts-bedrock-build - go-lint - go-test-kurtosis - go-e2e-test - cannon-prestate These jobs benefit from running on beefier hardware. Since Go cache data is shared among the executors, it also allowed me to remove a lot of the manual caching logic from our builds while improving Go compilation times overall. I'll provide a separate writeup detailing exactly how the executors work. * use test result directories in the workspace --- .circleci/config.yml | 206 +++++------------- .../deployer/integration_test/apply_test.go | 4 + op-e2e/Makefile | 4 +- 3 files changed, 56 insertions(+), 158 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index b15c6bec8452..9ea46831a800 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -152,8 +152,8 @@ commands: jobs: cannon-go-lint-and-test: - docker: - - image: <> + machine: true + resource_class: ethereum-optimism/latitude-1 parameters: skip_slow_tests: type: boolean @@ -165,19 +165,17 @@ jobs: mips64: type: boolean default: false - resource_class: xlarge steps: - checkout - check-changed: patterns: cannon,packages/contracts-bedrock/src/cannon,op-preimage,go.mod - attach_workspace: at: "." - - restore_cache: - name: Restore Go modules cache - key: gomod-{{ checksum "go.sum" }} - run: name: prep Cannon results dir - command: mkdir -p /tmp/test-results + command: | + mkdir -p ./tmp/test-results + mkdir -p ./tmp/testlogs - run: name: build Cannon example binaries command: make elf # only compile ELF binaries with Go, we do not have MIPS GCC for creating the debug-dumps. @@ -195,9 +193,8 @@ jobs: name: Cannon Go 32-bit tests command: | export SKIP_SLOW_TESTS=<> - mkdir -p /testlogs - gotestsum --format=testname --junitfile=/tmp/test-results/cannon.xml --jsonfile=/testlogs/log.json \ - -- -parallel=8 -coverpkg=github.com/ethereum-optimism/optimism/cannon/... -coverprofile=coverage.out ./... + gotestsum --format=testname --junitfile=../tmp/test-results/cannon.xml --jsonfile=../tmp/testlogs/log.json \ + -- -parallel=$(nproc) -coverpkg=github.com/ethereum-optimism/optimism/cannon/... -coverprofile=coverage.out ./... working_directory: cannon - when: condition: <> @@ -206,17 +203,16 @@ jobs: name: Cannon Go 64-bit tests command: | export SKIP_SLOW_TESTS=<> - mkdir -p /testlogs - gotestsum --format=testname --junitfile=/tmp/test-results/cannon.xml --jsonfile=/testlogs/log.json \ - -- --tags=cannon64 -parallel=8 -coverpkg=github.com/ethereum-optimism/optimism/cannon/... -coverprofile=coverage.out ./... + gotestsum --format=testname --junitfile=../tmp/test-results/cannon.xml --jsonfile=../tmp/testlogs/log.json \ + -- --tags=cannon64 -parallel=$(nproc) -coverpkg=github.com/ethereum-optimism/optimism/cannon/... -coverprofile=coverage.out ./... working_directory: cannon - run: name: upload Cannon coverage command: codecov --verbose --clean --flags cannon-go-tests - store_test_results: - path: /tmp/test-results + path: ./tmp/test-results - store_artifacts: - path: /testlogs + path: ./tmp/testlogs when: always - when: condition: <> @@ -238,9 +234,8 @@ jobs: working_directory: cannon/mipsevm/tests/open_mips_tests contracts-bedrock-build: - docker: - - image: <> - resource_class: xlarge + machine: true + resource_class: ethereum-optimism/latitude-1 parameters: skip_pattern: description: Glob pattern of tests to skip @@ -249,16 +244,6 @@ jobs: steps: - checkout - install-contracts-dependencies - - restore_cache: - name: Restore Go modules cache - keys: - - gomod-contracts-build-{{ checksum "go.sum" }} - - gomod-contracts-build- - - restore_cache: - name: Restore Go build cache - keys: - - golang-build-cache-contracts-build-{{ checksum "go.sum" }} - - golang-build-cache-contracts-build- - run: name: Print forge version command: forge --version @@ -276,16 +261,6 @@ jobs: name: Generate allocs command: | make devnet-allocs - - save_cache: - name: Save Go modules cache - key: gomod-contracts-build-{{ checksum "go.sum" }} - paths: - - "/go/pkg/mod" - - save_cache: - name: Save Go build cache - key: golang-build-cache-contracts-build-{{ checksum "go.sum" }} - paths: - - "/root/.cache/go-build" - persist_to_workspace: root: "." paths: @@ -798,40 +773,15 @@ jobs: - "/root/.cache/go-build" go-lint: - docker: - - image: <> + machine: true + resource_class: ethereum-optimism/latitude-1 steps: - checkout - - restore_cache: - name: Restore Go modules cache - key: gomod-{{ checksum "go.sum" }} - - restore_cache: - name: Restore Go build cache - keys: - - golang-build-cache-lint-{{ checksum "go.sum" }} - - golang-build-cache-lint- - - restore_cache: - name: Restore Go lint cache - keys: - - golang-lint-cache-{{ checksum "go.sum" }} - - golang-lint-cache- - run: name: run Go linter command: | - # Identify how many cores it defaults to - golangci-lint run --help | grep concurrency make lint-go working_directory: . - - save_cache: - name: Save Go build cache - key: golang-build-cache-lint-{{ checksum "go.sum" }} - paths: - - "/root/.cache/go-build" - - save_cache: - name: Save Go lint cache - key: golang-lint-cache-{{ checksum "go.sum" }} - paths: - - "/root/.cache/golangci-lint" go-test-kurtosis: parameters: @@ -846,22 +796,9 @@ jobs: description: Test directory type: string default: "./..." - machine: - image: <> # only used to enable codecov. - resource_class: xlarge + machine: true + resource_class: ethereum-optimism/latitude-1 steps: - - run: - name: Install components - command: | - go version - go install gotest.tools/gotestsum@v1.11.0 - - run: - name: Install Kurtosis - command: | - echo "deb [trusted=yes] https://apt.fury.io/kurtosis-tech/ /" | sudo tee /etc/apt/sources.list.d/kurtosis.list - sudo apt update - sudo apt install kurtosis-cli=1.3.0 - kurtosis engine start - checkout - when: condition: <> @@ -871,24 +808,23 @@ jobs: name: prep results dir command: | # Make sure the workspace is properly owned - sudo chown -R $USER:$USER . - mkdir -p /tmp/test-results - mkdir -p /tmp/testlogs + mkdir -p ./tmp/test-results + mkdir -p ./tmp/testlogs - run: name: run tests command: | ENABLE_KURTOSIS=true gotestsum \ --format=testname \ - --junitfile=/tmp/test-results/<>.xml \ - --jsonfile=/tmp/testlogs/log.json \ - -- -parallel=8 \ + --junitfile=../tmp/test-results/<>.xml \ + --jsonfile=../tmp/testlogs/log.json \ + -- -parallel=$(nproc) \ -coverpkg=github.com/ethereum-optimism/optimism/... \ -coverprofile=coverage.out <> working_directory: <> - store_test_results: - path: /tmp/test-results + path: tmp/test-results - store_artifacts: - path: /tmp/testlogs + path: tmp/testlogs when: always go-test: @@ -951,10 +887,6 @@ jobs: target: description: The make target to execute type: string - parallelism: - description: Number of parallel test runs - type: integer - default: 6 notify: description: Whether to notify on failure type: boolean @@ -963,35 +895,24 @@ jobs: description: Slack user or group to mention when notifying of failures type: string default: "" - docker: - - image: <> - resource_class: xlarge - parallelism: <> + resource_class: + description: Machine resource class + type: string + default: ethereum-optimism/latitude-1 + machine: true + resource_class: <> steps: - checkout - - check-changed: - patterns: op-(.+),cannon,contracts-bedrock - - run: - name: prep results dir - command: mkdir -p /tmp/test-results - - restore_cache: - name: Restore Go modules cache - key: gomod-{{ checksum "go.sum" }} - - restore_cache: - name: Restore Go build cache - keys: - - golang-build-cache-e2e-{{ checksum "go.sum" }} - - golang-build-cache-e2e- - attach_workspace: - at: /tmp/workspace + at: ./tmp/workspace - run: name: Load devnet-allocs and artifacts command: | mkdir -p .devnet - cp -r /tmp/workspace/.devnet* . - cp -r /tmp/workspace/packages/contracts-bedrock/forge-artifacts packages/contracts-bedrock/forge-artifacts - cp /tmp/workspace/packages/contracts-bedrock/deploy-config/devnetL1.json packages/contracts-bedrock/deploy-config/devnetL1.json - cp -r /tmp/workspace/packages/contracts-bedrock/deployments/devnetL1 packages/contracts-bedrock/deployments/devnetL1 + cp -r ./tmp/workspace/.devnet* . + cp -r ./tmp/workspace/packages/contracts-bedrock/forge-artifacts packages/contracts-bedrock/forge-artifacts + cp ./tmp/workspace/packages/contracts-bedrock/deploy-config/devnetL1.json packages/contracts-bedrock/deploy-config/devnetL1.json + cp -r ./tmp/workspace/packages/contracts-bedrock/deployments/devnetL1 packages/contracts-bedrock/deployments/devnetL1 - run: name: print go's available MIPS targets command: go tool dist list | grep mips @@ -999,7 +920,8 @@ jobs: name: run tests no_output_timeout: 20m command: | - mkdir -p /testlogs + mkdir -p ./tmp/testlogs + mkdir -p ./tmp/test-results # The below env var gets overridden when running make test-cannon, but we # need to explicitly set it here to prevent Cannon from running when we don't @@ -1007,21 +929,16 @@ jobs: export OP_E2E_CANNON_ENABLED="false" # Note: We don't use circle CI test splits because we need to split by test name, not by package. There is an additional # constraint that gotestsum does not currently (nor likely will) accept files from different packages when building. - JUNIT_FILE=/tmp/test-results/<>_<>.xml JSON_LOG_FILE=/testlogs/test.log make <> + JUNIT_FILE=../tmp/test-results/<>_<>.xml JSON_LOG_FILE=../tmp/testlogs/test.log make <> working_directory: <> - store_artifacts: - path: /testlogs + path: ./tmp/testlogs when: always - store_artifacts: - path: /tmp/test-results + path: ./tmp/test-results when: always - store_test_results: - path: /tmp/test-results - - save_cache: - name: Save Go build cache - key: golang-build-cache-e2e-{{ checksum "go.sum" }} - paths: - - "/root/.cache/go-build" + path: ./tmp/test-results - when: condition: "<>" steps: @@ -1029,18 +946,10 @@ jobs: mentions: "<>" cannon-prestate: - docker: - - image: <> + machine: true + resource_class: ethereum-optimism/latitude-1 steps: - checkout - - restore_cache: - name: Restore Go modules cache - key: gomod-{{ checksum "go.sum" }} - - restore_cache: - name: Restore Go build cache - keys: - - golang-build-cache-cannon-prestate-{{ checksum "go.sum" }} - - golang-build-cache-cannon-prestate- - run: name: Build cannon command: make cannon @@ -1073,11 +982,6 @@ jobs: - "op-program/bin/prestate-mt.json" - "op-program/bin/meta-mt.json" - "op-program/bin/prestate-proof-mt.json" - - save_cache: - name: Save Go build cache - key: golang-build-cache-cannon-prestate-{{ checksum "go.sum" }} - paths: - - "/root/.cache/go-build" - persist_to_workspace: root: . paths: @@ -1384,9 +1288,9 @@ workflows: and: - or: # Trigger on new commits - - equal: [ webhook, << pipeline.trigger_source >> ] - # Trigger on manual triggers if explicitly requested - - equal: [ true, << pipeline.parameters.main_dispatch >> ] + - equal: [ webhook, << pipeline.trigger_source >> ] + # Trigger on manual triggers if explicitly requested + - equal: [ true, << pipeline.parameters.main_dispatch >> ] - not: equal: [ scheduled_pipeline, << pipeline.trigger_source >> ] jobs: @@ -1425,9 +1329,7 @@ workflows: - semgrep-scan: name: semgrep-scan-local scan_command: semgrep scan --timeout=100 --config=./.semgrep --error . - - go-lint: - requires: - - go-mod-download + - go-lint - fuzz-golang: name: fuzz-golang-<> requires: @@ -1488,23 +1390,18 @@ workflows: name: op-e2e-HTTP-tests module: op-e2e target: test-http - parallelism: 8 requires: - - go-mod-download - contracts-bedrock-build - go-e2e-test: name: op-e2e-action-tests module: op-e2e target: test-actions - parallelism: 1 requires: - - go-mod-download - contracts-bedrock-build - go-e2e-test: name: op-e2e-fault-proof-tests module: op-e2e target: test-fault-proofs - parallelism: 4 requires: - contracts-bedrock-build - cannon-prestate @@ -1560,9 +1457,7 @@ workflows: - op-supervisor - op-deployer - cannon - - cannon-prestate: - requires: - - go-mod-download + - cannon-prestate - check-generated-mocks-op-node - check-generated-mocks-op-service - cannon-go-lint-and-test: @@ -1712,9 +1607,7 @@ workflows: equal: [ scheduled_pipeline, << pipeline.trigger_source >> ] jobs: - go-mod-download - - cannon-prestate: - requires: - - go-mod-download + - cannon-prestate - cannon-stf-verify: requires: - go-mod-download @@ -1728,7 +1621,6 @@ workflows: name: op-e2e-cannon-tests module: op-e2e target: test-cannon - parallelism: 8 notify: true mentions: "@proofs-team" requires: diff --git a/op-chain-ops/deployer/integration_test/apply_test.go b/op-chain-ops/deployer/integration_test/apply_test.go index 784637fe5b98..679d248596d6 100644 --- a/op-chain-ops/deployer/integration_test/apply_test.go +++ b/op-chain-ops/deployer/integration_test/apply_test.go @@ -13,6 +13,8 @@ import ( "testing" "time" + op_e2e "github.com/ethereum-optimism/optimism/op-e2e" + "github.com/ethereum-optimism/optimism/op-chain-ops/deployer" "github.com/holiman/uint256" @@ -66,6 +68,7 @@ func (d *deployerKey) String() string { } func TestEndToEndApply(t *testing.T) { + op_e2e.InitParallel(t) kurtosisutil.Test(t) lgr := testlog.Logger(t, slog.LevelDebug) @@ -363,6 +366,7 @@ func TestApplyExistingOPCM(t *testing.T) { } func TestL2BlockTimeOverride(t *testing.T) { + op_e2e.InitParallel(t) kurtosisutil.Test(t) lgr := testlog.Logger(t, slog.LevelDebug) diff --git a/op-e2e/Makefile b/op-e2e/Makefile index f3f67f8050da..22b4edb4bc5d 100644 --- a/op-e2e/Makefile +++ b/op-e2e/Makefile @@ -1,9 +1,11 @@ +num_cores := $(shell nproc) + # Generally, JUNIT_FILE is set in CI but may be specified to an arbitrary file location to emulate CI locally # If JUNIT_FILE is set, JSON_LOG_FILE should also be set ifdef JUNIT_FILE go_test = OP_TESTLOG_DISABLE_COLOR=true OP_E2E_DISABLE_PARALLEL=false gotestsum --format=testname --junitfile=$(JUNIT_FILE) --jsonfile=$(JSON_LOG_FILE) -- -failfast # Note: -parallel must be set to match the number of cores in the resource class - go_test_flags = -timeout=60m -parallel=8 + go_test_flags = -timeout=60m -parallel=$(num_cores) else go_test = go test go_test_flags = -v From f0d7738210228e908036d353b453322539f98f23 Mon Sep 17 00:00:00 2001 From: Michael de Hoog Date: Tue, 15 Oct 2024 02:11:20 -1000 Subject: [PATCH 26/31] Rename channel receiver (#12453) --- op-batcher/batcher/channel.go | 136 +++++++++++++++++----------------- 1 file changed, 68 insertions(+), 68 deletions(-) diff --git a/op-batcher/batcher/channel.go b/op-batcher/batcher/channel.go index 638fa098059e..dd0827d4686c 100644 --- a/op-batcher/batcher/channel.go +++ b/op-batcher/batcher/channel.go @@ -46,53 +46,53 @@ func newChannel(log log.Logger, metr metrics.Metricer, cfg ChannelConfig, rollup // TxFailed records a transaction as failed. It will attempt to resubmit the data // in the failed transaction. -func (s *channel) TxFailed(id string) { - if data, ok := s.pendingTransactions[id]; ok { - s.log.Trace("marked transaction as failed", "id", id) +func (c *channel) TxFailed(id string) { + if data, ok := c.pendingTransactions[id]; ok { + c.log.Trace("marked transaction as failed", "id", id) // Note: when the batcher is changed to send multiple frames per tx, // this needs to be changed to iterate over all frames of the tx data // and re-queue them. - s.channelBuilder.PushFrames(data.Frames()...) - delete(s.pendingTransactions, id) + c.channelBuilder.PushFrames(data.Frames()...) + delete(c.pendingTransactions, id) } else { - s.log.Warn("unknown transaction marked as failed", "id", id) + c.log.Warn("unknown transaction marked as failed", "id", id) } - s.metr.RecordBatchTxFailed() + c.metr.RecordBatchTxFailed() } // TxConfirmed marks a transaction as confirmed on L1. Unfortunately even if all frames in // a channel have been marked as confirmed on L1 the channel may be invalid & need to be // resubmitted. // This function may reset the pending channel if the pending channel has timed out. -func (s *channel) TxConfirmed(id string, inclusionBlock eth.BlockID) (bool, []*types.Block) { - s.metr.RecordBatchTxSubmitted() - s.log.Debug("marked transaction as confirmed", "id", id, "block", inclusionBlock) - if _, ok := s.pendingTransactions[id]; !ok { - s.log.Warn("unknown transaction marked as confirmed", "id", id, "block", inclusionBlock) +func (c *channel) TxConfirmed(id string, inclusionBlock eth.BlockID) (bool, []*types.Block) { + c.metr.RecordBatchTxSubmitted() + c.log.Debug("marked transaction as confirmed", "id", id, "block", inclusionBlock) + if _, ok := c.pendingTransactions[id]; !ok { + c.log.Warn("unknown transaction marked as confirmed", "id", id, "block", inclusionBlock) // TODO: This can occur if we clear the channel while there are still pending transactions // We need to keep track of stale transactions instead return false, nil } - delete(s.pendingTransactions, id) - s.confirmedTransactions[id] = inclusionBlock - s.channelBuilder.FramePublished(inclusionBlock.Number) + delete(c.pendingTransactions, id) + c.confirmedTransactions[id] = inclusionBlock + c.channelBuilder.FramePublished(inclusionBlock.Number) // Update min/max inclusion blocks for timeout check - s.minInclusionBlock = min(s.minInclusionBlock, inclusionBlock.Number) - s.maxInclusionBlock = max(s.maxInclusionBlock, inclusionBlock.Number) + c.minInclusionBlock = min(c.minInclusionBlock, inclusionBlock.Number) + c.maxInclusionBlock = max(c.maxInclusionBlock, inclusionBlock.Number) // If this channel timed out, put the pending blocks back into the local saved blocks // and then reset this state so it can try to build a new channel. - if s.isTimedOut() { - s.metr.RecordChannelTimedOut(s.ID()) - s.log.Warn("Channel timed out", "id", s.ID(), "min_inclusion_block", s.minInclusionBlock, "max_inclusion_block", s.maxInclusionBlock) - return true, s.channelBuilder.Blocks() + if c.isTimedOut() { + c.metr.RecordChannelTimedOut(c.ID()) + c.log.Warn("Channel timed out", "id", c.ID(), "min_inclusion_block", c.minInclusionBlock, "max_inclusion_block", c.maxInclusionBlock) + return true, c.channelBuilder.Blocks() } // If we are done with this channel, record that. - if s.isFullySubmitted() { - s.metr.RecordChannelFullySubmitted(s.ID()) - s.log.Info("Channel is fully submitted", "id", s.ID(), "min_inclusion_block", s.minInclusionBlock, "max_inclusion_block", s.maxInclusionBlock) + if c.isFullySubmitted() { + c.metr.RecordChannelFullySubmitted(c.ID()) + c.log.Info("Channel is fully submitted", "id", c.ID(), "min_inclusion_block", c.minInclusionBlock, "max_inclusion_block", c.maxInclusionBlock) return true, nil } @@ -100,31 +100,31 @@ func (s *channel) TxConfirmed(id string, inclusionBlock eth.BlockID) (bool, []*t } // Timeout returns the channel timeout L1 block number. If there is no timeout set, it returns 0. -func (s *channel) Timeout() uint64 { - return s.channelBuilder.Timeout() +func (c *channel) Timeout() uint64 { + return c.channelBuilder.Timeout() } // isTimedOut returns true if submitted channel has timed out. // A channel has timed out if the difference in L1 Inclusion blocks between // the first & last included block is greater than or equal to the channel timeout. -func (s *channel) isTimedOut() bool { +func (c *channel) isTimedOut() bool { // Prior to the granite hard fork activating, the use of the shorter ChannelTimeout here may cause the batcher // to believe the channel timed out when it was valid. It would then resubmit the blocks needlessly. // This wastes batcher funds but doesn't cause any problems for the chain progressing safe head. - return len(s.confirmedTransactions) > 0 && s.maxInclusionBlock-s.minInclusionBlock >= s.cfg.ChannelTimeout + return len(c.confirmedTransactions) > 0 && c.maxInclusionBlock-c.minInclusionBlock >= c.cfg.ChannelTimeout } // isFullySubmitted returns true if the channel has been fully submitted (all transactions are confirmed). -func (s *channel) isFullySubmitted() bool { - return s.IsFull() && len(s.pendingTransactions)+s.PendingFrames() == 0 +func (c *channel) isFullySubmitted() bool { + return c.IsFull() && len(c.pendingTransactions)+c.PendingFrames() == 0 } -func (s *channel) NoneSubmitted() bool { - return len(s.confirmedTransactions) == 0 && len(s.pendingTransactions) == 0 +func (c *channel) NoneSubmitted() bool { + return len(c.confirmedTransactions) == 0 && len(c.pendingTransactions) == 0 } -func (s *channel) ID() derive.ChannelID { - return s.channelBuilder.ID() +func (c *channel) ID() derive.ChannelID { + return c.channelBuilder.ID() } // NextTxData dequeues the next frames from the channel and returns them encoded in a tx data packet. @@ -133,68 +133,68 @@ func (s *channel) ID() derive.ChannelID { // until it either doesn't have more frames or the target number of frames is reached. // // NextTxData should only be called after HasTxData returned true. -func (s *channel) NextTxData() txData { - nf := s.cfg.MaxFramesPerTx() - txdata := txData{frames: make([]frameData, 0, nf), asBlob: s.cfg.UseBlobs} - for i := 0; i < nf && s.channelBuilder.HasFrame(); i++ { - frame := s.channelBuilder.NextFrame() +func (c *channel) NextTxData() txData { + nf := c.cfg.MaxFramesPerTx() + txdata := txData{frames: make([]frameData, 0, nf), asBlob: c.cfg.UseBlobs} + for i := 0; i < nf && c.channelBuilder.HasFrame(); i++ { + frame := c.channelBuilder.NextFrame() txdata.frames = append(txdata.frames, frame) } id := txdata.ID().String() - s.log.Debug("returning next tx data", "id", id, "num_frames", len(txdata.frames), "as_blob", txdata.asBlob) - s.pendingTransactions[id] = txdata + c.log.Debug("returning next tx data", "id", id, "num_frames", len(txdata.frames), "as_blob", txdata.asBlob) + c.pendingTransactions[id] = txdata return txdata } -func (s *channel) HasTxData() bool { - if s.IsFull() || // If the channel is full, we should start to submit it - !s.cfg.UseBlobs { // If using calldata, we only send one frame per tx - return s.channelBuilder.HasFrame() +func (c *channel) HasTxData() bool { + if c.IsFull() || // If the channel is full, we should start to submit it + !c.cfg.UseBlobs { // If using calldata, we only send one frame per tx + return c.channelBuilder.HasFrame() } // Collect enough frames if channel is not full yet - return s.channelBuilder.PendingFrames() >= int(s.cfg.MaxFramesPerTx()) + return c.channelBuilder.PendingFrames() >= int(c.cfg.MaxFramesPerTx()) } -func (s *channel) IsFull() bool { - return s.channelBuilder.IsFull() +func (c *channel) IsFull() bool { + return c.channelBuilder.IsFull() } -func (s *channel) FullErr() error { - return s.channelBuilder.FullErr() +func (c *channel) FullErr() error { + return c.channelBuilder.FullErr() } -func (s *channel) CheckTimeout(l1BlockNum uint64) { - s.channelBuilder.CheckTimeout(l1BlockNum) +func (c *channel) CheckTimeout(l1BlockNum uint64) { + c.channelBuilder.CheckTimeout(l1BlockNum) } -func (s *channel) AddBlock(block *types.Block) (*derive.L1BlockInfo, error) { - return s.channelBuilder.AddBlock(block) +func (c *channel) AddBlock(block *types.Block) (*derive.L1BlockInfo, error) { + return c.channelBuilder.AddBlock(block) } -func (s *channel) InputBytes() int { - return s.channelBuilder.InputBytes() +func (c *channel) InputBytes() int { + return c.channelBuilder.InputBytes() } -func (s *channel) ReadyBytes() int { - return s.channelBuilder.ReadyBytes() +func (c *channel) ReadyBytes() int { + return c.channelBuilder.ReadyBytes() } -func (s *channel) OutputBytes() int { - return s.channelBuilder.OutputBytes() +func (c *channel) OutputBytes() int { + return c.channelBuilder.OutputBytes() } -func (s *channel) TotalFrames() int { - return s.channelBuilder.TotalFrames() +func (c *channel) TotalFrames() int { + return c.channelBuilder.TotalFrames() } -func (s *channel) PendingFrames() int { - return s.channelBuilder.PendingFrames() +func (c *channel) PendingFrames() int { + return c.channelBuilder.PendingFrames() } -func (s *channel) OutputFrames() error { - return s.channelBuilder.OutputFrames() +func (c *channel) OutputFrames() error { + return c.channelBuilder.OutputFrames() } // LatestL1Origin returns the latest L1 block origin from all the L2 blocks that have been added to the channel @@ -217,6 +217,6 @@ func (c *channel) OldestL2() eth.BlockID { return c.channelBuilder.OldestL2() } -func (s *channel) Close() { - s.channelBuilder.Close() +func (c *channel) Close() { + c.channelBuilder.Close() } From 6ae28f56188c86b0ab90d9cfa9ec09fe84cdee9f Mon Sep 17 00:00:00 2001 From: Sebastian Stammler Date: Tue, 15 Oct 2024 16:44:08 +0200 Subject: [PATCH 27/31] op-node/rollup/derive: Add Holocene Channel Stage (#12334) This only adds the new stage, but doesn't wire it into the derivation pipeline yet. --- .../batch_decoder/reassemble/reassemble.go | 2 +- op-node/rollup/derive/channel.go | 35 ++-- op-node/rollup/derive/channel_assembler.go | 133 ++++++++++++++ .../rollup/derive/channel_assembler_test.go | 169 ++++++++++++++++++ op-node/rollup/derive/channel_bank.go | 2 +- op-node/rollup/derive/channel_bank_test.go | 13 +- op-node/rollup/derive/channel_out_test.go | 2 +- op-node/rollup/derive/channel_test.go | 37 +++- op-node/rollup/test/chain_spec.go | 18 ++ 9 files changed, 389 insertions(+), 22 deletions(-) create mode 100644 op-node/rollup/derive/channel_assembler.go create mode 100644 op-node/rollup/derive/channel_assembler_test.go create mode 100644 op-node/rollup/test/chain_spec.go diff --git a/op-node/cmd/batch_decoder/reassemble/reassemble.go b/op-node/cmd/batch_decoder/reassemble/reassemble.go index d8fc136b5043..08c32bf71447 100644 --- a/op-node/cmd/batch_decoder/reassemble/reassemble.go +++ b/op-node/cmd/batch_decoder/reassemble/reassemble.go @@ -94,7 +94,7 @@ func writeChannel(ch ChannelWithMetadata, filename string) error { // from the channel. Returns a ChannelWithMetadata struct containing all the relevant data. func ProcessFrames(cfg Config, rollupCfg *rollup.Config, id derive.ChannelID, frames []FrameWithMetadata) ChannelWithMetadata { spec := rollup.NewChainSpec(rollupCfg) - ch := derive.NewChannel(id, eth.L1BlockRef{Number: frames[0].InclusionBlock}) + ch := derive.NewChannel(id, eth.L1BlockRef{Number: frames[0].InclusionBlock}, rollupCfg.IsHolocene(frames[0].Timestamp)) invalidFrame := false for _, frame := range frames { diff --git a/op-node/rollup/derive/channel.go b/op-node/rollup/derive/channel.go index 48a9f585c4aa..eec618b97e56 100644 --- a/op-node/rollup/derive/channel.go +++ b/op-node/rollup/derive/channel.go @@ -18,13 +18,15 @@ const ( ) // A Channel is a set of batches that are split into at least one, but possibly multiple frames. -// Frames are allowed to be ingested out of order. +// Frames are allowed to be ingested out of order, unless the channel is set to follow Holocene +// rules. // Each frame is ingested one by one. Once a frame with `closed` is added to the channel, the // channel may mark itself as ready for reading once all intervening frames have been added type Channel struct { // id of the channel - id ChannelID - openBlock eth.L1BlockRef + id ChannelID + openBlock eth.L1BlockRef + requireInOrder bool // estimated memory size, used to drop the channel if we have too much data size uint64 @@ -45,11 +47,14 @@ type Channel struct { highestL1InclusionBlock eth.L1BlockRef } -func NewChannel(id ChannelID, openBlock eth.L1BlockRef) *Channel { +// NewChannel creates a new channel with the given id and openening block. If requireInOrder is +// true, frames must be added in order. +func NewChannel(id ChannelID, openBlock eth.L1BlockRef, requireInOrder bool) *Channel { return &Channel{ - id: id, - inputs: make(map[uint64]Frame), - openBlock: openBlock, + id: id, + inputs: make(map[uint64]Frame), + openBlock: openBlock, + requireInOrder: requireInOrder, } } @@ -70,18 +75,22 @@ func (ch *Channel) AddFrame(frame Frame, l1InclusionBlock eth.L1BlockRef) error if ch.closed && frame.FrameNumber >= ch.endFrameNumber { return fmt.Errorf("frame number (%d) is greater than or equal to end frame number (%d) of a closed channel", frame.FrameNumber, ch.endFrameNumber) } + if ch.requireInOrder && int(frame.FrameNumber) != len(ch.inputs) { + return fmt.Errorf("frame out of order, expected %d, got %d", len(ch.inputs), frame.FrameNumber) + } // Guaranteed to succeed. Now update internal state if frame.IsLast { ch.endFrameNumber = frame.FrameNumber ch.closed = true } - // Prune frames with a number higher than the closing frame number when we receive a closing frame + // Prune frames with a number higher than the closing frame number when we receive a closing frame. + // Note that the following condition is guaranteed to never be true with strict Holocene ordering. if frame.IsLast && ch.endFrameNumber < ch.highestFrameNumber { // Do a linear scan over saved inputs instead of ranging over ID numbers - for id, prunedFrame := range ch.inputs { - if id >= uint64(ch.endFrameNumber) { - delete(ch.inputs, id) + for idx, prunedFrame := range ch.inputs { + if idx >= uint64(ch.endFrameNumber) { + delete(ch.inputs, idx) } ch.size -= frameSize(prunedFrame) } @@ -119,6 +128,10 @@ func (ch *Channel) Size() uint64 { return ch.size } +func (ch *Channel) ID() ChannelID { + return ch.id +} + // IsReady returns true iff the channel is ready to be read. func (ch *Channel) IsReady() bool { // Must see the last frame before the channel is ready to be read diff --git a/op-node/rollup/derive/channel_assembler.go b/op-node/rollup/derive/channel_assembler.go new file mode 100644 index 000000000000..6d1424f46a32 --- /dev/null +++ b/op-node/rollup/derive/channel_assembler.go @@ -0,0 +1,133 @@ +package derive + +import ( + "context" + "errors" + "io" + + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum/go-ethereum/log" +) + +// ChannelAssembler assembles frames into a raw channel. It replaces the ChannelBank since Holocene. +type ChannelAssembler struct { + log log.Logger + spec ChannelStageSpec + metrics Metrics + + channel *Channel + + prev NextFrameProvider +} + +var _ ResettableStage = (*ChannelAssembler)(nil) + +type ChannelStageSpec interface { + ChannelTimeout(t uint64) uint64 + MaxRLPBytesPerChannel(t uint64) uint64 +} + +// NewChannelStage creates a Holocene ChannelStage. +// It must only be used for derivation from Holocene activation. +func NewChannelStage(log log.Logger, spec ChannelStageSpec, prev NextFrameProvider, m Metrics) *ChannelAssembler { + return &ChannelAssembler{ + log: log, + spec: spec, + metrics: m, + prev: prev, + } +} + +func (ca *ChannelAssembler) Log() log.Logger { + return ca.log.New("stage", "channel", "origin", ca.Origin()) +} + +func (ca *ChannelAssembler) Origin() eth.L1BlockRef { + return ca.prev.Origin() +} + +func (ca *ChannelAssembler) Reset(context.Context, eth.L1BlockRef, eth.SystemConfig) error { + ca.resetChannel() + return io.EOF +} + +func (ca *ChannelAssembler) resetChannel() { + ca.channel = nil +} + +// Returns whether the current staging channel is timed out. Panics if there's no current channel. +func (ca *ChannelAssembler) channelTimedOut() bool { + return ca.channel.OpenBlockNumber()+ca.spec.ChannelTimeout(ca.Origin().Time) < ca.Origin().Number +} + +func (ca *ChannelAssembler) NextData(ctx context.Context) ([]byte, error) { + if ca.channel != nil && ca.channelTimedOut() { + ca.metrics.RecordChannelTimedOut() + ca.resetChannel() + } + + lgr := ca.Log() + origin := ca.Origin() + + // Note that if the current channel was already completed, we would have forwarded its data + // already. So we start by reading in frames. + if ca.channel != nil && ca.channel.IsReady() { + return nil, NewCriticalError(errors.New("unexpected ready channel")) + } + + // Ingest frames until we either hit an error (including io.EOF and NotEnoughData) or complete a + // channel. + // Note that we ingest the frame queue in a loop instead of returning NotEnoughData after a + // single frame ingestion, because it is guaranteed that the total size of new frames ingested + // per L1 origin block is limited by the size of batcher transactions in that block and it + // doesn't make a difference in computational effort if these are many small frames or one large + // frame of that size. Plus, this is really just moving data around, no decompression etc. yet. + for { + frame, err := ca.prev.NextFrame(ctx) + if err != nil { // includes io.EOF; a last frame broke the loop already + return nil, err + } + + // first frames always start a new channel, discarding an existing one + if frame.FrameNumber == 0 { + ca.metrics.RecordHeadChannelOpened() + ca.channel = NewChannel(frame.ID, origin, true) + } + if frame.FrameNumber > 0 && ca.channel == nil { + lgr.Warn("dropping non-first frame without channel", + "frame_channel", frame.ID, "frame_number", frame.FrameNumber) + continue // read more frames + } + + // Catches Holocene ordering rules. Note that even though the frame queue is guaranteed to + // only hold ordered frames in the current queue, it cannot guarantee this w.r.t. frames + // that already got dequeued. So ordering has to be checked here again. + if err := ca.channel.AddFrame(frame, origin); err != nil { + lgr.Warn("failed to add frame to channel", + "channel", ca.channel.ID(), "frame_channel", frame.ID, + "frame_number", frame.FrameNumber, "err", err) + continue // read more frames + } + if ca.channel.Size() > ca.spec.MaxRLPBytesPerChannel(ca.Origin().Time) { + lgr.Warn("dropping oversized channel", + "channel", ca.channel.ID(), "frame_number", frame.FrameNumber) + ca.resetChannel() + continue // read more frames + } + ca.metrics.RecordFrame() + + if frame.IsLast { + break // forward current complete channel + } + } + + ch := ca.channel + // Note that if we exit the frame ingestion loop, we're guaranteed to have a ready channel. + if ch == nil || !ch.IsReady() { + return nil, NewCriticalError(errors.New("unexpected non-ready channel")) + } + + ca.resetChannel() + r := ch.Reader() + return io.ReadAll(r) +} diff --git a/op-node/rollup/derive/channel_assembler_test.go b/op-node/rollup/derive/channel_assembler_test.go new file mode 100644 index 000000000000..77dc1d87f7e3 --- /dev/null +++ b/op-node/rollup/derive/channel_assembler_test.go @@ -0,0 +1,169 @@ +package derive + +import ( + "context" + "io" + "log/slog" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ethereum-optimism/optimism/op-node/metrics" + "github.com/ethereum-optimism/optimism/op-node/rollup" + rolluptest "github.com/ethereum-optimism/optimism/op-node/rollup/test" + "github.com/ethereum-optimism/optimism/op-service/testlog" +) + +func TestChannelStage_NextData(t *testing.T) { + for _, tc := range []struct { + desc string + frames [][]testFrame + expErr []error + expData []string + expChID []string + rlpOverride *uint64 + }{ + { + desc: "simple", + frames: [][]testFrame{ + {"a:0:first!"}, + }, + expErr: []error{nil}, + expData: []string{"first"}, + expChID: []string{""}, + }, + { + desc: "simple-two", + frames: [][]testFrame{ + {"a:0:first", "a:1:second!"}, + }, + expErr: []error{nil}, + expData: []string{"firstsecond"}, + expChID: []string{""}, + }, + { + desc: "drop-other", + frames: [][]testFrame{ + {"a:0:first", "b:1:foo"}, + {"a:1:second", "c:1:bar!"}, + {"a:2:third!"}, + }, + expErr: []error{io.EOF, io.EOF, nil}, + expData: []string{"", "", "firstsecondthird"}, + expChID: []string{"a", "a", ""}, + }, + { + desc: "drop-non-first", + frames: [][]testFrame{ + {"a:1:foo"}, + }, + expErr: []error{io.EOF}, + expData: []string{""}, + expChID: []string{""}, + }, + { + desc: "first-discards", + frames: [][]testFrame{ + {"b:0:foo"}, + {"a:0:first!"}, + }, + expErr: []error{io.EOF, nil}, + expData: []string{"", "first"}, + expChID: []string{"b", ""}, + }, + { + desc: "already-closed", + frames: [][]testFrame{ + {"a:0:foo"}, + {"a:1:bar!", "a:2:baz!"}, + }, + expErr: []error{io.EOF, nil}, + expData: []string{"", "foobar"}, + expChID: []string{"a", ""}, + }, + { + desc: "max-size", + frames: [][]testFrame{ + {"a:0:0123456789!"}, + }, + expErr: []error{nil}, + expData: []string{"0123456789"}, + expChID: []string{""}, + rlpOverride: ptr[uint64](frameOverhead + 10), + }, + { + desc: "oversized", + frames: [][]testFrame{ + {"a:0:0123456789x!"}, + }, + expErr: []error{io.EOF}, + expData: []string{""}, + expChID: []string{""}, + rlpOverride: ptr[uint64](frameOverhead + 10), + }, + } { + t.Run(tc.desc, func(t *testing.T) { + fq := &fakeChannelBankInput{} + lgr := testlog.Logger(t, slog.LevelWarn) + spec := &rolluptest.ChainSpec{ + ChainSpec: rollup.NewChainSpec(&rollup.Config{}), + + MaxRLPBytesPerChannelOverride: tc.rlpOverride, + } + cs := NewChannelStage(lgr, spec, fq, metrics.NoopMetrics) + + for i, fs := range tc.frames { + fq.AddFrames(fs...) + data, err := cs.NextData(context.Background()) + require.Equal(t, tc.expData[i], string(data)) + require.ErrorIs(t, tc.expErr[i], err) + // invariant: never holds a ready channel + require.True(t, cs.channel == nil || !cs.channel.IsReady()) + + cid := tc.expChID[i] + if cid == "" { + require.Nil(t, cs.channel) + } else { + require.Equal(t, strChannelID(cid), cs.channel.ID()) + } + } + + // final call should always be io.EOF after exhausting frame queue + data, err := cs.NextData(context.Background()) + require.Nil(t, data) + require.Equal(t, io.EOF, err) + }) + } +} + +func TestChannelStage_NextData_Timeout(t *testing.T) { + require := require.New(t) + fq := &fakeChannelBankInput{} + lgr := testlog.Logger(t, slog.LevelWarn) + spec := rollup.NewChainSpec(&rollup.Config{GraniteTime: ptr(uint64(0))}) // const channel timeout + cs := NewChannelStage(lgr, spec, fq, metrics.NoopMetrics) + + fq.AddFrames("a:0:foo") + data, err := cs.NextData(context.Background()) + require.Nil(data) + require.Equal(io.EOF, err) + require.NotNil(cs.channel) + require.Equal(strChannelID("a"), cs.channel.ID()) + + // move close to timeout + fq.origin.Number = spec.ChannelTimeout(0) + fq.AddFrames("a:1:bar") + data, err = cs.NextData(context.Background()) + require.Nil(data) + require.Equal(io.EOF, err) + require.NotNil(cs.channel) + require.Equal(strChannelID("a"), cs.channel.ID()) + + // timeout channel by moving origin past timeout + fq.origin.Number = spec.ChannelTimeout(0) + 1 + fq.AddFrames("a:2:baz!") + data, err = cs.NextData(context.Background()) + require.Nil(data) + require.Equal(io.EOF, err) + require.Nil(cs.channel) +} diff --git a/op-node/rollup/derive/channel_bank.go b/op-node/rollup/derive/channel_bank.go index 8dd689dfadaa..39582d2712fa 100644 --- a/op-node/rollup/derive/channel_bank.go +++ b/op-node/rollup/derive/channel_bank.go @@ -89,7 +89,7 @@ func (cb *ChannelBank) IngestFrame(f Frame) { cb.metrics.RecordHeadChannelOpened() } // create new channel if it doesn't exist yet - currentCh = NewChannel(f.ID, origin) + currentCh = NewChannel(f.ID, origin, false) cb.channels[f.ID] = currentCh cb.channelQueue = append(cb.channelQueue, f.ID) log.Info("created new channel") diff --git a/op-node/rollup/derive/channel_bank_test.go b/op-node/rollup/derive/channel_bank_test.go index 33763c23c5e0..75b7503400b1 100644 --- a/op-node/rollup/derive/channel_bank_test.go +++ b/op-node/rollup/derive/channel_bank_test.go @@ -30,6 +30,9 @@ func (f *fakeChannelBankInput) Origin() eth.L1BlockRef { } func (f *fakeChannelBankInput) NextFrame(_ context.Context) (Frame, error) { + if len(f.data) == 0 { + return Frame{}, io.EOF + } out := f.data[0] f.data = f.data[1:] return out.frame, out.err @@ -58,8 +61,12 @@ type testFrame string func (tf testFrame) ChannelID() ChannelID { parts := strings.Split(string(tf), ":") + return strChannelID(parts[0]) +} + +func strChannelID(s string) ChannelID { var chID ChannelID - copy(chID[:], parts[0]) + copy(chID[:], s) return chID } @@ -98,7 +105,6 @@ func TestChannelBankSimple(t *testing.T) { input := &fakeChannelBankInput{origin: a} input.AddFrames("a:0:first", "a:2:third!") input.AddFrames("a:1:second") - input.AddFrame(Frame{}, io.EOF) cfg := &rollup.Config{ChannelTimeoutBedrock: 10} @@ -142,7 +148,6 @@ func TestChannelBankInterleavedPreCanyon(t *testing.T) { input.AddFrames("b:1:deux", "a:2:third!") input.AddFrames("b:0:premiere") input.AddFrames("a:1:second") - input.AddFrame(Frame{}, io.EOF) cfg := &rollup.Config{ChannelTimeoutBedrock: 10, CanyonTime: nil} @@ -206,7 +211,6 @@ func TestChannelBankInterleaved(t *testing.T) { input.AddFrames("b:1:deux", "a:2:third!") input.AddFrames("b:0:premiere") input.AddFrames("a:1:second") - input.AddFrame(Frame{}, io.EOF) ct := uint64(0) cfg := &rollup.Config{ChannelTimeoutBedrock: 10, CanyonTime: &ct} @@ -267,7 +271,6 @@ func TestChannelBankDuplicates(t *testing.T) { input.AddFrames("a:0:first", "a:2:third!") input.AddFrames("a:0:altfirst", "a:2:altthird!") input.AddFrames("a:1:second") - input.AddFrame(Frame{}, io.EOF) cfg := &rollup.Config{ChannelTimeoutBedrock: 10} diff --git a/op-node/rollup/derive/channel_out_test.go b/op-node/rollup/derive/channel_out_test.go index af7fe311bf1e..328be3b7c5ad 100644 --- a/op-node/rollup/derive/channel_out_test.go +++ b/op-node/rollup/derive/channel_out_test.go @@ -450,7 +450,7 @@ func testSpanChannelOut_MaxBlocksPerSpanBatch(t *testing.T, tt maxBlocksTest) { require.NoError(t, frame.UnmarshalBinary(&frameBuf)) require.True(t, frame.IsLast) spec := rollup.NewChainSpec(&rollupCfg) - ch := NewChannel(frame.ID, l1Origin) + ch := NewChannel(frame.ID, l1Origin, false) require.False(t, ch.IsReady()) require.NoError(t, ch.AddFrame(frame, l1Origin)) require.True(t, ch.IsReady()) diff --git a/op-node/rollup/derive/channel_test.go b/op-node/rollup/derive/channel_test.go index a6594492837b..034e847b5d34 100644 --- a/op-node/rollup/derive/channel_test.go +++ b/op-node/rollup/derive/channel_test.go @@ -18,12 +18,13 @@ type frameValidityTC struct { frames []Frame shouldErr []bool sizes []uint64 + holocene bool } func (tc *frameValidityTC) Run(t *testing.T) { id := [16]byte{0xff} block := eth.L1BlockRef{} - ch := NewChannel(id, block) + ch := NewChannel(id, block, tc.holocene) if len(tc.frames) != len(tc.shouldErr) || len(tc.frames) != len(tc.sizes) { t.Errorf("lengths should be the same. frames: %d, shouldErr: %d, sizes: %d", len(tc.frames), len(tc.shouldErr), len(tc.sizes)) @@ -32,9 +33,9 @@ func (tc *frameValidityTC) Run(t *testing.T) { for i, frame := range tc.frames { err := ch.AddFrame(frame, block) if tc.shouldErr[i] { - require.NotNil(t, err) + require.Error(t, err) } else { - require.Nil(t, err) + require.NoError(t, err) } require.Equal(t, tc.sizes[i], ch.Size()) } @@ -105,6 +106,36 @@ func TestFrameValidity(t *testing.T) { shouldErr: []bool{false, false}, sizes: []uint64{207, 411}, }, + { + name: "holocene non first", + holocene: true, + frames: []Frame{ + {ID: id, FrameNumber: 2, Data: []byte("four")}, + }, + shouldErr: []bool{true}, + sizes: []uint64{0}, + }, + { + name: "holocene out of order", + holocene: true, + frames: []Frame{ + {ID: id, FrameNumber: 0, Data: []byte("four")}, + {ID: id, FrameNumber: 2, Data: []byte("seven__")}, + }, + shouldErr: []bool{false, true}, + sizes: []uint64{204, 204}, + }, + { + name: "holocene in order", + holocene: true, + frames: []Frame{ + {ID: id, FrameNumber: 0, Data: []byte("four")}, + {ID: id, FrameNumber: 1, Data: []byte("seven__")}, + {ID: id, FrameNumber: 2, IsLast: true, Data: []byte("2_")}, + }, + shouldErr: []bool{false, false, false}, + sizes: []uint64{204, 411, 613}, + }, } for _, tc := range testCases { diff --git a/op-node/rollup/test/chain_spec.go b/op-node/rollup/test/chain_spec.go new file mode 100644 index 000000000000..fd527549118f --- /dev/null +++ b/op-node/rollup/test/chain_spec.go @@ -0,0 +1,18 @@ +package test + +import "github.com/ethereum-optimism/optimism/op-node/rollup" + +// ChainSpec wraps a *rollup.ChainSpec, allowing to optionally override individual values, +// otherwise just returning the underlying ChainSpec's values. +type ChainSpec struct { + *rollup.ChainSpec + + MaxRLPBytesPerChannelOverride *uint64 // MaxRLPBytesPerChannel override +} + +func (cs *ChainSpec) MaxRLPBytesPerChannel(t uint64) uint64 { + if o := cs.MaxRLPBytesPerChannelOverride; o != nil { + return *o + } + return cs.ChainSpec.MaxRLPBytesPerChannel(t) +} From 34e8e691b75021a7f48dbba99ed8465519fec878 Mon Sep 17 00:00:00 2001 From: Matthew Slipper Date: Tue, 15 Oct 2024 11:34:45 -0600 Subject: [PATCH 28/31] op-e2e: Parallelize CGT tests (#12464) * op-e2e: Parallelize CGT tests Parallelizes the custom gas token tests. This reduces runtime from ~120s to ~60s on my Macbook. * code review updates --- op-e2e/system/gastoken/gastoken_test.go | 814 +++++++++++++----------- 1 file changed, 430 insertions(+), 384 deletions(-) diff --git a/op-e2e/system/gastoken/gastoken_test.go b/op-e2e/system/gastoken/gastoken_test.go index 4b2e6009c3f2..7e03b19d3930 100644 --- a/op-e2e/system/gastoken/gastoken_test.go +++ b/op-e2e/system/gastoken/gastoken_test.go @@ -27,6 +27,14 @@ import ( "github.com/stretchr/testify/require" ) +// setup expectations using custom gas token +type cgtTestExpectations struct { + tokenAddress common.Address + tokenName string + tokenSymbol string + tokenDecimals uint8 +} + func TestMain(m *testing.M) { op_e2e.RunMain(m) } @@ -41,413 +49,104 @@ func TestCustomGasToken_Standard(t *testing.T) { func testCustomGasToken(t *testing.T, allocType config.AllocType) { op_e2e.InitParallel(t) - cfg := e2esys.DefaultSystemConfig(t, e2esys.WithAllocType(allocType)) - offset := hexutil.Uint64(0) - cfg.DeployConfig.L2GenesisRegolithTimeOffset = &offset - cfg.DeployConfig.L1CancunTimeOffset = &offset - cfg.DeployConfig.L2GenesisCanyonTimeOffset = &offset - cfg.DeployConfig.L2GenesisDeltaTimeOffset = &offset - cfg.DeployConfig.L2GenesisEcotoneTimeOffset = &offset - - sys, err := cfg.Start(t) - require.NoError(t, err, "Error starting up system") - l1Client := sys.NodeClient("l1") - l2Client := sys.NodeClient("sequencer") - - aliceOpts, err := bind.NewKeyedTransactorWithChainID(cfg.Secrets.Alice, cfg.L1ChainIDBig()) - require.NoError(t, err) - - // Deploy WETH9, we'll use this as our custom gas token for the purpose of the test - weth9Address, tx, weth9, err := bindings.DeployWETH9(aliceOpts, l1Client) - require.NoError(t, err) - _, err = wait.ForReceiptOK(context.Background(), l1Client, tx.Hash()) - require.NoError(t, err) - - // setup expectations using custom gas token - type Expectations struct { - tokenAddress common.Address - tokenName string - tokenSymbol string - tokenDecimals uint8 - } - disabledExpectations := Expectations{ + disabledExpectations := cgtTestExpectations{ common.HexToAddress("0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"), "Ether", "ETH", uint8(18), } - enabledExpectations := Expectations{} - enabledExpectations.tokenAddress = weth9Address - enabledExpectations.tokenName, err = weth9.Name(&bind.CallOpts{}) - require.NoError(t, err) - enabledExpectations.tokenSymbol, err = weth9.Symbol(&bind.CallOpts{}) - require.NoError(t, err) - enabledExpectations.tokenDecimals, err = weth9.Decimals(&bind.CallOpts{}) - require.NoError(t, err) - - // Get some WETH - aliceOpts.Value = big.NewInt(10_000_000) - tx, err = weth9.Deposit(aliceOpts) - waitForTx(t, tx, err, l1Client) - aliceOpts.Value = nil - newBalance, err := weth9.BalanceOf(&bind.CallOpts{}, aliceOpts.From) - require.NoError(t, err) - require.Equal(t, newBalance, big.NewInt(10_000_000)) - - // Function to prepare and make call to depositERC20Transaction and make - // appropriate assertions dependent on whether custom gas tokens have been enabled or not. - checkDeposit := func(t *testing.T, enabled bool) { - // Set amount of WETH9 to bridge to the recipient on L2 - amountToBridge := big.NewInt(10) - recipient := common.HexToAddress("0xbeefdead") - - // Approve OptimismPortal - tx, err = weth9.Approve(aliceOpts, cfg.L1Deployments.OptimismPortalProxy, amountToBridge) - waitForTx(t, tx, err, l1Client) - - // Get recipient L2 balance before bridging - previousL2Balance, err := l2Client.BalanceAt(context.Background(), recipient, nil) - require.NoError(t, err) - - // Bridge the tokens - optimismPortal, err := bindings.NewOptimismPortal(cfg.L1Deployments.OptimismPortalProxy, l1Client) - require.NoError(t, err) - tx, err = optimismPortal.DepositERC20Transaction(aliceOpts, - recipient, - amountToBridge, - amountToBridge, - 50_0000, // _gasLimit - false, - []byte{}, - ) - if enabled { - require.NoError(t, err) - receipt, err := wait.ForReceiptOK(context.Background(), l1Client, tx.Hash()) - require.NoError(t, err) - - // compute the deposit transaction hash + poll for it - depositEvent, err := receipts.FindLog(receipt.Logs, optimismPortal.ParseTransactionDeposited) - require.NoError(t, err, "Should emit deposit event") - depositTx, err := derive.UnmarshalDepositLogEvent(&depositEvent.Raw) - require.NoError(t, err) - _, err = wait.ForReceiptOK(context.Background(), l2Client, types.NewTx(depositTx).Hash()) - require.NoError(t, err) - - require.EventuallyWithT(t, func(t *assert.CollectT) { - // check for balance increase on L2 - newL2Balance, err := l2Client.BalanceAt(context.Background(), recipient, nil) - require.NoError(t, err) - l2BalanceIncrease := big.NewInt(0).Sub(newL2Balance, previousL2Balance) - require.Equal(t, amountToBridge, l2BalanceIncrease) - }, 10*time.Second, 1*time.Second) - } else { - require.Error(t, err) - } - } - - // Function to prepare and execute withdrawal flow for CGTs - // and assert token balance is increased on L1. - checkWithdrawal := func(t *testing.T) { - l2Seq := l2Client - l2Verif := sys.NodeClient("verifier") - fromAddr := aliceOpts.From - ethPrivKey := cfg.Secrets.Alice - - // Start L2 balance for withdrawal - startBalanceBeforeWithdrawal, err := l2Seq.BalanceAt(context.Background(), fromAddr, nil) - require.NoError(t, err) - - withdrawAmount := big.NewInt(5) - tx, receipt := helpers.SendWithdrawal(t, cfg, l2Seq, cfg.Secrets.Alice, func(opts *helpers.WithdrawalTxOpts) { - opts.Value = withdrawAmount - opts.VerifyOnClients(l2Verif) - }) - - // Verify L2 balance after withdrawal - header, err := l2Verif.HeaderByNumber(context.Background(), receipt.BlockNumber) - require.NoError(t, err) - - endBalanceAfterWithdrawal, err := wait.ForBalanceChange(context.Background(), l2Seq, fromAddr, startBalanceBeforeWithdrawal) - require.NoError(t, err) - - // Take fee into account - diff := new(big.Int).Sub(startBalanceBeforeWithdrawal, endBalanceAfterWithdrawal) - fees := helpers.CalcGasFees(receipt.GasUsed, tx.GasTipCap(), tx.GasFeeCap(), header.BaseFee) - fees = fees.Add(fees, receipt.L1Fee) - diff = diff.Sub(diff, fees) - require.Equal(t, withdrawAmount, diff) - - // Take start token balance on L1 - startTokenBalanceBeforeFinalize, err := weth9.BalanceOf(&bind.CallOpts{}, fromAddr) - require.NoError(t, err) - - startETHBalanceBeforeFinalize, err := l1Client.BalanceAt(context.Background(), fromAddr, nil) - require.NoError(t, err) - - proveReceipt, finalizeReceipt, resolveClaimReceipt, resolveReceipt := helpers.ProveAndFinalizeWithdrawal(t, cfg, sys, "verifier", ethPrivKey, receipt) - - // Verify L1 ETH balance change - proveFee := new(big.Int).Mul(new(big.Int).SetUint64(proveReceipt.GasUsed), proveReceipt.EffectiveGasPrice) - finalizeFee := new(big.Int).Mul(new(big.Int).SetUint64(finalizeReceipt.GasUsed), finalizeReceipt.EffectiveGasPrice) - fees = new(big.Int).Add(proveFee, finalizeFee) - if allocType.UsesProofs() { - resolveClaimFee := new(big.Int).Mul(new(big.Int).SetUint64(resolveClaimReceipt.GasUsed), resolveClaimReceipt.EffectiveGasPrice) - resolveFee := new(big.Int).Mul(new(big.Int).SetUint64(resolveReceipt.GasUsed), resolveReceipt.EffectiveGasPrice) - fees = new(big.Int).Add(fees, resolveClaimFee) - fees = new(big.Int).Add(fees, resolveFee) - } - - // Verify L1ETHBalance after withdrawal - // On CGT chains, the only change in ETH balance from a withdrawal - // is a decrease to pay for gas - endETHBalanceAfterFinalize, err := l1Client.BalanceAt(context.Background(), fromAddr, nil) - require.NoError(t, err) - diff = new(big.Int).Sub(endETHBalanceAfterFinalize, startETHBalanceBeforeFinalize) - require.Equal(t, new(big.Int).Sub(big.NewInt(0), fees), diff) - - // Verify token balance after withdrawal - // L1 Fees are paid in ETH, and - // withdrawal is of a Custom Gas Token, so we do not subtract l1 fees from expected balance change - // as we would if ETH was the gas paying token - endTokenBalanceAfterFinalize, err := weth9.BalanceOf(&bind.CallOpts{}, fromAddr) - require.NoError(t, err) - diff = new(big.Int).Sub(endTokenBalanceAfterFinalize, startTokenBalanceBeforeFinalize) - require.Equal(t, withdrawAmount, diff) - } - - // checkFeeWithdrawal ensures that the FeeVault can be withdrawn from - checkFeeWithdrawal := func(t *testing.T, enabled bool) { - feeVault, err := bindings.NewSequencerFeeVault(predeploys.SequencerFeeVaultAddr, l2Client) - require.NoError(t, err) - - // Alice will be sending transactions - aliceOpts, err := bind.NewKeyedTransactorWithChainID(cfg.Secrets.Alice, cfg.L2ChainIDBig()) - require.NoError(t, err) - - // Get the recipient of the funds - recipient, err := feeVault.RECIPIENT(&bind.CallOpts{}) - require.NoError(t, err) - - // This test depends on the withdrawal network being L1 which is represented - // by 0 in the enum. - withdrawalNetwork, err := feeVault.WITHDRAWALNETWORK(&bind.CallOpts{}) - require.NoError(t, err) - require.Equal(t, withdrawalNetwork, uint8(0)) - - // Get the balance of the recipient on L1 - var recipientBalanceBefore *big.Int - if enabled { - recipientBalanceBefore, err = weth9.BalanceOf(&bind.CallOpts{}, recipient) - } else { - recipientBalanceBefore, err = l1Client.BalanceAt(context.Background(), recipient, nil) - } - require.NoError(t, err) - - // Get the min withdrawal amount for the FeeVault - amount, err := feeVault.MINWITHDRAWALAMOUNT(&bind.CallOpts{}) - require.NoError(t, err) - - l1opts, err := bind.NewKeyedTransactorWithChainID(cfg.Secrets.Alice, cfg.L1ChainIDBig()) - require.NoError(t, err) - - optimismPortal, err := bindings.NewOptimismPortal(cfg.L1Deployments.OptimismPortalProxy, l1Client) - require.NoError(t, err) - - depositAmount := new(big.Int).Mul(amount, big.NewInt(14)) - l1opts.Value = depositAmount - - var receipt *types.Receipt - - // Alice deposits funds - if enabled { - // approve + transferFrom flow - // Cannot use `transfer` because of the tracking of balance in the OptimismPortal - dep, err := weth9.Deposit(l1opts) - waitForTx(t, dep, err, l1Client) - - l1opts.Value = nil - tx, err := weth9.Approve(l1opts, cfg.L1Deployments.OptimismPortalProxy, depositAmount) - waitForTx(t, tx, err, l1Client) - - require.NoError(t, err) - deposit, err := optimismPortal.DepositERC20Transaction(l1opts, cfg.Secrets.Addresses().Alice, depositAmount, depositAmount, 500_000, false, []byte{}) - waitForTx(t, deposit, err, l1Client) - - receipt, err = wait.ForReceiptOK(context.Background(), l1Client, deposit.Hash()) - require.NoError(t, err) - } else { - // send ether to the portal directly, alice already has funds on L2 - tx, err := optimismPortal.DepositTransaction(l1opts, cfg.Secrets.Addresses().Alice, depositAmount, 500_000, false, []byte{}) - waitForTx(t, tx, err, l1Client) - - receipt, err = wait.ForReceiptOK(context.Background(), l1Client, tx.Hash()) - require.NoError(t, err) - } - - // Compute the deposit transaction hash + poll for it - depositEvent, err := receipts.FindLog(receipt.Logs, optimismPortal.ParseTransactionDeposited) - require.NoError(t, err, "Should emit deposit event") - depositTx, err := derive.UnmarshalDepositLogEvent(&depositEvent.Raw) - require.NoError(t, err) - _, err = wait.ForReceiptOK(context.Background(), l2Client, types.NewTx(depositTx).Hash()) - require.NoError(t, err) - - // Get Alice's balance on L2 - aliceBalance, err := l2Client.BalanceAt(context.Background(), cfg.Secrets.Addresses().Alice, nil) - require.NoError(t, err) - require.GreaterOrEqual(t, aliceBalance.Uint64(), amount.Uint64()) - - // Send funds to the FeeVault so its balance is above the min withdrawal amount - aliceOpts.Value = amount - feeVaultTx, err := feeVault.Receive(aliceOpts) - waitForTx(t, feeVaultTx, err, l2Client) - - // Ensure that the balance of the vault is large enough to withdraw - vaultBalance, err := l2Client.BalanceAt(context.Background(), predeploys.SequencerFeeVaultAddr, nil) - require.NoError(t, err) - require.GreaterOrEqual(t, vaultBalance.Uint64(), amount.Uint64()) - - // Ensure there is code at the vault address - code, err := l2Client.CodeAt(context.Background(), predeploys.SequencerFeeVaultAddr, nil) - require.NoError(t, err) - require.NotEmpty(t, code) - - // Poke the fee vault to withdraw - l2Opts, err := bind.NewKeyedTransactorWithChainID(cfg.Secrets.Bob, cfg.L2ChainIDBig()) - require.NoError(t, err) - withdrawalTx, err := feeVault.Withdraw(l2Opts) - waitForTx(t, withdrawalTx, err, l2Client) - - // Get the receipt and the amount withdrawn - receipt, err = l2Client.TransactionReceipt(context.Background(), withdrawalTx.Hash()) - require.NoError(t, err) - - inclusionHeight := receipt.BlockNumber.Uint64() - it, err := feeVault.FilterWithdrawal(&bind.FilterOpts{ - Start: inclusionHeight, - End: &inclusionHeight, - }) - require.NoError(t, err) - require.True(t, it.Next()) - - withdrawnAmount := it.Event.Value - - // Finalize the withdrawal - proveReceipt, finalizeReceipt, resolveClaimReceipt, resolveReceipt := helpers.ProveAndFinalizeWithdrawal(t, cfg, sys, "verifier", cfg.Secrets.Alice, receipt) - require.Equal(t, types.ReceiptStatusSuccessful, proveReceipt.Status) - require.Equal(t, types.ReceiptStatusSuccessful, finalizeReceipt.Status) - if allocType.UsesProofs() { - require.Equal(t, types.ReceiptStatusSuccessful, resolveClaimReceipt.Status) - require.Equal(t, types.ReceiptStatusSuccessful, resolveReceipt.Status) - } - - // Assert that the recipient's balance did increase - var recipientBalanceAfter *big.Int - if enabled { - recipientBalanceAfter, err = weth9.BalanceOf(&bind.CallOpts{}, recipient) - } else { - recipientBalanceAfter, err = l1Client.BalanceAt(context.Background(), recipient, nil) - } - require.NoError(t, err) - - require.Equal(t, recipientBalanceAfter, new(big.Int).Add(recipientBalanceBefore, withdrawnAmount)) - } - - checkL1TokenNameAndSymbol := func(t *testing.T, enabled bool) { - systemConfig, err := bindings.NewSystemConfig(cfg.L1Deployments.SystemConfigProxy, l1Client) - require.NoError(t, err) - - token, err := systemConfig.GasPayingToken(&bind.CallOpts{}) - require.NoError(t, err) - - name, err := systemConfig.GasPayingTokenName(&bind.CallOpts{}) - require.NoError(t, err) - - symbol, err := systemConfig.GasPayingTokenSymbol(&bind.CallOpts{}) - require.NoError(t, err) - if enabled { - require.Equal(t, enabledExpectations.tokenAddress, token.Addr) - require.Equal(t, enabledExpectations.tokenDecimals, token.Decimals) - require.Equal(t, enabledExpectations.tokenName, name) - require.Equal(t, enabledExpectations.tokenSymbol, symbol) - } else { - require.Equal(t, disabledExpectations.tokenAddress, token.Addr) - require.Equal(t, disabledExpectations.tokenDecimals, token.Decimals) - require.Equal(t, disabledExpectations.tokenName, name) - require.Equal(t, disabledExpectations.tokenSymbol, symbol) - } - } + setup := func() gasTokenTestOpts { + cfg := e2esys.DefaultSystemConfig(t, e2esys.WithAllocType(allocType)) + offset := hexutil.Uint64(0) + cfg.DeployConfig.L2GenesisRegolithTimeOffset = &offset + cfg.DeployConfig.L1CancunTimeOffset = &offset + cfg.DeployConfig.L2GenesisCanyonTimeOffset = &offset + cfg.DeployConfig.L2GenesisDeltaTimeOffset = &offset + cfg.DeployConfig.L2GenesisEcotoneTimeOffset = &offset - checkL2TokenNameAndSymbol := func(t *testing.T, enabled bool) { - l1Block, err := bindings.NewL1Block(predeploys.L1BlockAddr, l2Client) - require.NoError(t, err) + sys, err := cfg.Start(t) + require.NoError(t, err, "Error starting up system") - token, err := l1Block.GasPayingToken(&bind.CallOpts{}) + l1Client := sys.NodeClient("l1") + aliceOpts, err := bind.NewKeyedTransactorWithChainID(cfg.Secrets.Alice, cfg.L1ChainIDBig()) require.NoError(t, err) - name, err := l1Block.GasPayingTokenName(&bind.CallOpts{}) + // Deploy WETH9, we'll use this as our custom gas token for the purpose of the test + weth9Address, tx, weth9, err := bindings.DeployWETH9(aliceOpts, l1Client) require.NoError(t, err) - - symbol, err := l1Block.GasPayingTokenSymbol(&bind.CallOpts{}) + _, err = wait.ForReceiptOK(context.Background(), l1Client, tx.Hash()) require.NoError(t, err) - if enabled { - require.Equal(t, enabledExpectations.tokenAddress, token.Addr) - require.Equal(t, enabledExpectations.tokenDecimals, token.Decimals) - require.Equal(t, enabledExpectations.tokenName, name) - require.Equal(t, enabledExpectations.tokenSymbol, symbol) - } else { - require.Equal(t, disabledExpectations.tokenAddress, token.Addr) - require.Equal(t, disabledExpectations.tokenDecimals, token.Decimals) - require.Equal(t, disabledExpectations.tokenName, name) - require.Equal(t, disabledExpectations.tokenSymbol, symbol) - } - } - - checkWETHTokenNameAndSymbol := func(t *testing.T, enabled bool) { - // Check name and symbol in WETH predeploy - weth, err := bindings.NewWETH(predeploys.WETHAddr, l2Client) + enabledExpectations := cgtTestExpectations{} + enabledExpectations.tokenAddress = weth9Address + enabledExpectations.tokenName, err = weth9.Name(&bind.CallOpts{}) require.NoError(t, err) - - name, err := weth.Name(&bind.CallOpts{}) + enabledExpectations.tokenSymbol, err = weth9.Symbol(&bind.CallOpts{}) require.NoError(t, err) - - symbol, err := weth.Symbol(&bind.CallOpts{}) + enabledExpectations.tokenDecimals, err = weth9.Decimals(&bind.CallOpts{}) require.NoError(t, err) - if enabled { - require.Equal(t, "Wrapped "+enabledExpectations.tokenName, name) - require.Equal(t, "W"+enabledExpectations.tokenSymbol, symbol) - } else { - require.Equal(t, "Wrapped "+disabledExpectations.tokenName, name) - require.Equal(t, "W"+disabledExpectations.tokenSymbol, symbol) + // Get some WETH + aliceOpts.Value = big.NewInt(10_000_000) + tx, err = weth9.Deposit(aliceOpts) + waitForTx(t, tx, err, l1Client) + aliceOpts.Value = nil + newBalance, err := weth9.BalanceOf(&bind.CallOpts{}, aliceOpts.From) + require.NoError(t, err) + require.Equal(t, newBalance, big.NewInt(10_000_000)) + + return gasTokenTestOpts{ + aliceOpts: aliceOpts, + cfg: cfg, + weth9: weth9, + weth9Address: weth9Address, + allocType: allocType, + sys: sys, + enabledExpectations: enabledExpectations, + disabledExpectations: disabledExpectations, } } - // Begin by testing behaviour when CGT feature is not enabled - enabled := false - checkDeposit(t, enabled) - checkL1TokenNameAndSymbol(t, enabled) - checkL2TokenNameAndSymbol(t, enabled) - checkWETHTokenNameAndSymbol(t, enabled) - checkFeeWithdrawal(t, enabled) - - // Activate custom gas token feature (devnet does not have this activated at genesis) - setCustomGasToken(t, cfg, sys, weth9Address) - - // Now test behaviour given CGT feature is enabled - enabled = true - checkDeposit(t, enabled) - checkWithdrawal(t) - checkL1TokenNameAndSymbol(t, enabled) - checkL2TokenNameAndSymbol(t, enabled) - checkWETHTokenNameAndSymbol(t, enabled) - checkFeeWithdrawal(t, enabled) + t.Run("deposit", func(t *testing.T) { + op_e2e.InitParallel(t) + gto := setup() + checkDeposit(t, gto, false) + setCustomGasToken(t, gto.cfg, gto.sys, gto.weth9Address) + checkDeposit(t, gto, true) + }) + + t.Run("withdrawal", func(t *testing.T) { + op_e2e.InitParallel(t) + gto := setup() + setCustomGasToken(t, gto.cfg, gto.sys, gto.weth9Address) + checkDeposit(t, gto, true) + checkWithdrawal(t, gto) + }) + + t.Run("fee withdrawal", func(t *testing.T) { + op_e2e.InitParallel(t) + gto := setup() + setCustomGasToken(t, gto.cfg, gto.sys, gto.weth9Address) + checkDeposit(t, gto, true) + checkFeeWithdrawal(t, gto, true) + }) + + t.Run("token name and symbol", func(t *testing.T) { + op_e2e.InitParallel(t) + gto := setup() + checkL1TokenNameAndSymbol(t, gto, gto.disabledExpectations) + checkL2TokenNameAndSymbol(t, gto, gto.disabledExpectations) + checkWETHTokenNameAndSymbol(t, gto, gto.disabledExpectations) + setCustomGasToken(t, gto.cfg, gto.sys, gto.weth9Address) + checkL1TokenNameAndSymbol(t, gto, gto.enabledExpectations) + checkL2TokenNameAndSymbol(t, gto, gto.enabledExpectations) + checkWETHTokenNameAndSymbol(t, gto, gto.enabledExpectations) + }) } -// setCustomGasToeken enables the Custom Gas Token feature on a chain where it wasn't enabled at genesis. +// setCustomGasToken enables the Custom Gas Token feature on a chain where it wasn't enabled at genesis. // It reads existing parameters from the SystemConfig contract, inserts the supplied cgtAddress and reinitializes that contract. // To do this it uses the ProxyAdmin and StorageSetter from the supplied cfg. func setCustomGasToken(t *testing.T, cfg e2esys.SystemConfig, sys *e2esys.System, cgtAddress common.Address) { @@ -571,3 +270,350 @@ func waitForTx(t *testing.T, tx *types.Transaction, err error, client *ethclient _, err = wait.ForReceiptOK(context.Background(), client, tx.Hash()) require.NoError(t, err) } + +type gasTokenTestOpts struct { + aliceOpts *bind.TransactOpts + cfg e2esys.SystemConfig + weth9 *bindings.WETH9 + weth9Address common.Address + allocType config.AllocType + sys *e2esys.System + enabledExpectations cgtTestExpectations + disabledExpectations cgtTestExpectations +} + +// Function to prepare and make call to depositERC20Transaction and make +// appropriate assertions dependent on whether custom gas tokens have been enabled or not. +func checkDeposit(t *testing.T, gto gasTokenTestOpts, enabled bool) { + aliceOpts := gto.aliceOpts + cfg := gto.cfg + l1Client := gto.sys.NodeClient("l1") + l2Client := gto.sys.NodeClient("sequencer") + weth9 := gto.weth9 + + // Set amount of WETH9 to bridge to the recipient on L2 + amountToBridge := big.NewInt(10) + recipient := common.HexToAddress("0xbeefdead") + + // Approve OptimismPortal + tx, err := weth9.Approve(aliceOpts, cfg.L1Deployments.OptimismPortalProxy, amountToBridge) + waitForTx(t, tx, err, l1Client) + + // Get recipient L2 balance before bridging + previousL2Balance, err := l2Client.BalanceAt(context.Background(), recipient, nil) + require.NoError(t, err) + + // Bridge the tokens + optimismPortal, err := bindings.NewOptimismPortal(cfg.L1Deployments.OptimismPortalProxy, l1Client) + require.NoError(t, err) + tx, err = optimismPortal.DepositERC20Transaction(aliceOpts, + recipient, + amountToBridge, + amountToBridge, + 50_0000, // _gasLimit + false, + []byte{}, + ) + if enabled { + require.NoError(t, err) + receipt, err := wait.ForReceiptOK(context.Background(), l1Client, tx.Hash()) + require.NoError(t, err) + + // compute the deposit transaction hash + poll for it + depositEvent, err := receipts.FindLog(receipt.Logs, optimismPortal.ParseTransactionDeposited) + require.NoError(t, err, "Should emit deposit event") + depositTx, err := derive.UnmarshalDepositLogEvent(&depositEvent.Raw) + require.NoError(t, err) + _, err = wait.ForReceiptOK(context.Background(), l2Client, types.NewTx(depositTx).Hash()) + require.NoError(t, err) + + require.EventuallyWithT(t, func(t *assert.CollectT) { + // check for balance increase on L2 + newL2Balance, err := l2Client.BalanceAt(context.Background(), recipient, nil) + require.NoError(t, err) + l2BalanceIncrease := big.NewInt(0).Sub(newL2Balance, previousL2Balance) + require.Equal(t, amountToBridge, l2BalanceIncrease) + }, 10*time.Second, 1*time.Second) + } else { + require.Error(t, err) + } +} + +// Function to prepare and execute withdrawal flow for CGTs +// and assert token balance is increased on L1. +func checkWithdrawal(t *testing.T, gto gasTokenTestOpts) { + aliceOpts := gto.aliceOpts + cfg := gto.cfg + weth9 := gto.weth9 + allocType := gto.allocType + l1Client := gto.sys.NodeClient("l1") + l2Seq := gto.sys.NodeClient("sequencer") + l2Verif := gto.sys.NodeClient("verifier") + fromAddr := aliceOpts.From + ethPrivKey := cfg.Secrets.Alice + + // Start L2 balance for withdrawal + startBalanceBeforeWithdrawal, err := l2Seq.BalanceAt(context.Background(), fromAddr, nil) + require.NoError(t, err) + + withdrawAmount := big.NewInt(5) + tx, receipt := helpers.SendWithdrawal(t, cfg, l2Seq, cfg.Secrets.Alice, func(opts *helpers.WithdrawalTxOpts) { + opts.Value = withdrawAmount + opts.VerifyOnClients(l2Verif) + }) + + // Verify L2 balance after withdrawal + header, err := l2Verif.HeaderByNumber(context.Background(), receipt.BlockNumber) + require.NoError(t, err) + + endBalanceAfterWithdrawal, err := wait.ForBalanceChange(context.Background(), l2Seq, fromAddr, startBalanceBeforeWithdrawal) + require.NoError(t, err) + + // Take fee into account + diff := new(big.Int).Sub(startBalanceBeforeWithdrawal, endBalanceAfterWithdrawal) + fees := helpers.CalcGasFees(receipt.GasUsed, tx.GasTipCap(), tx.GasFeeCap(), header.BaseFee) + fees = fees.Add(fees, receipt.L1Fee) + diff = diff.Sub(diff, fees) + require.Equal(t, withdrawAmount, diff) + + // Take start token balance on L1 + startTokenBalanceBeforeFinalize, err := weth9.BalanceOf(&bind.CallOpts{}, fromAddr) + require.NoError(t, err) + + startETHBalanceBeforeFinalize, err := l1Client.BalanceAt(context.Background(), fromAddr, nil) + require.NoError(t, err) + + proveReceipt, finalizeReceipt, resolveClaimReceipt, resolveReceipt := helpers.ProveAndFinalizeWithdrawal(t, cfg, gto.sys, "verifier", ethPrivKey, receipt) + + // Verify L1 ETH balance change + proveFee := new(big.Int).Mul(new(big.Int).SetUint64(proveReceipt.GasUsed), proveReceipt.EffectiveGasPrice) + finalizeFee := new(big.Int).Mul(new(big.Int).SetUint64(finalizeReceipt.GasUsed), finalizeReceipt.EffectiveGasPrice) + fees = new(big.Int).Add(proveFee, finalizeFee) + if allocType.UsesProofs() { + resolveClaimFee := new(big.Int).Mul(new(big.Int).SetUint64(resolveClaimReceipt.GasUsed), resolveClaimReceipt.EffectiveGasPrice) + resolveFee := new(big.Int).Mul(new(big.Int).SetUint64(resolveReceipt.GasUsed), resolveReceipt.EffectiveGasPrice) + fees = new(big.Int).Add(fees, resolveClaimFee) + fees = new(big.Int).Add(fees, resolveFee) + } + + // Verify L1ETHBalance after withdrawal + // On CGT chains, the only change in ETH balance from a withdrawal + // is a decrease to pay for gas + endETHBalanceAfterFinalize, err := l1Client.BalanceAt(context.Background(), fromAddr, nil) + require.NoError(t, err) + diff = new(big.Int).Sub(endETHBalanceAfterFinalize, startETHBalanceBeforeFinalize) + require.Equal(t, new(big.Int).Sub(big.NewInt(0), fees), diff) + + // Verify token balance after withdrawal + // L1 Fees are paid in ETH, and + // withdrawal is of a Custom Gas Token, so we do not subtract l1 fees from expected balance change + // as we would if ETH was the gas paying token + endTokenBalanceAfterFinalize, err := weth9.BalanceOf(&bind.CallOpts{}, fromAddr) + require.NoError(t, err) + diff = new(big.Int).Sub(endTokenBalanceAfterFinalize, startTokenBalanceBeforeFinalize) + require.Equal(t, withdrawAmount, diff) +} + +// checkFeeWithdrawal ensures that the FeeVault can be withdrawn from +func checkFeeWithdrawal(t *testing.T, gto gasTokenTestOpts, enabled bool) { + cfg := gto.cfg + weth9 := gto.weth9 + allocType := gto.allocType + l1Client := gto.sys.NodeClient("l1") + l2Client := gto.sys.NodeClient("sequencer") + + feeVault, err := bindings.NewSequencerFeeVault(predeploys.SequencerFeeVaultAddr, l2Client) + require.NoError(t, err) + + // Alice will be sending transactions + aliceOpts, err := bind.NewKeyedTransactorWithChainID(cfg.Secrets.Alice, cfg.L2ChainIDBig()) + require.NoError(t, err) + + // Get the recipient of the funds + recipient, err := feeVault.RECIPIENT(&bind.CallOpts{}) + require.NoError(t, err) + + // This test depends on the withdrawal network being L1 which is represented + // by 0 in the enum. + withdrawalNetwork, err := feeVault.WITHDRAWALNETWORK(&bind.CallOpts{}) + require.NoError(t, err) + require.Equal(t, withdrawalNetwork, uint8(0)) + + // Get the balance of the recipient on L1 + var recipientBalanceBefore *big.Int + if enabled { + recipientBalanceBefore, err = weth9.BalanceOf(&bind.CallOpts{}, recipient) + } else { + recipientBalanceBefore, err = l1Client.BalanceAt(context.Background(), recipient, nil) + } + require.NoError(t, err) + + // Get the min withdrawal amount for the FeeVault + amount, err := feeVault.MINWITHDRAWALAMOUNT(&bind.CallOpts{}) + require.NoError(t, err) + + l1opts, err := bind.NewKeyedTransactorWithChainID(cfg.Secrets.Alice, cfg.L1ChainIDBig()) + require.NoError(t, err) + + optimismPortal, err := bindings.NewOptimismPortal(cfg.L1Deployments.OptimismPortalProxy, l1Client) + require.NoError(t, err) + + depositAmount := new(big.Int).Mul(amount, big.NewInt(14)) + l1opts.Value = depositAmount + + var receipt *types.Receipt + + // Alice deposits funds + if enabled { + // approve + transferFrom flow + // Cannot use `transfer` because of the tracking of balance in the OptimismPortal + dep, err := weth9.Deposit(l1opts) + waitForTx(t, dep, err, l1Client) + + l1opts.Value = nil + tx, err := weth9.Approve(l1opts, cfg.L1Deployments.OptimismPortalProxy, depositAmount) + waitForTx(t, tx, err, l1Client) + + require.NoError(t, err) + deposit, err := optimismPortal.DepositERC20Transaction(l1opts, cfg.Secrets.Addresses().Alice, depositAmount, depositAmount, 500_000, false, []byte{}) + waitForTx(t, deposit, err, l1Client) + + receipt, err = wait.ForReceiptOK(context.Background(), l1Client, deposit.Hash()) + require.NoError(t, err) + } else { + // send ether to the portal directly, alice already has funds on L2 + tx, err := optimismPortal.DepositTransaction(l1opts, cfg.Secrets.Addresses().Alice, depositAmount, 500_000, false, []byte{}) + waitForTx(t, tx, err, l1Client) + + receipt, err = wait.ForReceiptOK(context.Background(), l1Client, tx.Hash()) + require.NoError(t, err) + } + + // Compute the deposit transaction hash + poll for it + depositEvent, err := receipts.FindLog(receipt.Logs, optimismPortal.ParseTransactionDeposited) + require.NoError(t, err, "Should emit deposit event") + depositTx, err := derive.UnmarshalDepositLogEvent(&depositEvent.Raw) + require.NoError(t, err) + _, err = wait.ForReceiptOK(context.Background(), l2Client, types.NewTx(depositTx).Hash()) + require.NoError(t, err) + + // Get Alice's balance on L2 + aliceBalance, err := l2Client.BalanceAt(context.Background(), cfg.Secrets.Addresses().Alice, nil) + require.NoError(t, err) + require.GreaterOrEqual(t, aliceBalance.Uint64(), amount.Uint64()) + + // Send funds to the FeeVault so its balance is above the min withdrawal amount + aliceOpts.Value = amount + feeVaultTx, err := feeVault.Receive(aliceOpts) + waitForTx(t, feeVaultTx, err, l2Client) + + // Ensure that the balance of the vault is large enough to withdraw + vaultBalance, err := l2Client.BalanceAt(context.Background(), predeploys.SequencerFeeVaultAddr, nil) + require.NoError(t, err) + require.GreaterOrEqual(t, vaultBalance.Uint64(), amount.Uint64()) + + // Ensure there is code at the vault address + code, err := l2Client.CodeAt(context.Background(), predeploys.SequencerFeeVaultAddr, nil) + require.NoError(t, err) + require.NotEmpty(t, code) + + // Poke the fee vault to withdraw + l2Opts, err := bind.NewKeyedTransactorWithChainID(cfg.Secrets.Bob, cfg.L2ChainIDBig()) + require.NoError(t, err) + withdrawalTx, err := feeVault.Withdraw(l2Opts) + waitForTx(t, withdrawalTx, err, l2Client) + + // Get the receipt and the amount withdrawn + receipt, err = l2Client.TransactionReceipt(context.Background(), withdrawalTx.Hash()) + require.NoError(t, err) + + inclusionHeight := receipt.BlockNumber.Uint64() + it, err := feeVault.FilterWithdrawal(&bind.FilterOpts{ + Start: inclusionHeight, + End: &inclusionHeight, + }) + require.NoError(t, err) + require.True(t, it.Next()) + + withdrawnAmount := it.Event.Value + + // Finalize the withdrawal + proveReceipt, finalizeReceipt, resolveClaimReceipt, resolveReceipt := helpers.ProveAndFinalizeWithdrawal(t, cfg, gto.sys, "verifier", cfg.Secrets.Alice, receipt) + require.Equal(t, types.ReceiptStatusSuccessful, proveReceipt.Status) + require.Equal(t, types.ReceiptStatusSuccessful, finalizeReceipt.Status) + if allocType.UsesProofs() { + require.Equal(t, types.ReceiptStatusSuccessful, resolveClaimReceipt.Status) + require.Equal(t, types.ReceiptStatusSuccessful, resolveReceipt.Status) + } + + // Assert that the recipient's balance did increase + var recipientBalanceAfter *big.Int + if enabled { + recipientBalanceAfter, err = weth9.BalanceOf(&bind.CallOpts{}, recipient) + } else { + recipientBalanceAfter, err = l1Client.BalanceAt(context.Background(), recipient, nil) + } + require.NoError(t, err) + + require.Equal(t, recipientBalanceAfter, new(big.Int).Add(recipientBalanceBefore, withdrawnAmount)) +} + +func checkL1TokenNameAndSymbol(t *testing.T, gto gasTokenTestOpts, expectations cgtTestExpectations) { + l1Client := gto.sys.NodeClient("l1") + cfg := gto.cfg + + systemConfig, err := bindings.NewSystemConfig(cfg.L1Deployments.SystemConfigProxy, l1Client) + require.NoError(t, err) + + token, err := systemConfig.GasPayingToken(&bind.CallOpts{}) + require.NoError(t, err) + + name, err := systemConfig.GasPayingTokenName(&bind.CallOpts{}) + require.NoError(t, err) + + symbol, err := systemConfig.GasPayingTokenSymbol(&bind.CallOpts{}) + require.NoError(t, err) + + require.Equal(t, expectations.tokenAddress, token.Addr) + require.Equal(t, expectations.tokenDecimals, token.Decimals) + require.Equal(t, expectations.tokenName, name) + require.Equal(t, expectations.tokenSymbol, symbol) +} + +func checkL2TokenNameAndSymbol(t *testing.T, gto gasTokenTestOpts, enabledExpectations cgtTestExpectations) { + l2Client := gto.sys.NodeClient("sequencer") + + l1Block, err := bindings.NewL1Block(predeploys.L1BlockAddr, l2Client) + require.NoError(t, err) + + token, err := l1Block.GasPayingToken(&bind.CallOpts{}) + require.NoError(t, err) + + name, err := l1Block.GasPayingTokenName(&bind.CallOpts{}) + require.NoError(t, err) + + symbol, err := l1Block.GasPayingTokenSymbol(&bind.CallOpts{}) + require.NoError(t, err) + + require.Equal(t, enabledExpectations.tokenAddress, token.Addr) + require.Equal(t, enabledExpectations.tokenDecimals, token.Decimals) + require.Equal(t, enabledExpectations.tokenName, name) + require.Equal(t, enabledExpectations.tokenSymbol, symbol) +} + +func checkWETHTokenNameAndSymbol(t *testing.T, gto gasTokenTestOpts, expectations cgtTestExpectations) { + l2Client := gto.sys.NodeClient("sequencer") + + // Check name and symbol in WETH predeploy + weth, err := bindings.NewWETH(predeploys.WETHAddr, l2Client) + require.NoError(t, err) + + name, err := weth.Name(&bind.CallOpts{}) + require.NoError(t, err) + + symbol, err := weth.Symbol(&bind.CallOpts{}) + require.NoError(t, err) + + require.Equal(t, "Wrapped "+expectations.tokenName, name) + require.Equal(t, "W"+expectations.tokenSymbol, symbol) +} From c8edbe24cb9579ee9dd7b0f8d164647d6af3a056 Mon Sep 17 00:00:00 2001 From: Maurelian Date: Tue, 15 Oct 2024 13:43:04 -0400 Subject: [PATCH 29/31] test: Remove exceptions for unlabelled fdg contracts (#12462) * test: Remove exceptions for unlabelled fdg contracts * fix: Unused import --- .../contracts-bedrock/scripts/Artifacts.s.sol | 7 ------- .../test/vendor/Initializable.t.sol | 16 +++------------- 2 files changed, 3 insertions(+), 20 deletions(-) diff --git a/packages/contracts-bedrock/scripts/Artifacts.s.sol b/packages/contracts-bedrock/scripts/Artifacts.s.sol index d43bd6915455..ff63f6bb3762 100644 --- a/packages/contracts-bedrock/scripts/Artifacts.s.sol +++ b/packages/contracts-bedrock/scripts/Artifacts.s.sol @@ -216,13 +216,6 @@ abstract contract Artifacts { /// @notice Returns the value of the internal `_initialized` storage slot for a given contract. function loadInitializedSlot(string memory _contractName) public returns (uint8 initialized_) { - // FaultDisputeGame and PermissionedDisputeGame are initializable but cannot be loaded with - // this function yet because they are not properly labeled in the deploy script. - // TODO: Remove this restriction once the deploy script is fixed. - if (LibString.eq(_contractName, "FaultDisputeGame") || LibString.eq(_contractName, "PermissionedDisputeGame")) { - revert UnsupportedInitializableContract(_contractName); - } - address contractAddress; // Check if the contract name ends with `Proxy` and, if so, get the implementation address if (LibString.endsWith(_contractName, "Proxy")) { diff --git a/packages/contracts-bedrock/test/vendor/Initializable.t.sol b/packages/contracts-bedrock/test/vendor/Initializable.t.sol index 3d1465704aff..17fb79339ca1 100644 --- a/packages/contracts-bedrock/test/vendor/Initializable.t.sol +++ b/packages/contracts-bedrock/test/vendor/Initializable.t.sol @@ -6,7 +6,7 @@ import { Bridge_Initializer } from "test/setup/Bridge_Initializer.sol"; // Scripts import { Executables } from "scripts/libraries/Executables.sol"; -import { ForgeArtifacts, StorageSlot } from "scripts/libraries/ForgeArtifacts.sol"; +import { ForgeArtifacts } from "scripts/libraries/ForgeArtifacts.sol"; import { Process } from "scripts/libraries/Process.sol"; // Libraries @@ -473,19 +473,9 @@ contract Initializer_Test is Bridge_Initializer { InitializeableContract memory _contract = contracts[i]; string memory name = _getRealContractName(_contract.name); - // Grab the value of the "initialized" storage slot. Must handle special case for the - // FaultDisputeGame and PermissionedDisputeGame contracts since these have a different - // name for the "initialized" storage slot and are currently not properly labeled in - // the deployment script. - // TODO: Update deployment script to properly label the dispute game contracts. + // Grab the value of the "initialized" storage slot. uint8 initializedSlotVal; - if (LibString.eq(name, "FaultDisputeGame") || LibString.eq(name, "PermissionedDisputeGame")) { - StorageSlot memory slot = ForgeArtifacts.getInitializedSlot(name); - bytes32 slotVal = vm.load(_contract.target, bytes32(vm.parseUint(slot.slot))); - initializedSlotVal = uint8((uint256(slotVal) >> (slot.offset * 8)) & 0xFF); - } else { - initializedSlotVal = deploy.loadInitializedSlot(name); - } + initializedSlotVal = deploy.loadInitializedSlot(name); // Assert that the contract is already initialized. assertTrue( From cb2066b0b961958189a2ae25ac1421fd157df387 Mon Sep 17 00:00:00 2001 From: AgusDuha <81362284+agusduha@users.noreply.github.com> Date: Tue, 15 Oct 2024 16:59:56 -0300 Subject: [PATCH 30/31] feat: introduce SuperchainERC20 redesign + ICrosschainERC20 (#12321) * feat: add superchain erc20 bridge (#61) * feat: add superchain erc20 bridge * fix: interfaces and versions * refactor: optimism superchain erc20 redesign (#62) * refactor: use oz upgradeable erc20 as dependency * chore: update interfaces * fix: tests based on changes * refactor: remove op as dependency * feat: add check for supererc20 bridge on modifier * chore: update tests and interfaces * chore: update stack vars name on test * chore: remove empty gitmodules file * chore: update superchain weth errors * test: add superchain erc20 bridge tests (#65) * test: add superchain erc20 bridge tests * test: add optimism superchain erc20 beacon tests * test: remove unnecessary test * test: tests fixes * test: tests fixes * chore: update missing bridge on natspec (#69) * chore: update missing bridge on natspec * fix: natspecs --------- Co-authored-by: agusduha * fix: remove superchain erc20 base (#70) * refactor: update isuperchainweth (#71) --------- Co-authored-by: agusduha * feat: rename mint/burn and add SuperchainERC20 (#74) * refactor: rename mint and burn functions on superchain erc20 * chore: rename optimism superchain erc20 to superchain erc20 * feat: create optimism superchain erc20 contract * chore: update natspec and errors * fix: superchain erc20 tests * refactor: make superchain erc20 abstract * refactor: move storage and erc20 metadata functions to implementation * chore: update interfaces * chore: update superchain erc20 events * fix: tests * fix: natspecs * fix: add semmver lock and snapshots * fix: remove unused imports * fix: natspecs --------- Co-authored-by: 0xDiscotech <131301107+0xDiscotech@users.noreply.github.com> * fix: refactor zero check (#76) * fix: pre pr * fix: semver natspec check failure (#79) * fix: semver natspec check failure * fix: ignore mock contracts in semver natspec script * fix: error message * feat: add crosschain erc20 interface (#80) * feat: add crosschain erc20 interface * fix: refactor interfaces * fix: superchain bridge natspec (#83) * fix: superchain weth natspec (#84) Co-authored-by: 0xng Co-authored-by: 0xParticle Co-authored-by: gotzenx <78360669+gotzenx@users.noreply.github.com> * fix: stop inheriting superchain interfaces (#85) * fix: stop inheriting superchain interfaces * fix: move events and erros into the implementation * fix: make superchainERC20 inherits from crosschainERC20 * fix: superchain bridge rename (#86) * fix: fee vault compiler error (#87) * fix: remove unused imports * fix: refactor common errors (#90) * fix: refactor common errors * fix: remove unused version * fix: reuse unauthorized error (#92) * fix: superchain erc20 factory conflicts * fix: rename crosschain functions (#94) --------- Co-authored-by: Disco <131301107+0xDiscotech@users.noreply.github.com> Co-authored-by: 0xng Co-authored-by: 0xParticle Co-authored-by: gotzenx <78360669+gotzenx@users.noreply.github.com> --- packages/contracts-bedrock/.gas-snapshot | 10 +- .../contracts-bedrock/scripts/Artifacts.s.sol | 2 + .../contracts-bedrock/scripts/L2Genesis.s.sol | 7 + .../scripts/checks/check-interfaces.sh | 3 +- .../scripts/checks/interfaces/main.go | 5 +- .../scripts/checks/semver-natspec/main.go | 7 +- packages/contracts-bedrock/semver-lock.json | 24 +- .../abi/OptimismSuperchainERC20.json | 154 ++++------- .../abi/OptimismSuperchainERC20Factory.json | 2 +- .../snapshots/abi/SuperchainTokenBridge.json | 166 ++++++++++++ .../snapshots/abi/SuperchainWETH.json | 9 +- .../storageLayout/SuperchainTokenBridge.json | 1 + .../src/L2/L2StandardBridgeInterop.sol | 25 +- .../src/L2/OptimismSuperchainERC20.sol | 54 ++-- .../src/L2/OptimismSuperchainERC20Factory.sol | 17 +- .../src/L2/SuperchainERC20.sol | 63 ++--- .../src/L2/SuperchainTokenBridge.sol | 93 +++++++ .../src/L2/SuperchainWETH.sol | 19 +- .../src/L2/interfaces/ICrosschainERC20.sol | 26 ++ .../interfaces/IL2StandardBridgeInterop.sol | 6 - .../interfaces/IMintableAndBurnableERC20.sol | 18 ++ .../interfaces/IOptimismSuperchainERC20.sol | 46 ++-- .../IOptimismSuperchainERC20Factory.sol | 2 - .../src/L2/interfaces/ISuperchainERC20.sol | 56 +--- .../L2/interfaces/ISuperchainTokenBridge.sol | 31 +++ .../src/L2/interfaces/ISuperchainWETH.sol | 61 +++-- .../src/libraries/Predeploys.sol | 7 +- .../src/libraries/errors/CommonErrors.sol | 3 + .../contracts-bedrock/test/L2/L2Genesis.t.sol | 4 +- .../test/L2/L2StandardBridgeInterop.t.sol | 23 +- .../test/L2/OptimismSuperchainERC20.t.sol | 249 ++++-------------- .../L2/OptimismSuperchainERC20Factory.t.sol | 68 ++--- .../test/L2/SuperchainERC20.t.sol | 217 ++++----------- .../test/L2/SuperchainTokenBridge.t.sol | 202 ++++++++++++++ .../test/L2/SuperchainWETH.t.sol | 7 +- .../OptimismSuperchainERC20.t.sol | 8 +- .../OptimismSuperchainERC20/PROPERTIES.md | 38 +-- .../fuzz/Protocol.guided.t.sol | 152 +---------- .../fuzz/Protocol.unguided.t.sol | 76 +----- .../handlers/Protocol.t.sol | 4 +- .../MockL2ToL2CrossDomainMessenger.t.sol | 6 +- .../test/invariants/SuperchainWETH.t.sol | 6 +- .../mocks/SuperchainERC20Implementation.sol | 16 ++ .../contracts-bedrock/test/setup/Setup.sol | 15 +- 44 files changed, 1007 insertions(+), 1001 deletions(-) create mode 100644 packages/contracts-bedrock/snapshots/abi/SuperchainTokenBridge.json create mode 100644 packages/contracts-bedrock/snapshots/storageLayout/SuperchainTokenBridge.json create mode 100644 packages/contracts-bedrock/src/L2/SuperchainTokenBridge.sol create mode 100644 packages/contracts-bedrock/src/L2/interfaces/ICrosschainERC20.sol create mode 100644 packages/contracts-bedrock/src/L2/interfaces/IMintableAndBurnableERC20.sol create mode 100644 packages/contracts-bedrock/src/L2/interfaces/ISuperchainTokenBridge.sol create mode 100644 packages/contracts-bedrock/test/L2/SuperchainTokenBridge.t.sol create mode 100644 packages/contracts-bedrock/test/mocks/SuperchainERC20Implementation.sol diff --git a/packages/contracts-bedrock/.gas-snapshot b/packages/contracts-bedrock/.gas-snapshot index da67af9f81fc..3157ce04da58 100644 --- a/packages/contracts-bedrock/.gas-snapshot +++ b/packages/contracts-bedrock/.gas-snapshot @@ -4,13 +4,13 @@ GasBenchMark_L1BlockInterop_SetValuesInterop:test_setL1BlockValuesInterop_benchm GasBenchMark_L1BlockInterop_SetValuesInterop_Warm:test_setL1BlockValuesInterop_benchmark() (gas: 5099) GasBenchMark_L1Block_SetValuesEcotone:test_setL1BlockValuesEcotone_benchmark() (gas: 158531) GasBenchMark_L1Block_SetValuesEcotone_Warm:test_setL1BlockValuesEcotone_benchmark() (gas: 7597) -GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_0() (gas: 369245) -GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_1() (gas: 2967385) -GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_0() (gas: 564368) -GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_1() (gas: 4076583) +GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_0() (gas: 369242) +GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_1() (gas: 2967382) +GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_0() (gas: 564356) +GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_1() (gas: 4076571) GasBenchMark_L1StandardBridge_Deposit:test_depositETH_benchmark_0() (gas: 467019) GasBenchMark_L1StandardBridge_Deposit:test_depositETH_benchmark_1() (gas: 3512723) -GasBenchMark_L1StandardBridge_Finalize:test_finalizeETHWithdrawal_benchmark() (gas: 72618) +GasBenchMark_L1StandardBridge_Finalize:test_finalizeETHWithdrawal_benchmark() (gas: 72621) GasBenchMark_L2OutputOracle:test_proposeL2Output_benchmark() (gas: 92973) GasBenchMark_OptimismPortal:test_depositTransaction_benchmark() (gas: 68357) GasBenchMark_OptimismPortal:test_depositTransaction_benchmark_1() (gas: 68921) diff --git a/packages/contracts-bedrock/scripts/Artifacts.s.sol b/packages/contracts-bedrock/scripts/Artifacts.s.sol index ff63f6bb3762..9846bc1ac4e7 100644 --- a/packages/contracts-bedrock/scripts/Artifacts.s.sol +++ b/packages/contracts-bedrock/scripts/Artifacts.s.sol @@ -157,6 +157,8 @@ abstract contract Artifacts { return payable(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY); } else if (digest == keccak256(bytes("OptimismSuperchainERC20Beacon"))) { return payable(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_BEACON); + } else if (digest == keccak256(bytes("SuperchainTokenBridge"))) { + return payable(Predeploys.SUPERCHAIN_TOKEN_BRIDGE); } return payable(address(0)); } diff --git a/packages/contracts-bedrock/scripts/L2Genesis.s.sol b/packages/contracts-bedrock/scripts/L2Genesis.s.sol index dd685d546ed7..2dd0e906dbeb 100644 --- a/packages/contracts-bedrock/scripts/L2Genesis.s.sol +++ b/packages/contracts-bedrock/scripts/L2Genesis.s.sol @@ -277,6 +277,7 @@ contract L2Genesis is Deployer { setETHLiquidity(); // 25 setOptimismSuperchainERC20Factory(); // 26 setOptimismSuperchainERC20Beacon(); // 27 + setSuperchainTokenBridge(); // 28 } } @@ -601,6 +602,12 @@ contract L2Genesis is Deployer { vm.resetNonce(address(beacon)); } + /// @notice This predeploy is following the safety invariant #1. + /// This contract has no initializer. + function setSuperchainTokenBridge() internal { + _setImplementationCode(Predeploys.SUPERCHAIN_TOKEN_BRIDGE); + } + /// @notice Sets all the preinstalls. function setPreinstalls() public { address tmpSetPreinstalls = address(uint160(uint256(keccak256("SetPreinstalls")))); diff --git a/packages/contracts-bedrock/scripts/checks/check-interfaces.sh b/packages/contracts-bedrock/scripts/checks/check-interfaces.sh index c37783a08b90..a2cda470d2a8 100755 --- a/packages/contracts-bedrock/scripts/checks/check-interfaces.sh +++ b/packages/contracts-bedrock/scripts/checks/check-interfaces.sh @@ -62,15 +62,14 @@ EXCLUDE_CONTRACTS=( "ILegacyMintableERC20" "IOptimismMintableERC20" "IOptimismMintableERC721" - "IOptimismSuperchainERC20" # Doesn't start with "I" - "MintableAndBurnable" "KontrolCheatsBase" # Currently inherit from interface, needs to be fixed. "IWETH" "IDelayedWETH" + "ISuperchainWETH" "IL2ToL2CrossDomainMessenger" "ICrossL2Inbox" "ISystemConfigInterop" diff --git a/packages/contracts-bedrock/scripts/checks/interfaces/main.go b/packages/contracts-bedrock/scripts/checks/interfaces/main.go index 004f1d5c03cd..ab07dbb4d84b 100644 --- a/packages/contracts-bedrock/scripts/checks/interfaces/main.go +++ b/packages/contracts-bedrock/scripts/checks/interfaces/main.go @@ -27,9 +27,8 @@ var excludeContracts = []string{ // TODO: Interfaces that need to be fixed "IInitializable", "IPreimageOracle", "ILegacyMintableERC20", "IOptimismMintableERC20", - "IOptimismMintableERC721", "IOptimismSuperchainERC20", "MintableAndBurnable", - "KontrolCheatsBase", "IWETH", "IDelayedWETH", "IL2ToL2CrossDomainMessenger", - "ICrossL2Inbox", "ISystemConfigInterop", "IResolvedDelegateProxy", + "IOptimismMintableERC721", "KontrolCheatsBase", "IWETH", "IDelayedWETH", "ISuperchainWETH", + "IL2ToL2CrossDomainMessenger", "ICrossL2Inbox", "ISystemConfigInterop", "IResolvedDelegateProxy", } type ContractDefinition struct { diff --git a/packages/contracts-bedrock/scripts/checks/semver-natspec/main.go b/packages/contracts-bedrock/scripts/checks/semver-natspec/main.go index d1e2153c02ef..1be082dda763 100644 --- a/packages/contracts-bedrock/scripts/checks/semver-natspec/main.go +++ b/packages/contracts-bedrock/scripts/checks/semver-natspec/main.go @@ -129,9 +129,14 @@ func run() error { return } + // Skip mock contracts + if strings.HasPrefix(contractName, "Mock") { + return + } + contractPath := contractFiles[contractName] if contractPath == "" { - fail("%s: Source file not found", contractName) + fail("%s: Source file not found (For test mock contracts, prefix the name with 'Mock' to ignore this warning)", contractName) return } diff --git a/packages/contracts-bedrock/semver-lock.json b/packages/contracts-bedrock/semver-lock.json index f058f3b4758f..9488435c1ca3 100644 --- a/packages/contracts-bedrock/semver-lock.json +++ b/packages/contracts-bedrock/semver-lock.json @@ -96,8 +96,8 @@ "sourceCodeHash": "0xb55e58b5d4912edf05026878a5f5ac8019372212ed2a77843775d595fbf51b84" }, "src/L2/L2StandardBridgeInterop.sol": { - "initCodeHash": "0x9bc28e8511a4593362c2517ff90b26f9c1ecee382cce3950b47ca08892a872ef", - "sourceCodeHash": "0x6c814f4536d9fb8f384ed2195957f868abd15252e36d6dd243f3d60349a61994" + "initCodeHash": "0xd43d07c2ba5a73af56181c0221c28f3b495851b03cf8e35f9b009857bb9538a6", + "sourceCodeHash": "0x36087332a4365ee172eed8fa35aa48e804b08279e97332058a588c2d4ae30c6b" }, "src/L2/L2ToL1MessagePasser.sol": { "initCodeHash": "0x13fe3729beb9ed966c97bef09acb9fe5043fe651d453145073d05f2567fa988d", @@ -108,24 +108,32 @@ "sourceCodeHash": "0xfea53344596d735eff3be945ed1300dc75a6f8b7b2c02c0043af5b0036f5f239" }, "src/L2/OptimismSuperchainERC20.sol": { - "initCodeHash": "0x965af580568bad2b47d04c6ea536490aa263e9fcb5fb43e6c8bc00929fda3df5", - "sourceCodeHash": "0x9de349519900b1051f45d507b2fac1cf3f3ae8e2cfb1ceb56875a7ace1cb6ab8" + "initCodeHash": "0xd5c84e45746fd741d541a917ddc1cc0c7043c6b21d5c18040d4bc999d6a7b2db", + "sourceCodeHash": "0xf32130f0b46333daba062c50ff6dcfadce1f177ff753bed2374d499ea9c2d98a" }, "src/L2/OptimismSuperchainERC20Beacon.sol": { "initCodeHash": "0x99ce8095b23c124850d866cbc144fee6cee05dbc6bb5d83acadfe00b90cf42c7", "sourceCodeHash": "0x5e58b7c867fafa49fe39d68d83875425e9cf94f05f2835bdcdaa08fc8bc6b68e" }, "src/L2/OptimismSuperchainERC20Factory.sol": { - "initCodeHash": "0x43ec413140b05bfb83ec453b0d4f82b33a2d560bf8c76405d08de17565b87053", - "sourceCodeHash": "0x1e02d78a4e7ee93a07f7af7a78fe1773d0e87711f23a4ccd10a8692b47644a34" + "initCodeHash": "0x18a362c57f08b611db98dfde96121385e938f995c84e3547c1c03fd49f9db2fd", + "sourceCodeHash": "0x450cd89d0aae7bbc85ff57a14a6d3468c24c6743f25943f6d895d34b1456c456" }, "src/L2/SequencerFeeVault.sol": { "initCodeHash": "0xcaadbf08057b5d47f7704257e9385a29e42a7a08c818646d109c5952d3d35218", "sourceCodeHash": "0x05bbc6039e5a9ff38987e7b9b89c69e2ee8aa4b7ca20dd002ea1bbd3d70f27f3" }, + "src/L2/SuperchainERC20.sol": { + "initCodeHash": "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", + "sourceCodeHash": "0x75d061633a141af11a19b86e599a1725dfae8d245dcddfb6bb244a50d5e53f96" + }, + "src/L2/SuperchainTokenBridge.sol": { + "initCodeHash": "0x07fc1d495928d9c13bd945a049d17e1d105d01c2082a7719e5d18cbc0e1c7d9e", + "sourceCodeHash": "0xaf2458e48dcadcafa8940cde7368549eae2280eef91247600d864ddac20f5d82" + }, "src/L2/SuperchainWETH.sol": { - "initCodeHash": "0x4ccd25f37a816205bc26f8532afa66e02f2b36ca7b7404d0fa48a4313ed16f0c", - "sourceCodeHash": "0xd186614f1515fa3ba2f43e401e639bfa3159603954e39a51769e9b57ad19a3fd" + "initCodeHash": "0x50f6ea9bfe650fcf792e98e44b1bf66c036fd0e6d4b753da680253d7d8609816", + "sourceCodeHash": "0x82d03262decf52d5954d40bca8703f96a0f3ba7accf6c1d75292856c2f34cf8f" }, "src/L2/WETH.sol": { "initCodeHash": "0xfb253765520690623f177941c2cd9eba23e4c6d15063bccdd5e98081329d8956", diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json b/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json index 6eb57764a8cb..f1b7f83e3b53 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json @@ -102,6 +102,42 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "_from", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "crosschainBurn", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "crosschainMint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "decimals", @@ -236,29 +272,6 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [ - { - "internalType": "address", - "name": "_from", - "type": "address" - }, - { - "internalType": "address", - "name": "_to", - "type": "address" - }, - { - "internalType": "uint256", - "name": "_amount", - "type": "uint256" - } - ], - "name": "relayERC20", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [], "name": "remoteToken", @@ -272,29 +285,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [ - { - "internalType": "address", - "name": "_to", - "type": "address" - }, - { - "internalType": "uint256", - "name": "_amount", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "_chainId", - "type": "uint256" - } - ], - "name": "sendERC20", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [ { @@ -437,7 +427,7 @@ { "indexed": true, "internalType": "address", - "name": "account", + "name": "from", "type": "address" }, { @@ -450,26 +440,13 @@ "name": "Burn", "type": "event" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint64", - "name": "version", - "type": "uint64" - } - ], - "name": "Initialized", - "type": "event" - }, { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "address", - "name": "account", + "name": "from", "type": "address" }, { @@ -479,18 +456,12 @@ "type": "uint256" } ], - "name": "Mint", + "name": "CrosschainBurnt", "type": "event" }, { "anonymous": false, "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "from", - "type": "address" - }, { "indexed": true, "internalType": "address", @@ -502,26 +473,27 @@ "internalType": "uint256", "name": "amount", "type": "uint256" - }, + } + ], + "name": "CrosschainMinted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ { "indexed": false, - "internalType": "uint256", - "name": "source", - "type": "uint256" + "internalType": "uint64", + "name": "version", + "type": "uint64" } ], - "name": "RelayERC20", + "name": "Initialized", "type": "event" }, { "anonymous": false, "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "from", - "type": "address" - }, { "indexed": true, "internalType": "address", @@ -533,15 +505,9 @@ "internalType": "uint256", "name": "amount", "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "destination", - "type": "uint256" } ], - "name": "SendERC20", + "name": "Mint", "type": "event" }, { @@ -579,11 +545,6 @@ "name": "AllowanceUnderflow", "type": "error" }, - { - "inputs": [], - "name": "CallerNotL2ToL2CrossDomainMessenger", - "type": "error" - }, { "inputs": [], "name": "InsufficientAllowance", @@ -594,11 +555,6 @@ "name": "InsufficientBalance", "type": "error" }, - { - "inputs": [], - "name": "InvalidCrossDomainSender", - "type": "error" - }, { "inputs": [], "name": "InvalidInitialization", @@ -616,17 +572,17 @@ }, { "inputs": [], - "name": "OnlyBridge", + "name": "PermitExpired", "type": "error" }, { "inputs": [], - "name": "PermitExpired", + "name": "TotalSupplyOverflow", "type": "error" }, { "inputs": [], - "name": "TotalSupplyOverflow", + "name": "Unauthorized", "type": "error" }, { diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20Factory.json b/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20Factory.json index fdf212052c0e..f6020300b036 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20Factory.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20Factory.json @@ -37,7 +37,7 @@ "inputs": [ { "internalType": "address", - "name": "_superchainToken", + "name": "_localToken", "type": "address" } ], diff --git a/packages/contracts-bedrock/snapshots/abi/SuperchainTokenBridge.json b/packages/contracts-bedrock/snapshots/abi/SuperchainTokenBridge.json new file mode 100644 index 000000000000..36358db1b307 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/abi/SuperchainTokenBridge.json @@ -0,0 +1,166 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "address", + "name": "_from", + "type": "address" + }, + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "relayERC20", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_chainId", + "type": "uint256" + } + ], + "name": "sendERC20", + "outputs": [ + { + "internalType": "bytes32", + "name": "msgHash_", + "type": "bytes32" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "source", + "type": "uint256" + } + ], + "name": "RelayERC20", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "destination", + "type": "uint256" + } + ], + "name": "SendERC20", + "type": "event" + }, + { + "inputs": [], + "name": "InvalidCrossDomainSender", + "type": "error" + }, + { + "inputs": [], + "name": "Unauthorized", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddress", + "type": "error" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/SuperchainWETH.json b/packages/contracts-bedrock/snapshots/abi/SuperchainWETH.json index 600e0e6b64f7..b0b86ea7c8ce 100644 --- a/packages/contracts-bedrock/snapshots/abi/SuperchainWETH.json +++ b/packages/contracts-bedrock/snapshots/abi/SuperchainWETH.json @@ -410,12 +410,17 @@ }, { "inputs": [], - "name": "NotCustomGasToken", + "name": "CallerNotL2ToL2CrossDomainMessenger", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidCrossDomainSender", "type": "error" }, { "inputs": [], - "name": "Unauthorized", + "name": "NotCustomGasToken", "type": "error" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/SuperchainTokenBridge.json b/packages/contracts-bedrock/snapshots/storageLayout/SuperchainTokenBridge.json new file mode 100644 index 000000000000..0637a088a01e --- /dev/null +++ b/packages/contracts-bedrock/snapshots/storageLayout/SuperchainTokenBridge.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/L2/L2StandardBridgeInterop.sol b/packages/contracts-bedrock/src/L2/L2StandardBridgeInterop.sol index be6c9e4c878d..eb25a406ede7 100644 --- a/packages/contracts-bedrock/src/L2/L2StandardBridgeInterop.sol +++ b/packages/contracts-bedrock/src/L2/L2StandardBridgeInterop.sol @@ -11,6 +11,7 @@ import { Predeploys } from "src/libraries/Predeploys.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import { IOptimismERC20Factory } from "src/L2/interfaces/IOptimismERC20Factory.sol"; +import { IMintableAndBurnableERC20 } from "src/L2/interfaces/IMintableAndBurnableERC20.sol"; /// @notice Thrown when the decimals of the tokens are not the same. error InvalidDecimals(); @@ -18,26 +19,18 @@ error InvalidDecimals(); /// @notice Thrown when the legacy address is not found in the OptimismMintableERC20Factory. error InvalidLegacyERC20Address(); -/// @notice Thrown when the SuperchainERC20 address is not found in the SuperchainERC20Factory. +/// @notice Thrown when the OptimismSuperchainERC20 address is not found in the OptimismSuperchainERC20Factory. error InvalidSuperchainERC20Address(); /// @notice Thrown when the remote addresses of the tokens are not the same. error InvalidTokenPair(); -/// TODO: Define a better naming convention for this interface. -/// @notice Interface for minting and burning tokens in the L2StandardBridge. -/// Used for StandardL2ERC20, OptimismMintableERC20 and OptimismSuperchainERC20. -interface MintableAndBurnable is IERC20 { - function mint(address, uint256) external; - function burn(address, uint256) external; -} - /// @custom:proxied true /// @custom:predeploy 0x4200000000000000000000000000000000000010 /// @title L2StandardBridgeInterop /// @notice The L2StandardBridgeInterop is an extension of the L2StandardBridge that allows for /// the conversion of tokens between legacy tokens (OptimismMintableERC20 or StandardL2ERC20) -/// and SuperchainERC20 tokens. +/// and OptimismSuperchainERC20 tokens. contract L2StandardBridgeInterop is L2StandardBridge { /// @notice Emitted when a conversion is made. /// @param from The token being converted from. @@ -47,9 +40,9 @@ contract L2StandardBridgeInterop is L2StandardBridge { event Converted(address indexed from, address indexed to, address indexed caller, uint256 amount); /// @notice Semantic version. - /// @custom:semver +interop + /// @custom:semver +interop-beta.1 function version() public pure override returns (string memory) { - return string.concat(super.version(), "+interop"); + return string.concat(super.version(), "+interop-beta.1"); } /// @notice Converts `amount` of `from` token to `to` token. @@ -59,8 +52,8 @@ contract L2StandardBridgeInterop is L2StandardBridge { function convert(address _from, address _to, uint256 _amount) external { _validatePair(_from, _to); - MintableAndBurnable(_from).burn(msg.sender, _amount); - MintableAndBurnable(_to).mint(msg.sender, _amount); + IMintableAndBurnableERC20(_from).burn(msg.sender, _amount); + IMintableAndBurnableERC20(_to).mint(msg.sender, _amount); emit Converted(_from, _to, msg.sender, _amount); } @@ -82,14 +75,14 @@ contract L2StandardBridgeInterop is L2StandardBridge { /// @notice Validates that the tokens are deployed by the correct factory. /// @param _legacyAddr The legacy token address (OptimismMintableERC20 or StandardL2ERC20). - /// @param _superAddr The SuperchainERC20 address. + /// @param _superAddr The OptimismSuperchainERC20 address. function _validateFactories(address _legacyAddr, address _superAddr) internal view { // 2. Valid legacy check address _legacyRemoteToken = IOptimismERC20Factory(Predeploys.OPTIMISM_MINTABLE_ERC20_FACTORY).deployments(_legacyAddr); if (_legacyRemoteToken == address(0)) revert InvalidLegacyERC20Address(); - // 3. Valid SuperchainERC20 check + // 3. Valid OptimismSuperchainERC20 check address _superRemoteToken = IOptimismERC20Factory(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY).deployments(_superAddr); if (_superRemoteToken == address(0)) revert InvalidSuperchainERC20Address(); diff --git a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol index 6b500e2b4dfc..6e8ef9057325 100644 --- a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol @@ -1,34 +1,31 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -import { IOptimismSuperchainERC20Extension } from "src/L2/interfaces/IOptimismSuperchainERC20.sol"; -import { ISemver } from "src/universal/interfaces/ISemver.sol"; +import { IOptimismSuperchainERC20 } from "src/L2/interfaces/IOptimismSuperchainERC20.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; -import { ERC20 } from "@solady/tokens/ERC20.sol"; +import { ERC165 } from "@openzeppelin/contracts-v5/utils/introspection/ERC165.sol"; import { SuperchainERC20 } from "src/L2/SuperchainERC20.sol"; import { Initializable } from "@openzeppelin/contracts-v5/proxy/utils/Initializable.sol"; -import { ERC165 } from "@openzeppelin/contracts-v5/utils/introspection/ERC165.sol"; - -/// @notice Thrown when attempting to mint or burn tokens and the function caller is not the StandardBridge. -error OnlyBridge(); +import { ZeroAddress, Unauthorized } from "src/libraries/errors/CommonErrors.sol"; /// @custom:proxied true /// @title OptimismSuperchainERC20 /// @notice OptimismSuperchainERC20 is a standard extension of the base ERC20 token contract that unifies ERC20 token /// bridging to make it fungible across the Superchain. This construction allows the L2StandardBridge to burn -/// and mint tokens. This makes it possible to convert a valid OptimismMintableERC20 token to a SuperchainERC20 -/// token, turning it fungible and interoperable across the superchain. Likewise, it also enables the inverse -/// conversion path. +/// and mint tokens. This makes it possible to convert a valid OptimismMintableERC20 token to a +/// OptimismSuperchainERC20 token, turning it fungible and interoperable across the superchain. Likewise, it +/// also enables the inverse conversion path. /// Moreover, it builds on top of the L2ToL2CrossDomainMessenger for both replay protection and domain binding. -contract OptimismSuperchainERC20 is - IOptimismSuperchainERC20Extension, - SuperchainERC20, - ISemver, - Initializable, - ERC165 -{ - /// @notice Address of the StandardBridge Predeploy. - address internal constant BRIDGE = Predeploys.L2_STANDARD_BRIDGE; +contract OptimismSuperchainERC20 is SuperchainERC20, Initializable, ERC165 { + /// @notice Emitted whenever tokens are minted for an account. + /// @param to Address of the account tokens are being minted for. + /// @param amount Amount of tokens minted. + event Mint(address indexed to, uint256 amount); + + /// @notice Emitted whenever tokens are burned from an account. + /// @param from Address of the account tokens are being burned from. + /// @param amount Amount of tokens burned. + event Burn(address indexed from, uint256 amount); /// @notice Storage slot that the OptimismSuperchainERC20Metadata struct is stored at. /// keccak256(abi.encode(uint256(keccak256("optimismSuperchainERC20.metadata")) - 1)) & ~bytes32(uint256(0xff)); @@ -55,15 +52,15 @@ contract OptimismSuperchainERC20 is } } - /// @notice A modifier that only allows the bridge to call - modifier onlyBridge() { - if (msg.sender != BRIDGE) revert OnlyBridge(); + /// @notice A modifier that only allows the L2StandardBridge to call + modifier onlyL2StandardBridge() { + if (msg.sender != Predeploys.L2_STANDARD_BRIDGE) revert Unauthorized(); _; } /// @notice Semantic version. - /// @custom:semver 1.0.0-beta.5 - string public constant version = "1.0.0-beta.5"; + /// @custom:semver 1.0.0-beta.6 + string public constant override version = "1.0.0-beta.6"; /// @notice Constructs the OptimismSuperchainERC20 contract. constructor() { @@ -94,7 +91,7 @@ contract OptimismSuperchainERC20 is /// @notice Allows the L2StandardBridge to mint tokens. /// @param _to Address to mint tokens to. /// @param _amount Amount of tokens to mint. - function mint(address _to, uint256 _amount) external virtual onlyBridge { + function mint(address _to, uint256 _amount) external virtual onlyL2StandardBridge { if (_to == address(0)) revert ZeroAddress(); _mint(_to, _amount); @@ -105,7 +102,7 @@ contract OptimismSuperchainERC20 is /// @notice Allows the L2StandardBridge to burn tokens. /// @param _from Address to burn tokens from. /// @param _amount Amount of tokens to burn. - function burn(address _from, uint256 _amount) external virtual onlyBridge { + function burn(address _from, uint256 _amount) external virtual onlyL2StandardBridge { if (_from == address(0)) revert ZeroAddress(); _burn(_from, _amount); @@ -114,7 +111,7 @@ contract OptimismSuperchainERC20 is } /// @notice Returns the address of the corresponding version of this token on the remote chain. - function remoteToken() public view override returns (address) { + function remoteToken() public view returns (address) { return _getStorage().remoteToken; } @@ -142,7 +139,6 @@ contract OptimismSuperchainERC20 is /// @param _interfaceId Interface ID to check. /// @return Whether or not the interface is supported by this contract. function supportsInterface(bytes4 _interfaceId) public view virtual override returns (bool) { - return - _interfaceId == type(IOptimismSuperchainERC20Extension).interfaceId || super.supportsInterface(_interfaceId); + return _interfaceId == type(IOptimismSuperchainERC20).interfaceId || super.supportsInterface(_interfaceId); } } diff --git a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20Factory.sol b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20Factory.sol index d144f4a8b505..454e3b455d62 100644 --- a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20Factory.sol +++ b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20Factory.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -import { IOptimismERC20Factory } from "src/L2/interfaces/IOptimismERC20Factory.sol"; import { ISemver } from "src/universal/interfaces/ISemver.sol"; import { OptimismSuperchainERC20 } from "src/L2/OptimismSuperchainERC20.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; @@ -13,13 +12,9 @@ import { CREATE3 } from "@rari-capital/solmate/src/utils/CREATE3.sol"; /// @title OptimismSuperchainERC20Factory /// @notice OptimismSuperchainERC20Factory is a factory contract that deploys OptimismSuperchainERC20 Beacon Proxies /// using CREATE3. -contract OptimismSuperchainERC20Factory is IOptimismERC20Factory, ISemver { - /// @notice Mapping of the deployed OptimismSuperchainERC20 to the remote token address. - /// This is used to keep track of the token deployments. - mapping(address _superchainToken => address remoteToken_) public deployments; - +contract OptimismSuperchainERC20Factory is ISemver { /// @notice Emitted when an OptimismSuperchainERC20 is deployed. - /// @param superchainToken Address of the SuperchainERC20 deployment. + /// @param superchainToken Address of the OptimismSuperchainERC20 deployment. /// @param remoteToken Address of the corresponding token on the remote chain. /// @param deployer Address of the account that deployed the token. event OptimismSuperchainERC20Created( @@ -27,8 +22,12 @@ contract OptimismSuperchainERC20Factory is IOptimismERC20Factory, ISemver { ); /// @notice Semantic version. - /// @custom:semver 1.0.0-beta.3 - string public constant version = "1.0.0-beta.3"; + /// @custom:semver 1.0.0-beta.4 + string public constant version = "1.0.0-beta.4"; + + /// @notice Mapping of the deployed OptimismSuperchainERC20 to the remote token address. + /// This is used to keep track of the token deployments. + mapping(address _localToken => address remoteToken_) public deployments; /// @notice Deploys a OptimismSuperchainERC20 Beacon Proxy using CREATE3. /// @param _remoteToken Address of the remote token. diff --git a/packages/contracts-bedrock/src/L2/SuperchainERC20.sol b/packages/contracts-bedrock/src/L2/SuperchainERC20.sol index e20b375ff891..9ead2645828b 100644 --- a/packages/contracts-bedrock/src/L2/SuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/SuperchainERC20.sol @@ -1,49 +1,44 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -import { ISuperchainERC20Extensions, ISuperchainERC20Errors } from "src/L2/interfaces/ISuperchainERC20.sol"; -import { ERC20 } from "@solady/tokens/ERC20.sol"; -import { IL2ToL2CrossDomainMessenger } from "src/L2/interfaces/IL2ToL2CrossDomainMessenger.sol"; +import { ICrosschainERC20 } from "src/L2/interfaces/ICrosschainERC20.sol"; +import { ISemver } from "src/universal/interfaces/ISemver.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; +import { ERC20 } from "@solady/tokens/ERC20.sol"; +import { Unauthorized } from "src/libraries/errors/CommonErrors.sol"; /// @title SuperchainERC20 /// @notice SuperchainERC20 is a standard extension of the base ERC20 token contract that unifies ERC20 token -/// bridging to make it fungible across the Superchain. It builds on top of the L2ToL2CrossDomainMessenger for -/// both replay protection and domain binding. -abstract contract SuperchainERC20 is ISuperchainERC20Extensions, ISuperchainERC20Errors, ERC20 { - /// @notice Address of the L2ToL2CrossDomainMessenger Predeploy. - address internal constant MESSENGER = Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER; - - /// @notice Sends tokens to some target address on another chain. - /// @param _to Address to send tokens to. - /// @param _amount Amount of tokens to send. - /// @param _chainId Chain ID of the destination chain. - function sendERC20(address _to, uint256 _amount, uint256 _chainId) external virtual { - if (_to == address(0)) revert ZeroAddress(); - - _burn(msg.sender, _amount); - - bytes memory _message = abi.encodeCall(this.relayERC20, (msg.sender, _to, _amount)); - IL2ToL2CrossDomainMessenger(MESSENGER).sendMessage(_chainId, address(this), _message); - - emit SendERC20(msg.sender, _to, _amount, _chainId); +/// bridging to make it fungible across the Superchain. This construction allows the SuperchainTokenBridge to +/// burn and mint tokens. +abstract contract SuperchainERC20 is ERC20, ICrosschainERC20, ISemver { + /// @notice A modifier that only allows the SuperchainTokenBridge to call + modifier onlySuperchainTokenBridge() { + if (msg.sender != Predeploys.SUPERCHAIN_TOKEN_BRIDGE) revert Unauthorized(); + _; } - /// @notice Relays tokens received from another chain. - /// @param _from Address of the msg.sender of sendERC20 on the source chain. - /// @param _to Address to relay tokens to. - /// @param _amount Amount of tokens to relay. - function relayERC20(address _from, address _to, uint256 _amount) external virtual { - if (msg.sender != MESSENGER) revert CallerNotL2ToL2CrossDomainMessenger(); + /// @notice Semantic version. + /// @custom:semver 1.0.0-beta.1 + function version() external view virtual returns (string memory) { + return "1.0.0-beta.1"; + } - if (IL2ToL2CrossDomainMessenger(MESSENGER).crossDomainMessageSender() != address(this)) { - revert InvalidCrossDomainSender(); - } + /// @notice Allows the SuperchainTokenBridge to mint tokens. + /// @param _to Address to mint tokens to. + /// @param _amount Amount of tokens to mint. + function crosschainMint(address _to, uint256 _amount) external onlySuperchainTokenBridge { + _mint(_to, _amount); - uint256 source = IL2ToL2CrossDomainMessenger(MESSENGER).crossDomainMessageSource(); + emit CrosschainMinted(_to, _amount); + } - _mint(_to, _amount); + /// @notice Allows the SuperchainTokenBridge to burn tokens. + /// @param _from Address to burn tokens from. + /// @param _amount Amount of tokens to burn. + function crosschainBurn(address _from, uint256 _amount) external onlySuperchainTokenBridge { + _burn(_from, _amount); - emit RelayERC20(_from, _to, _amount, source); + emit CrosschainBurnt(_from, _amount); } } diff --git a/packages/contracts-bedrock/src/L2/SuperchainTokenBridge.sol b/packages/contracts-bedrock/src/L2/SuperchainTokenBridge.sol new file mode 100644 index 000000000000..1dcc233638f5 --- /dev/null +++ b/packages/contracts-bedrock/src/L2/SuperchainTokenBridge.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +// Libraries +import { Predeploys } from "src/libraries/Predeploys.sol"; +import { ZeroAddress, Unauthorized } from "src/libraries/errors/CommonErrors.sol"; + +// Interfaces +import { ISuperchainERC20 } from "src/L2/interfaces/ISuperchainERC20.sol"; +import { IL2ToL2CrossDomainMessenger } from "src/L2/interfaces/IL2ToL2CrossDomainMessenger.sol"; + +/// @custom:proxied true +/// @custom:predeploy 0x4200000000000000000000000000000000000028 +/// @title SuperchainTokenBridge +/// @notice The SuperchainTokenBridge allows for the bridging of ERC20 tokens to make them fungible across the +/// Superchain. It builds on top of the L2ToL2CrossDomainMessenger for both replay protection and domain +/// binding. +contract SuperchainTokenBridge { + /// @notice Thrown when attempting to relay a message and the cross domain message sender is not the + /// SuperchainTokenBridge. + error InvalidCrossDomainSender(); + + /// @notice Emitted when tokens are sent from one chain to another. + /// @param token Address of the token sent. + /// @param from Address of the sender. + /// @param to Address of the recipient. + /// @param amount Number of tokens sent. + /// @param destination Chain ID of the destination chain. + event SendERC20( + address indexed token, address indexed from, address indexed to, uint256 amount, uint256 destination + ); + + /// @notice Emitted whenever tokens are successfully relayed on this chain. + /// @param token Address of the token relayed. + /// @param from Address of the msg.sender of sendERC20 on the source chain. + /// @param to Address of the recipient. + /// @param amount Amount of tokens relayed. + /// @param source Chain ID of the source chain. + event RelayERC20(address indexed token, address indexed from, address indexed to, uint256 amount, uint256 source); + + /// @notice Address of the L2ToL2CrossDomainMessenger Predeploy. + address internal constant MESSENGER = Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER; + + /// @notice Semantic version. + /// @custom:semver 1.0.0-beta.1 + string public constant version = "1.0.0-beta.1"; + + /// @notice Sends tokens to a target address on another chain. + /// @dev Tokens are burned on the source chain. + /// @param _token Token to send. + /// @param _to Address to send tokens to. + /// @param _amount Amount of tokens to send. + /// @param _chainId Chain ID of the destination chain. + /// @return msgHash_ Hash of the message sent. + function sendERC20( + address _token, + address _to, + uint256 _amount, + uint256 _chainId + ) + external + returns (bytes32 msgHash_) + { + if (_to == address(0)) revert ZeroAddress(); + + ISuperchainERC20(_token).crosschainBurn(msg.sender, _amount); + + bytes memory message = abi.encodeCall(this.relayERC20, (_token, msg.sender, _to, _amount)); + msgHash_ = IL2ToL2CrossDomainMessenger(MESSENGER).sendMessage(_chainId, address(this), message); + + emit SendERC20(_token, msg.sender, _to, _amount, _chainId); + } + + /// @notice Relays tokens received from another chain. + /// @dev Tokens are minted on the destination chain. + /// @param _token Token to relay. + /// @param _from Address of the msg.sender of sendERC20 on the source chain. + /// @param _to Address to relay tokens to. + /// @param _amount Amount of tokens to relay. + function relayERC20(address _token, address _from, address _to, uint256 _amount) external { + if (msg.sender != MESSENGER) revert Unauthorized(); + + if (IL2ToL2CrossDomainMessenger(MESSENGER).crossDomainMessageSender() != address(this)) { + revert InvalidCrossDomainSender(); + } + + uint256 source = IL2ToL2CrossDomainMessenger(MESSENGER).crossDomainMessageSource(); + + ISuperchainERC20(_token).crosschainMint(_to, _amount); + + emit RelayERC20(_token, _from, _to, _amount, source); + } +} diff --git a/packages/contracts-bedrock/src/L2/SuperchainWETH.sol b/packages/contracts-bedrock/src/L2/SuperchainWETH.sol index 4788b70b1bc9..3706a511cdeb 100644 --- a/packages/contracts-bedrock/src/L2/SuperchainWETH.sol +++ b/packages/contracts-bedrock/src/L2/SuperchainWETH.sol @@ -5,24 +5,25 @@ pragma solidity 0.8.15; import { WETH98 } from "src/universal/WETH98.sol"; // Libraries -import { Unauthorized, NotCustomGasToken } from "src/libraries/errors/CommonErrors.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; // Interfaces import { ISemver } from "src/universal/interfaces/ISemver.sol"; import { IL2ToL2CrossDomainMessenger } from "src/L2/interfaces/IL2ToL2CrossDomainMessenger.sol"; -import { ISuperchainERC20Extensions } from "src/L2/interfaces/ISuperchainERC20.sol"; import { IL1Block } from "src/L2/interfaces/IL1Block.sol"; import { IETHLiquidity } from "src/L2/interfaces/IETHLiquidity.sol"; +import { ISuperchainWETH } from "src/L2/interfaces/ISuperchainWETH.sol"; +/// @custom:proxied true +/// @custom:predeploy 0x4200000000000000000000000000000000000024 /// @title SuperchainWETH /// @notice SuperchainWETH is a version of WETH that can be freely transfrered between chains /// within the superchain. SuperchainWETH can be converted into native ETH on chains that /// do not use a custom gas token. -contract SuperchainWETH is WETH98, ISuperchainERC20Extensions, ISemver { +contract SuperchainWETH is WETH98, ISuperchainWETH, ISemver { /// @notice Semantic version. - /// @custom:semver 1.0.0-beta.5 - string public constant version = "1.0.0-beta.5"; + /// @custom:semver 1.0.0-beta.6 + string public constant version = "1.0.0-beta.6"; /// @inheritdoc WETH98 function deposit() public payable override { @@ -36,7 +37,7 @@ contract SuperchainWETH is WETH98, ISuperchainERC20Extensions, ISemver { super.withdraw(wad); } - /// @inheritdoc ISuperchainERC20Extensions + /// @inheritdoc ISuperchainWETH function sendERC20(address dst, uint256 wad, uint256 chainId) public { // Burn from user's balance. _burn(msg.sender, wad); @@ -57,12 +58,12 @@ contract SuperchainWETH is WETH98, ISuperchainERC20Extensions, ISemver { emit SendERC20(msg.sender, dst, wad, chainId); } - /// @inheritdoc ISuperchainERC20Extensions + /// @inheritdoc ISuperchainWETH function relayERC20(address from, address dst, uint256 wad) external { // Receive message from other chain. IL2ToL2CrossDomainMessenger messenger = IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); - if (msg.sender != address(messenger)) revert Unauthorized(); - if (messenger.crossDomainMessageSender() != address(this)) revert Unauthorized(); + if (msg.sender != address(messenger)) revert CallerNotL2ToL2CrossDomainMessenger(); + if (messenger.crossDomainMessageSender() != address(this)) revert InvalidCrossDomainSender(); // Mint from ETHLiquidity contract. if (!IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) { diff --git a/packages/contracts-bedrock/src/L2/interfaces/ICrosschainERC20.sol b/packages/contracts-bedrock/src/L2/interfaces/ICrosschainERC20.sol new file mode 100644 index 000000000000..efdebe8f8b47 --- /dev/null +++ b/packages/contracts-bedrock/src/L2/interfaces/ICrosschainERC20.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @title ICrosschainERC20 +/// @notice Defines the interface for crosschain ERC20 transfers. +interface ICrosschainERC20 { + /// @notice Emitted when a crosschain transfer mints tokens. + /// @param to Address of the account tokens are being minted for. + /// @param amount Amount of tokens minted. + event CrosschainMinted(address indexed to, uint256 amount); + + /// @notice Emitted when a crosschain transfer burns tokens. + /// @param from Address of the account tokens are being burned from. + /// @param amount Amount of tokens burned. + event CrosschainBurnt(address indexed from, uint256 amount); + + /// @notice Mint tokens through a crosschain transfer. + /// @param _to Address to mint tokens to. + /// @param _amount Amount of tokens to mint. + function crosschainMint(address _to, uint256 _amount) external; + + /// @notice Burn tokens through a crosschain transfer. + /// @param _from Address to burn tokens from. + /// @param _amount Amount of tokens to burn. + function crosschainBurn(address _from, uint256 _amount) external; +} diff --git a/packages/contracts-bedrock/src/L2/interfaces/IL2StandardBridgeInterop.sol b/packages/contracts-bedrock/src/L2/interfaces/IL2StandardBridgeInterop.sol index ed4ec1cef519..6b60f5e4f9b2 100644 --- a/packages/contracts-bedrock/src/L2/interfaces/IL2StandardBridgeInterop.sol +++ b/packages/contracts-bedrock/src/L2/interfaces/IL2StandardBridgeInterop.sol @@ -1,15 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IStandardBridge } from "src/universal/interfaces/IStandardBridge.sol"; import { ICrossDomainMessenger } from "src/universal/interfaces/ICrossDomainMessenger.sol"; -interface IMintableAndBurnable is IERC20 { - function mint(address, uint256) external; - function burn(address, uint256) external; -} - interface IL2StandardBridgeInterop is IStandardBridge { error InvalidDecimals(); error InvalidLegacyERC20Address(); diff --git a/packages/contracts-bedrock/src/L2/interfaces/IMintableAndBurnableERC20.sol b/packages/contracts-bedrock/src/L2/interfaces/IMintableAndBurnableERC20.sol new file mode 100644 index 000000000000..166fa0c8077f --- /dev/null +++ b/packages/contracts-bedrock/src/L2/interfaces/IMintableAndBurnableERC20.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/// @title IMintableAndBurnableERC20 +/// @notice Interface for mintable and burnable ERC20 tokens. +interface IMintableAndBurnableERC20 is IERC20 { + /// @notice Mints `_amount` of tokens to `_to`. + /// @param _to Address to mint tokens to. + /// @param _amount Amount of tokens to mint. + function mint(address _to, uint256 _amount) external; + + /// @notice Burns `_amount` of tokens from `_from`. + /// @param _from Address to burn tokens from. + /// @param _amount Amount of tokens to burn. + function burn(address _from, uint256 _amount) external; +} diff --git a/packages/contracts-bedrock/src/L2/interfaces/IOptimismSuperchainERC20.sol b/packages/contracts-bedrock/src/L2/interfaces/IOptimismSuperchainERC20.sol index 5f537c1f51ec..0284e29841cb 100644 --- a/packages/contracts-bedrock/src/L2/interfaces/IOptimismSuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/interfaces/IOptimismSuperchainERC20.sol @@ -2,38 +2,30 @@ pragma solidity ^0.8.0; // Interfaces -import { IERC20Solady } from "src/vendor/interfaces/IERC20Solady.sol"; -import { ISuperchainERC20Extensions, ISuperchainERC20Errors } from "src/L2/interfaces/ISuperchainERC20.sol"; +import { ISuperchainERC20 } from "src/L2/interfaces/ISuperchainERC20.sol"; -/// @title IOptimismSuperchainERC20Extension +/// @title IOptimismSuperchainERC20 /// @notice This interface is available on the OptimismSuperchainERC20 contract. -/// We declare it as a separate interface so that it can be used in -/// custom implementations of SuperchainERC20. -interface IOptimismSuperchainERC20Extension is ISuperchainERC20Extensions, ISuperchainERC20Errors { - /// @notice Emitted whenever tokens are minted for an account. - /// @param account Address of the account tokens are being minted for. - /// @param amount Amount of tokens minted. - event Mint(address indexed account, uint256 amount); - - /// @notice Emitted whenever tokens are burned from an account. - /// @param account Address of the account tokens are being burned from. - /// @param amount Amount of tokens burned. - event Burn(address indexed account, uint256 amount); - - /// @notice Allows the L2StandardBridge to mint tokens. - /// @param _to Address to mint tokens to. - /// @param _amount Amount of tokens to mint. +interface IOptimismSuperchainERC20 is ISuperchainERC20 { + error ZeroAddress(); + error InvalidInitialization(); + error NotInitializing(); + + event Initialized(uint64 version); + + event Mint(address indexed to, uint256 amount); + + event Burn(address indexed from, uint256 amount); + + function initialize(address _remoteToken, string memory _name, string memory _symbol, uint8 _decimals) external; + function mint(address _to, uint256 _amount) external; - /// @notice Allows the L2StandardBridge to burn tokens. - /// @param _from Address to burn tokens from. - /// @param _amount Amount of tokens to burn. function burn(address _from, uint256 _amount) external; - /// @notice Returns the address of the corresponding version of this token on the remote chain. function remoteToken() external view returns (address); -} -/// @title IOptimismSuperchainERC20 -/// @notice Combines Solady's ERC20 interface with the OptimismSuperchainERC20Extension interface. -interface IOptimismSuperchainERC20 is IERC20Solady, IOptimismSuperchainERC20Extension { } + function supportsInterface(bytes4 _interfaceId) external view returns (bool); + + function __constructor__() external; +} diff --git a/packages/contracts-bedrock/src/L2/interfaces/IOptimismSuperchainERC20Factory.sol b/packages/contracts-bedrock/src/L2/interfaces/IOptimismSuperchainERC20Factory.sol index dc21a3d51ad7..aa8e4f1f1605 100644 --- a/packages/contracts-bedrock/src/L2/interfaces/IOptimismSuperchainERC20Factory.sol +++ b/packages/contracts-bedrock/src/L2/interfaces/IOptimismSuperchainERC20Factory.sol @@ -11,8 +11,6 @@ interface IOptimismSuperchainERC20Factory is IOptimismERC20Factory, ISemver { address indexed superchainToken, address indexed remoteToken, address deployer ); - function deployments(address _superchainToken) external view override returns (address remoteToken_); - function version() external view override returns (string memory); function deploy( address _remoteToken, string memory _name, diff --git a/packages/contracts-bedrock/src/L2/interfaces/ISuperchainERC20.sol b/packages/contracts-bedrock/src/L2/interfaces/ISuperchainERC20.sol index 6ed17a9f46ec..4aad888535a7 100644 --- a/packages/contracts-bedrock/src/L2/interfaces/ISuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/interfaces/ISuperchainERC20.sol @@ -2,57 +2,15 @@ pragma solidity ^0.8.0; // Interfaces +import { ICrosschainERC20 } from "src/L2/interfaces/ICrosschainERC20.sol"; import { IERC20Solady } from "src/vendor/interfaces/IERC20Solady.sol"; - -/// @title ISuperchainERC20Extensions -/// @notice Interface for the extensions to the ERC20 standard that are used by SuperchainERC20. -/// Exists in case developers are already importing the ERC20 interface separately and -/// importing the full SuperchainERC20 interface would cause conflicting imports. -interface ISuperchainERC20Extensions { - /// @notice Emitted when tokens are sent from one chain to another. - /// @param from Address of the sender. - /// @param to Address of the recipient. - /// @param amount Number of tokens sent. - /// @param destination Chain ID of the destination chain. - event SendERC20(address indexed from, address indexed to, uint256 amount, uint256 destination); - - /// @notice Emitted whenever tokens are successfully relayed on this chain. - /// @param from Address of the msg.sender of sendERC20 on the source chain. - /// @param to Address of the recipient. - /// @param amount Amount of tokens relayed. - /// @param source Chain ID of the source chain. - event RelayERC20(address indexed from, address indexed to, uint256 amount, uint256 source); - - /// @notice Sends tokens to some target address on another chain. - /// @param _to Address to send tokens to. - /// @param _amount Amount of tokens to send. - /// @param _chainId Chain ID of the destination chain. - function sendERC20(address _to, uint256 _amount, uint256 _chainId) external; - - /// @notice Relays tokens received from another chain. - /// @param _from Address of the msg.sender of sendERC20 on the source chain. - /// @param _to Address to relay tokens to. - /// @param _amount Amount of tokens to relay. - function relayERC20(address _from, address _to, uint256 _amount) external; -} - -/// @title ISuperchainERC20Errors -/// @notice Interface containing the errors added in the SuperchainERC20 implementation. -interface ISuperchainERC20Errors { - /// @notice Thrown when attempting to relay a message and the function caller (msg.sender) is not - /// L2ToL2CrossDomainMessenger. - error CallerNotL2ToL2CrossDomainMessenger(); - - /// @notice Thrown when attempting to relay a message and the cross domain message sender is not this - /// SuperchainERC20. - error InvalidCrossDomainSender(); - - /// @notice Thrown when attempting to perform an operation and the account is the zero address. - error ZeroAddress(); -} +import { ISemver } from "src/universal/interfaces/ISemver.sol"; /// @title ISuperchainERC20 -/// @notice Combines Solady's ERC20 interface with the SuperchainERC20Extensions interface. -interface ISuperchainERC20 is IERC20Solady, ISuperchainERC20Extensions, ISuperchainERC20Errors { +/// @notice This interface is available on the SuperchainERC20 contract. +/// @dev This interface is needed for the abstract SuperchainERC20 implementation but is not part of the standard +interface ISuperchainERC20 is ICrosschainERC20, IERC20Solady, ISemver { + error Unauthorized(); + function __constructor__() external; } diff --git a/packages/contracts-bedrock/src/L2/interfaces/ISuperchainTokenBridge.sol b/packages/contracts-bedrock/src/L2/interfaces/ISuperchainTokenBridge.sol new file mode 100644 index 000000000000..f2a61d02d555 --- /dev/null +++ b/packages/contracts-bedrock/src/L2/interfaces/ISuperchainTokenBridge.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { ISemver } from "src/universal/interfaces/ISemver.sol"; + +/// @title ISuperchainTokenBridge +/// @notice Interface for the SuperchainTokenBridge contract. +interface ISuperchainTokenBridge is ISemver { + error ZeroAddress(); + error Unauthorized(); + error InvalidCrossDomainSender(); + + event SendERC20( + address indexed token, address indexed from, address indexed to, uint256 amount, uint256 destination + ); + + event RelayERC20(address indexed token, address indexed from, address indexed to, uint256 amount, uint256 source); + + function sendERC20( + address _token, + address _to, + uint256 _amount, + uint256 _chainId + ) + external + returns (bytes32 msgHash_); + + function relayERC20(address _token, address _from, address _to, uint256 _amount) external; + + function __constructor__() external; +} diff --git a/packages/contracts-bedrock/src/L2/interfaces/ISuperchainWETH.sol b/packages/contracts-bedrock/src/L2/interfaces/ISuperchainWETH.sol index 1204c328fc89..bccab456f5fd 100644 --- a/packages/contracts-bedrock/src/L2/interfaces/ISuperchainWETH.sol +++ b/packages/contracts-bedrock/src/L2/interfaces/ISuperchainWETH.sol @@ -1,35 +1,44 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; +import { IWETH } from "src/universal/interfaces/IWETH.sol"; + interface ISuperchainWETH { + /// @notice Thrown when attempting a deposit or withdrawal and the chain uses a custom gas token. error NotCustomGasToken(); - error Unauthorized(); - event Approval(address indexed src, address indexed guy, uint256 wad); - event Deposit(address indexed dst, uint256 wad); + /// @notice Thrown when attempting to relay a message and the function caller (msg.sender) is not + /// L2ToL2CrossDomainMessenger. + error CallerNotL2ToL2CrossDomainMessenger(); + + /// @notice Thrown when attempting to relay a message and the cross domain message sender is not `address(this)` + error InvalidCrossDomainSender(); + + /// @notice Emitted whenever tokens are successfully relayed on this chain. + /// @param from Address of the msg.sender of sendERC20 on the source chain. + /// @param to Address of the recipient. + /// @param amount Amount of tokens relayed. + /// @param source Chain ID of the source chain. event RelayERC20(address indexed from, address indexed to, uint256 amount, uint256 source); + + /// @notice Emitted when tokens are sent from one chain to another. + /// @param from Address of the sender. + /// @param to Address of the recipient. + /// @param amount Number of tokens sent. + /// @param destination Chain ID of the destination chain. event SendERC20(address indexed from, address indexed to, uint256 amount, uint256 destination); - event Transfer(address indexed src, address indexed dst, uint256 wad); - event Withdrawal(address indexed src, uint256 wad); - - fallback() external payable; - - receive() external payable; - - function allowance(address, address) external view returns (uint256); - function approve(address guy, uint256 wad) external returns (bool); - function balanceOf(address) external view returns (uint256); - function decimals() external view returns (uint8); - function deposit() external payable; - function name() external view returns (string memory); - function relayERC20(address from, address dst, uint256 wad) external; - function sendERC20(address dst, uint256 wad, uint256 chainId) external; - function symbol() external view returns (string memory); - function totalSupply() external view returns (uint256); - function transfer(address dst, uint256 wad) external returns (bool); - function transferFrom(address src, address dst, uint256 wad) external returns (bool); - function version() external view returns (string memory); - function withdraw(uint256 wad) external; - - function __constructor__() external; + + /// @notice Sends tokens to some target address on another chain. + /// @param _dst Address to send tokens to. + /// @param _wad Amount of tokens to send. + /// @param _chainId Chain ID of the destination chain. + function sendERC20(address _dst, uint256 _wad, uint256 _chainId) external; + + /// @notice Relays tokens received from another chain. + /// @param _from Address of the msg.sender of sendERC20 on the source chain. + /// @param _dst Address to relay tokens to. + /// @param _wad Amount of tokens to relay. + function relayERC20(address _from, address _dst, uint256 _wad) external; } + +interface ISuperchainWETHERC20 is IWETH, ISuperchainWETH { } diff --git a/packages/contracts-bedrock/src/libraries/Predeploys.sol b/packages/contracts-bedrock/src/libraries/Predeploys.sol index c8fca4376bde..30cad3b1f854 100644 --- a/packages/contracts-bedrock/src/libraries/Predeploys.sol +++ b/packages/contracts-bedrock/src/libraries/Predeploys.sol @@ -105,6 +105,9 @@ library Predeploys { /// @notice Arbitrary address of the OptimismSuperchainERC20 implementation contract. address internal constant OPTIMISM_SUPERCHAIN_ERC20 = 0xB9415c6cA93bdC545D4c5177512FCC22EFa38F28; + /// @notice Address of the SuperchainTokenBridge predeploy. + address internal constant SUPERCHAIN_TOKEN_BRIDGE = 0x4200000000000000000000000000000000000028; + /// @notice Returns the name of the predeploy at the given address. function getName(address _addr) internal pure returns (string memory out_) { require(isPredeployNamespace(_addr), "Predeploys: address must be a predeploy"); @@ -135,6 +138,7 @@ library Predeploys { if (_addr == ETH_LIQUIDITY) return "ETHLiquidity"; if (_addr == OPTIMISM_SUPERCHAIN_ERC20_FACTORY) return "OptimismSuperchainERC20Factory"; if (_addr == OPTIMISM_SUPERCHAIN_ERC20_BEACON) return "OptimismSuperchainERC20Beacon"; + if (_addr == SUPERCHAIN_TOKEN_BRIDGE) return "SuperchainTokenBridge"; revert("Predeploys: unnamed predeploy"); } @@ -154,7 +158,8 @@ library Predeploys { || (_useInterop && _addr == CROSS_L2_INBOX) || (_useInterop && _addr == L2_TO_L2_CROSS_DOMAIN_MESSENGER) || (_useInterop && _addr == SUPERCHAIN_WETH) || (_useInterop && _addr == ETH_LIQUIDITY) || (_useInterop && _addr == OPTIMISM_SUPERCHAIN_ERC20_FACTORY) - || (_useInterop && _addr == OPTIMISM_SUPERCHAIN_ERC20_BEACON); + || (_useInterop && _addr == OPTIMISM_SUPERCHAIN_ERC20_BEACON) + || (_useInterop && _addr == SUPERCHAIN_TOKEN_BRIDGE); } function isPredeployNamespace(address _addr) internal pure returns (bool) { diff --git a/packages/contracts-bedrock/src/libraries/errors/CommonErrors.sol b/packages/contracts-bedrock/src/libraries/errors/CommonErrors.sol index eee6cc699489..30ce96972a19 100644 --- a/packages/contracts-bedrock/src/libraries/errors/CommonErrors.sol +++ b/packages/contracts-bedrock/src/libraries/errors/CommonErrors.sol @@ -12,3 +12,6 @@ error NotCustomGasToken(); /// @notice Error for when a transfer via call fails. error TransferFailed(); + +/// @notice Thrown when attempting to perform an operation and the account is the zero address. +error ZeroAddress(); diff --git a/packages/contracts-bedrock/test/L2/L2Genesis.t.sol b/packages/contracts-bedrock/test/L2/L2Genesis.t.sol index ee993fe1110c..68fc374178ee 100644 --- a/packages/contracts-bedrock/test/L2/L2Genesis.t.sol +++ b/packages/contracts-bedrock/test/L2/L2Genesis.t.sol @@ -150,8 +150,8 @@ contract L2GenesisTest is Test { // 2 predeploys do not have proxies assertEq(getCodeCount(_path, "Proxy.sol:Proxy"), Predeploys.PREDEPLOY_COUNT - 2); - // 23 proxies have the implementation set if useInterop is true and 17 if useInterop is false - assertEq(getPredeployCountWithSlotSet(_path, Constants.PROXY_IMPLEMENTATION_ADDRESS), _useInterop ? 23 : 17); + // 24 proxies have the implementation set if useInterop is true and 17 if useInterop is false + assertEq(getPredeployCountWithSlotSet(_path, Constants.PROXY_IMPLEMENTATION_ADDRESS), _useInterop ? 24 : 17); // All proxies except 2 have the proxy 1967 admin slot set to the proxy admin assertEq( diff --git a/packages/contracts-bedrock/test/L2/L2StandardBridgeInterop.t.sol b/packages/contracts-bedrock/test/L2/L2StandardBridgeInterop.t.sol index 819b3562c98e..e57bc8977659 100644 --- a/packages/contracts-bedrock/test/L2/L2StandardBridgeInterop.t.sol +++ b/packages/contracts-bedrock/test/L2/L2StandardBridgeInterop.t.sol @@ -5,7 +5,8 @@ pragma solidity 0.8.15; import { Bridge_Initializer } from "test/setup/Bridge_Initializer.sol"; // Interfaces -import { IL2StandardBridgeInterop, IMintableAndBurnable } from "src/L2/interfaces/IL2StandardBridgeInterop.sol"; +import { IMintableAndBurnableERC20 } from "src/L2/interfaces/IMintableAndBurnableERC20.sol"; +import { IL2StandardBridgeInterop } from "src/L2/interfaces/IL2StandardBridgeInterop.sol"; import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import { IOptimismMintableERC20 } from "src/universal/interfaces/IOptimismMintableERC20.sol"; @@ -53,9 +54,9 @@ contract L2StandardBridgeInterop_Test is Bridge_Initializer { } } -/// @notice Test suite when converting from a legacy token to a SuperchainERC20 token +/// @notice Test suite when converting from a legacy token to a OptimismSuperchainERC20 token contract L2StandardBridgeInterop_LegacyToSuper_Test is L2StandardBridgeInterop_Test { - /// @notice Set up the test for converting from a legacy token to a SuperchainERC20 token + /// @notice Set up the test for converting from a legacy token to a OptimismSuperchainERC20 token function _setUpLegacyToSuper(address _from, address _to) internal { // Assume _assumeAddress(_from); @@ -198,9 +199,11 @@ contract L2StandardBridgeInterop_LegacyToSuper_Test is L2StandardBridgeInterop_T // Mock and expect the `burn` and `mint` functions _mockAndExpect( - _from, abi.encodeWithSelector(IMintableAndBurnable.burn.selector, _caller, _amount), abi.encode() + _from, abi.encodeWithSelector(IMintableAndBurnableERC20.burn.selector, _caller, _amount), abi.encode() + ); + _mockAndExpect( + _to, abi.encodeWithSelector(IMintableAndBurnableERC20.mint.selector, _caller, _amount), abi.encode() ); - _mockAndExpect(_to, abi.encodeWithSelector(IMintableAndBurnable.mint.selector, _caller, _amount), abi.encode()); // Act vm.prank(_caller); @@ -208,9 +211,9 @@ contract L2StandardBridgeInterop_LegacyToSuper_Test is L2StandardBridgeInterop_T } } -/// @notice Test suite when converting from a SuperchainERC20 token to a legacy token +/// @notice Test suite when converting from a OptimismSuperchainERC20 token to a legacy token contract L2StandardBridgeInterop_SuperToLegacy_Test is L2StandardBridgeInterop_Test { - /// @notice Set up the test for converting from a SuperchainERC20 token to a legacy token + /// @notice Set up the test for converting from a OptimismSuperchainERC20 token to a legacy token function _setUpSuperToLegacy(address _from, address _to) internal { // Assume _assumeAddress(_from); @@ -354,9 +357,11 @@ contract L2StandardBridgeInterop_SuperToLegacy_Test is L2StandardBridgeInterop_T // Mock and expect the `burn` and `mint` functions _mockAndExpect( - _from, abi.encodeWithSelector(IMintableAndBurnable.burn.selector, _caller, _amount), abi.encode() + _from, abi.encodeWithSelector(IMintableAndBurnableERC20.burn.selector, _caller, _amount), abi.encode() + ); + _mockAndExpect( + _to, abi.encodeWithSelector(IMintableAndBurnableERC20.mint.selector, _caller, _amount), abi.encode() ); - _mockAndExpect(_to, abi.encodeWithSelector(IMintableAndBurnable.mint.selector, _caller, _amount), abi.encode()); // Act vm.prank(_caller); diff --git a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol index 95091c5f0ded..87d5cbb74b23 100644 --- a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol +++ b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol @@ -8,19 +8,15 @@ import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; // Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; import { IERC20 } from "@openzeppelin/contracts-v5/token/ERC20/IERC20.sol"; -import { IL2ToL2CrossDomainMessenger } from "src/L2/interfaces/IL2ToL2CrossDomainMessenger.sol"; import { Initializable } from "@openzeppelin/contracts-v5/proxy/utils/Initializable.sol"; import { IERC165 } from "@openzeppelin/contracts-v5/utils/introspection/IERC165.sol"; import { IBeacon } from "@openzeppelin/contracts-v5/proxy/beacon/IBeacon.sol"; import { BeaconProxy } from "@openzeppelin/contracts-v5/proxy/beacon/BeaconProxy.sol"; +import { Unauthorized } from "src/libraries/errors/CommonErrors.sol"; // Target contract -import { - OptimismSuperchainERC20, IOptimismSuperchainERC20Extension, OnlyBridge -} from "src/L2/OptimismSuperchainERC20.sol"; - -// SuperchainERC20 Interfaces -import { ISuperchainERC20Extensions, ISuperchainERC20Errors } from "src/L2/interfaces/ISuperchainERC20.sol"; +import { OptimismSuperchainERC20 } from "src/L2/OptimismSuperchainERC20.sol"; +import { IOptimismSuperchainERC20 } from "src/L2/interfaces/IOptimismSuperchainERC20.sol"; /// @title OptimismSuperchainERC20Test /// @notice Contract for testing the OptimismSuperchainERC20 contract. @@ -28,22 +24,22 @@ contract OptimismSuperchainERC20Test is Test { address internal constant ZERO_ADDRESS = address(0); address internal constant REMOTE_TOKEN = address(0x123); string internal constant NAME = "OptimismSuperchainERC20"; - string internal constant SYMBOL = "SCE"; + string internal constant SYMBOL = "OSC"; uint8 internal constant DECIMALS = 18; - address internal constant BRIDGE = Predeploys.L2_STANDARD_BRIDGE; + address internal constant L2_BRIDGE = Predeploys.L2_STANDARD_BRIDGE; address internal constant MESSENGER = Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER; - OptimismSuperchainERC20 public superchainERC20Impl; - OptimismSuperchainERC20 public superchainERC20; + OptimismSuperchainERC20 public optimismSuperchainERC20Impl; + OptimismSuperchainERC20 public optimismSuperchainERC20; /// @notice Sets up the test suite. function setUp() public { - superchainERC20Impl = new OptimismSuperchainERC20(); + optimismSuperchainERC20Impl = new OptimismSuperchainERC20(); // Deploy the OptimismSuperchainERC20Beacon contract _deployBeacon(); - superchainERC20 = _deploySuperchainERC20Proxy(REMOTE_TOKEN, NAME, SYMBOL, DECIMALS); + optimismSuperchainERC20 = _deploySuperchainERC20Proxy(REMOTE_TOKEN, NAME, SYMBOL, DECIMALS); } /// @notice Deploy the OptimismSuperchainERC20Beacon predeploy contract @@ -61,7 +57,9 @@ contract OptimismSuperchainERC20Test is Test { // Mock implementation address vm.mockCall( - _impl, abi.encodeWithSelector(IBeacon.implementation.selector), abi.encode(address(superchainERC20Impl)) + _impl, + abi.encodeWithSelector(IBeacon.implementation.selector), + abi.encode(address(optimismSuperchainERC20Impl)) ); } @@ -93,10 +91,10 @@ contract OptimismSuperchainERC20Test is Test { /// @notice Test that the contract's `initializer` sets the correct values. function test_initializer_succeeds() public view { - assertEq(superchainERC20.name(), NAME); - assertEq(superchainERC20.symbol(), SYMBOL); - assertEq(superchainERC20.decimals(), DECIMALS); - assertEq(superchainERC20.remoteToken(), REMOTE_TOKEN); + assertEq(optimismSuperchainERC20.name(), NAME); + assertEq(optimismSuperchainERC20.symbol(), SYMBOL); + assertEq(optimismSuperchainERC20.decimals(), DECIMALS); + assertEq(optimismSuperchainERC20.remoteToken(), REMOTE_TOKEN); } /// @notice Tests the `initialize` function reverts when the contract is already initialized. @@ -112,30 +110,30 @@ contract OptimismSuperchainERC20Test is Test { vm.expectRevert(Initializable.InvalidInitialization.selector); // Call the `initialize` function again - superchainERC20.initialize(_remoteToken, _name, _symbol, _decimals); + optimismSuperchainERC20.initialize(_remoteToken, _name, _symbol, _decimals); } /// @notice Tests the `mint` function reverts when the caller is not the bridge. function testFuzz_mint_callerNotBridge_reverts(address _caller, address _to, uint256 _amount) public { // Ensure the caller is not the bridge - vm.assume(_caller != BRIDGE); + vm.assume(_caller != L2_BRIDGE); - // Expect the revert with `OnlyBridge` selector - vm.expectRevert(OnlyBridge.selector); + // Expect the revert with `Unauthorized` selector + vm.expectRevert(Unauthorized.selector); // Call the `mint` function with the non-bridge caller vm.prank(_caller); - superchainERC20.mint(_to, _amount); + optimismSuperchainERC20.mint(_to, _amount); } /// @notice Tests the `mint` function reverts when the amount is zero. function testFuzz_mint_zeroAddressTo_reverts(uint256 _amount) public { // Expect the revert with `ZeroAddress` selector - vm.expectRevert(ISuperchainERC20Errors.ZeroAddress.selector); + vm.expectRevert(IOptimismSuperchainERC20.ZeroAddress.selector); // Call the `mint` function with the zero address - vm.prank(BRIDGE); - superchainERC20.mint({ _to: ZERO_ADDRESS, _amount: _amount }); + vm.prank(L2_BRIDGE); + optimismSuperchainERC20.mint({ _to: ZERO_ADDRESS, _amount: _amount }); } /// @notice Tests the `mint` succeeds and emits the `Mint` event. @@ -144,47 +142,47 @@ contract OptimismSuperchainERC20Test is Test { vm.assume(_to != ZERO_ADDRESS); // Get the total supply and balance of `_to` before the mint to compare later on the assertions - uint256 _totalSupplyBefore = superchainERC20.totalSupply(); - uint256 _toBalanceBefore = superchainERC20.balanceOf(_to); + uint256 _totalSupplyBefore = IERC20(address(optimismSuperchainERC20)).totalSupply(); + uint256 _toBalanceBefore = IERC20(address(optimismSuperchainERC20)).balanceOf(_to); // Look for the emit of the `Transfer` event - vm.expectEmit(address(superchainERC20)); + vm.expectEmit(address(optimismSuperchainERC20)); emit IERC20.Transfer(ZERO_ADDRESS, _to, _amount); // Look for the emit of the `Mint` event - vm.expectEmit(address(superchainERC20)); - emit IOptimismSuperchainERC20Extension.Mint(_to, _amount); + vm.expectEmit(address(optimismSuperchainERC20)); + emit IOptimismSuperchainERC20.Mint(_to, _amount); // Call the `mint` function with the bridge caller - vm.prank(BRIDGE); - superchainERC20.mint(_to, _amount); + vm.prank(L2_BRIDGE); + optimismSuperchainERC20.mint(_to, _amount); // Check the total supply and balance of `_to` after the mint were updated correctly - assertEq(superchainERC20.totalSupply(), _totalSupplyBefore + _amount); - assertEq(superchainERC20.balanceOf(_to), _toBalanceBefore + _amount); + assertEq(optimismSuperchainERC20.totalSupply(), _totalSupplyBefore + _amount); + assertEq(optimismSuperchainERC20.balanceOf(_to), _toBalanceBefore + _amount); } /// @notice Tests the `burn` function reverts when the caller is not the bridge. function testFuzz_burn_callerNotBridge_reverts(address _caller, address _from, uint256 _amount) public { // Ensure the caller is not the bridge - vm.assume(_caller != BRIDGE); + vm.assume(_caller != L2_BRIDGE); - // Expect the revert with `OnlyBridge` selector - vm.expectRevert(OnlyBridge.selector); + // Expect the revert with `Unauthorized` selector + vm.expectRevert(Unauthorized.selector); // Call the `burn` function with the non-bridge caller vm.prank(_caller); - superchainERC20.burn(_from, _amount); + optimismSuperchainERC20.burn(_from, _amount); } /// @notice Tests the `burn` function reverts when the amount is zero. function testFuzz_burn_zeroAddressFrom_reverts(uint256 _amount) public { // Expect the revert with `ZeroAddress` selector - vm.expectRevert(ISuperchainERC20Errors.ZeroAddress.selector); + vm.expectRevert(IOptimismSuperchainERC20.ZeroAddress.selector); // Call the `burn` function with the zero address - vm.prank(BRIDGE); - superchainERC20.burn({ _from: ZERO_ADDRESS, _amount: _amount }); + vm.prank(L2_BRIDGE); + optimismSuperchainERC20.burn({ _from: ZERO_ADDRESS, _amount: _amount }); } /// @notice Tests the `burn` burns the amount and emits the `Burn` event. @@ -193,161 +191,28 @@ contract OptimismSuperchainERC20Test is Test { vm.assume(_from != ZERO_ADDRESS); // Mint some tokens to `_from` so then they can be burned - vm.prank(BRIDGE); - superchainERC20.mint(_from, _amount); + vm.prank(L2_BRIDGE); + optimismSuperchainERC20.mint(_from, _amount); // Get the total supply and balance of `_from` before the burn to compare later on the assertions - uint256 _totalSupplyBefore = superchainERC20.totalSupply(); - uint256 _fromBalanceBefore = superchainERC20.balanceOf(_from); + uint256 _totalSupplyBefore = optimismSuperchainERC20.totalSupply(); + uint256 _fromBalanceBefore = optimismSuperchainERC20.balanceOf(_from); // Look for the emit of the `Transfer` event - vm.expectEmit(address(superchainERC20)); + vm.expectEmit(address(optimismSuperchainERC20)); emit IERC20.Transfer(_from, ZERO_ADDRESS, _amount); // Look for the emit of the `Burn` event - vm.expectEmit(address(superchainERC20)); - emit IOptimismSuperchainERC20Extension.Burn(_from, _amount); + vm.expectEmit(address(optimismSuperchainERC20)); + emit IOptimismSuperchainERC20.Burn(_from, _amount); // Call the `burn` function with the bridge caller - vm.prank(BRIDGE); - superchainERC20.burn(_from, _amount); + vm.prank(L2_BRIDGE); + optimismSuperchainERC20.burn(_from, _amount); // Check the total supply and balance of `_from` after the burn were updated correctly - assertEq(superchainERC20.totalSupply(), _totalSupplyBefore - _amount); - assertEq(superchainERC20.balanceOf(_from), _fromBalanceBefore - _amount); - } - - /// @notice Tests the `sendERC20` function reverts when the `_to` address is the zero address. - function testFuzz_sendERC20_zeroAddressTo_reverts(uint256 _amount, uint256 _chainId) public { - // Expect the revert with `ZeroAddress` selector - vm.expectRevert(ISuperchainERC20Errors.ZeroAddress.selector); - - // Call the `sendERC20` function with the zero address - vm.prank(BRIDGE); - superchainERC20.sendERC20({ _to: ZERO_ADDRESS, _amount: _amount, _chainId: _chainId }); - } - - /// @notice Tests the `sendERC20` function burns the sender tokens, sends the message, and emits the `SendERC20` - /// event. - function testFuzz_sendERC20_succeeds(address _sender, address _to, uint256 _amount, uint256 _chainId) external { - // Ensure `_sender` is not the zero address - vm.assume(_sender != ZERO_ADDRESS); - vm.assume(_to != ZERO_ADDRESS); - - // Mint some tokens to the sender so then they can be sent - vm.prank(BRIDGE); - superchainERC20.mint(_sender, _amount); - - // Get the total supply and balance of `_sender` before the send to compare later on the assertions - uint256 _totalSupplyBefore = superchainERC20.totalSupply(); - uint256 _senderBalanceBefore = superchainERC20.balanceOf(_sender); - - // Look for the emit of the `Transfer` event - vm.expectEmit(address(superchainERC20)); - emit IERC20.Transfer(_sender, ZERO_ADDRESS, _amount); - - // Look for the emit of the `SendERC20` event - vm.expectEmit(address(superchainERC20)); - emit ISuperchainERC20Extensions.SendERC20(_sender, _to, _amount, _chainId); - - // Mock the call over the `sendMessage` function and expect it to be called properly - bytes memory _message = abi.encodeCall(superchainERC20.relayERC20, (_sender, _to, _amount)); - _mockAndExpect( - MESSENGER, - abi.encodeWithSelector( - IL2ToL2CrossDomainMessenger.sendMessage.selector, _chainId, address(superchainERC20), _message - ), - abi.encode("") - ); - - // Call the `sendERC20` function - vm.prank(_sender); - superchainERC20.sendERC20(_to, _amount, _chainId); - - // Check the total supply and balance of `_sender` after the send were updated correctly - assertEq(superchainERC20.totalSupply(), _totalSupplyBefore - _amount); - assertEq(superchainERC20.balanceOf(_sender), _senderBalanceBefore - _amount); - } - - /// @notice Tests the `relayERC20` function reverts when the caller is not the L2ToL2CrossDomainMessenger. - function testFuzz_relayERC20_notMessenger_reverts(address _caller, address _to, uint256 _amount) public { - // Ensure the caller is not the messenger - vm.assume(_caller != MESSENGER); - vm.assume(_to != ZERO_ADDRESS); - - // Expect the revert with `CallerNotL2ToL2CrossDomainMessenger` selector - vm.expectRevert(ISuperchainERC20Errors.CallerNotL2ToL2CrossDomainMessenger.selector); - - // Call the `relayERC20` function with the non-messenger caller - vm.prank(_caller); - superchainERC20.relayERC20(_caller, _to, _amount); - } - - /// @notice Tests the `relayERC20` function reverts when the `crossDomainMessageSender` that sent the message is not - /// the same SuperchainERC20 address. - function testFuzz_relayERC20_notCrossDomainSender_reverts( - address _crossDomainMessageSender, - address _to, - uint256 _amount - ) - public - { - vm.assume(_to != ZERO_ADDRESS); - vm.assume(_crossDomainMessageSender != address(superchainERC20)); - - // Mock the call over the `crossDomainMessageSender` function setting a wrong sender - vm.mockCall( - MESSENGER, - abi.encodeWithSelector(IL2ToL2CrossDomainMessenger.crossDomainMessageSender.selector), - abi.encode(_crossDomainMessageSender) - ); - - // Expect the revert with `InvalidCrossDomainSender` selector - vm.expectRevert(ISuperchainERC20Errors.InvalidCrossDomainSender.selector); - - // Call the `relayERC20` function with the sender caller - vm.prank(MESSENGER); - superchainERC20.relayERC20(_crossDomainMessageSender, _to, _amount); - } - - /// @notice Tests the `relayERC20` mints the proper amount and emits the `RelayERC20` event. - function testFuzz_relayERC20_succeeds(address _from, address _to, uint256 _amount, uint256 _source) public { - vm.assume(_from != ZERO_ADDRESS); - vm.assume(_to != ZERO_ADDRESS); - - // Mock the call over the `crossDomainMessageSender` function setting the same address as value - _mockAndExpect( - MESSENGER, - abi.encodeWithSelector(IL2ToL2CrossDomainMessenger.crossDomainMessageSender.selector), - abi.encode(address(superchainERC20)) - ); - - // Mock the call over the `crossDomainMessageSource` function setting the source chain ID as value - _mockAndExpect( - MESSENGER, - abi.encodeWithSelector(IL2ToL2CrossDomainMessenger.crossDomainMessageSource.selector), - abi.encode(_source) - ); - - // Get the total supply and balance of `_to` before the relay to compare later on the assertions - uint256 _totalSupplyBefore = superchainERC20.totalSupply(); - uint256 _toBalanceBefore = superchainERC20.balanceOf(_to); - - // Look for the emit of the `Transfer` event - vm.expectEmit(address(superchainERC20)); - emit IERC20.Transfer(ZERO_ADDRESS, _to, _amount); - - // Look for the emit of the `RelayERC20` event - vm.expectEmit(address(superchainERC20)); - emit ISuperchainERC20Extensions.RelayERC20(_from, _to, _amount, _source); - - // Call the `relayERC20` function with the messenger caller - vm.prank(MESSENGER); - superchainERC20.relayERC20(_from, _to, _amount); - - // Check the total supply and balance of `_to` after the relay were updated correctly - assertEq(superchainERC20.totalSupply(), _totalSupplyBefore + _amount); - assertEq(superchainERC20.balanceOf(_to), _toBalanceBefore + _amount); + assertEq(optimismSuperchainERC20.totalSupply(), _totalSupplyBefore - _amount); + assertEq(optimismSuperchainERC20.balanceOf(_from), _fromBalanceBefore - _amount); } /// @notice Tests the `decimals` function always returns the correct value. @@ -374,17 +239,17 @@ contract OptimismSuperchainERC20Test is Test { assertEq(_newSuperchainERC20.symbol(), _symbol); } - /// @notice Tests that the `supportsInterface` function returns true for the `IOptimismSuperchainERC20` interface. + /// @notice Tests that the `supportsInterface` function returns true for the `ISuperchainERC20` interface. function test_supportInterface_succeeds() public view { - assertTrue(superchainERC20.supportsInterface(type(IERC165).interfaceId)); - assertTrue(superchainERC20.supportsInterface(type(IOptimismSuperchainERC20Extension).interfaceId)); + assertTrue(optimismSuperchainERC20.supportsInterface(type(IERC165).interfaceId)); + assertTrue(optimismSuperchainERC20.supportsInterface(type(IOptimismSuperchainERC20).interfaceId)); } /// @notice Tests that the `supportsInterface` function returns false for any other interface than the - /// `IOptimismSuperchainERC20` one. + /// `ISuperchainERC20` one. function testFuzz_supportInterface_returnFalse(bytes4 _interfaceId) public view { vm.assume(_interfaceId != type(IERC165).interfaceId); - vm.assume(_interfaceId != type(IOptimismSuperchainERC20Extension).interfaceId); - assertFalse(superchainERC20.supportsInterface(_interfaceId)); + vm.assume(_interfaceId != type(IOptimismSuperchainERC20).interfaceId); + assertFalse(optimismSuperchainERC20.supportsInterface(_interfaceId)); } } diff --git a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20Factory.t.sol b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20Factory.t.sol index 3951ebf7452e..15630a15d8eb 100644 --- a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20Factory.t.sol +++ b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20Factory.t.sol @@ -1,53 +1,29 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.25; +pragma solidity 0.8.15; // Testing utilities -import { Test } from "forge-std/Test.sol"; -import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; +import { Bridge_Initializer } from "test/setup/Bridge_Initializer.sol"; // Libraries -import { Predeploys } from "src/libraries/Predeploys.sol"; import { CREATE3, Bytes32AddressLib } from "@rari-capital/solmate/src/utils/CREATE3.sol"; -import { IBeacon } from "@openzeppelin/contracts-v5/proxy/beacon/IBeacon.sol"; // Target contract -import { OptimismSuperchainERC20Factory, OptimismSuperchainERC20 } from "src/L2/OptimismSuperchainERC20Factory.sol"; +import { IOptimismSuperchainERC20 } from "src/L2/interfaces/IOptimismSuperchainERC20.sol"; +import { IERC20Metadata } from "@openzeppelin/contracts/interfaces/IERC20Metadata.sol"; /// @title OptimismSuperchainERC20FactoryTest /// @notice Contract for testing the OptimismSuperchainERC20Factory contract. -contract OptimismSuperchainERC20FactoryTest is Test { +contract OptimismSuperchainERC20FactoryTest is Bridge_Initializer { using Bytes32AddressLib for bytes32; - OptimismSuperchainERC20 public superchainERC20Impl; - OptimismSuperchainERC20Factory public superchainERC20Factory; + event OptimismSuperchainERC20Created( + address indexed superchainToken, address indexed remoteToken, address deployer + ); /// @notice Sets up the test suite. - function setUp() public { - superchainERC20Impl = new OptimismSuperchainERC20(); - - // Deploy the OptimismSuperchainERC20Beacon contract - _deployBeacon(); - - superchainERC20Factory = new OptimismSuperchainERC20Factory(); - } - - /// @notice Deploy the OptimismSuperchainERC20Beacon predeploy contract - function _deployBeacon() internal { - // Deploy the OptimismSuperchainERC20Beacon implementation - address _addr = Predeploys.OPTIMISM_SUPERCHAIN_ERC20_BEACON; - address _impl = Predeploys.predeployToCodeNamespace(_addr); - vm.etch(_impl, vm.getDeployedCode("OptimismSuperchainERC20Beacon.sol:OptimismSuperchainERC20Beacon")); - - // Deploy the ERC1967Proxy contract at the Predeploy - bytes memory code = vm.getDeployedCode("universal/Proxy.sol:Proxy"); - vm.etch(_addr, code); - EIP1967Helper.setAdmin(_addr, Predeploys.PROXY_ADMIN); - EIP1967Helper.setImplementation(_addr, _impl); - - // Mock implementation address - vm.mockCall( - _impl, abi.encodeWithSelector(IBeacon.implementation.selector), abi.encode(address(superchainERC20Impl)) - ); + function setUp() public override { + super.enableInterop(); + super.setUp(); } /// @notice Test that calling `deploy` with valid parameters succeeds. @@ -62,22 +38,22 @@ contract OptimismSuperchainERC20FactoryTest is Test { { // Arrange bytes32 salt = keccak256(abi.encode(_remoteToken, _name, _symbol, _decimals)); - address deployment = _calculateTokenAddress(salt, address(superchainERC20Factory)); + address deployment = _calculateTokenAddress(salt, address(l2OptimismSuperchainERC20Factory)); - vm.expectEmit(address(superchainERC20Factory)); - emit OptimismSuperchainERC20Factory.OptimismSuperchainERC20Created(deployment, _remoteToken, _caller); + vm.expectEmit(address(l2OptimismSuperchainERC20Factory)); + emit OptimismSuperchainERC20Created(deployment, _remoteToken, _caller); // Act vm.prank(_caller); - address addr = superchainERC20Factory.deploy(_remoteToken, _name, _symbol, _decimals); + address addr = l2OptimismSuperchainERC20Factory.deploy(_remoteToken, _name, _symbol, _decimals); // Assert assertTrue(addr == deployment); - assertTrue(OptimismSuperchainERC20(deployment).decimals() == _decimals); - assertTrue(OptimismSuperchainERC20(deployment).remoteToken() == _remoteToken); - assertEq(OptimismSuperchainERC20(deployment).name(), _name); - assertEq(OptimismSuperchainERC20(deployment).symbol(), _symbol); - assertEq(superchainERC20Factory.deployments(deployment), _remoteToken); + assertTrue(IERC20Metadata(deployment).decimals() == _decimals); + assertTrue(IOptimismSuperchainERC20(deployment).remoteToken() == _remoteToken); + assertEq(IERC20Metadata(deployment).name(), _name); + assertEq(IERC20Metadata(deployment).symbol(), _symbol); + assertEq(l2OptimismSuperchainERC20Factory.deployments(deployment), _remoteToken); } /// @notice Test that calling `deploy` with the same parameters twice reverts. @@ -92,13 +68,13 @@ contract OptimismSuperchainERC20FactoryTest is Test { { // Arrange vm.prank(_caller); - superchainERC20Factory.deploy(_remoteToken, _name, _symbol, _decimals); + l2OptimismSuperchainERC20Factory.deploy(_remoteToken, _name, _symbol, _decimals); vm.expectRevert(bytes("DEPLOYMENT_FAILED")); // Act vm.prank(_caller); - superchainERC20Factory.deploy(_remoteToken, _name, _symbol, _decimals); + l2OptimismSuperchainERC20Factory.deploy(_remoteToken, _name, _symbol, _decimals); } /// @notice Precalculates the address of the token contract using CREATE3. diff --git a/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol b/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol index 66b20c1b911a..999b0ad4ee88 100644 --- a/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol +++ b/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol @@ -7,56 +7,25 @@ import { Test } from "forge-std/Test.sol"; // Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; import { IERC20 } from "@openzeppelin/contracts-v5/token/ERC20/IERC20.sol"; -import { IL2ToL2CrossDomainMessenger } from "src/L2/interfaces/IL2ToL2CrossDomainMessenger.sol"; // Target contract import { SuperchainERC20 } from "src/L2/SuperchainERC20.sol"; -import { ISuperchainERC20Extensions, ISuperchainERC20Errors } from "src/L2/interfaces/ISuperchainERC20.sol"; - -/// @notice Mock contract for the SuperchainERC20 contract so tests can mint tokens. -contract SuperchainERC20Mock is SuperchainERC20 { - string private _name; - string private _symbol; - uint8 private _decimals; - - constructor(string memory __name, string memory __symbol, uint8 __decimals) { - _name = __name; - _symbol = __symbol; - _decimals = __decimals; - } - - function mint(address _account, uint256 _amount) public { - _mint(_account, _amount); - } - - function name() public view virtual override returns (string memory) { - return _name; - } +import { ICrosschainERC20 } from "src/L2/interfaces/ICrosschainERC20.sol"; +import { ISuperchainERC20 } from "src/L2/interfaces/ISuperchainERC20.sol"; +import { MockSuperchainERC20Implementation } from "test/mocks/SuperchainERC20Implementation.sol"; - function symbol() public view virtual override returns (string memory) { - return _symbol; - } - - function decimals() public view virtual override returns (uint8) { - return _decimals; - } -} /// @title SuperchainERC20Test /// @notice Contract for testing the SuperchainERC20 contract. - contract SuperchainERC20Test is Test { address internal constant ZERO_ADDRESS = address(0); - string internal constant NAME = "SuperchainERC20"; - string internal constant SYMBOL = "SCE"; - uint8 internal constant DECIMALS = 18; + address internal constant SUPERCHAIN_TOKEN_BRIDGE = Predeploys.SUPERCHAIN_TOKEN_BRIDGE; address internal constant MESSENGER = Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER; - SuperchainERC20 public superchainERC20Impl; - SuperchainERC20Mock public superchainERC20; + SuperchainERC20 public superchainERC20; /// @notice Sets up the test suite. function setUp() public { - superchainERC20 = new SuperchainERC20Mock(NAME, SYMBOL, DECIMALS); + superchainERC20 = new MockSuperchainERC20Implementation(); } /// @notice Helper function to setup a mock and expect a call to it. @@ -65,159 +34,85 @@ contract SuperchainERC20Test is Test { vm.expectCall(_receiver, _calldata); } - /// @notice Test that the contract's `constructor` sets the correct values. - function test_constructor_succeeds() public view { - assertEq(superchainERC20.name(), NAME); - assertEq(superchainERC20.symbol(), SYMBOL); - assertEq(superchainERC20.decimals(), DECIMALS); - } + /// @notice Tests the `mint` function reverts when the caller is not the bridge. + function testFuzz_crosschainMint_callerNotBridge_reverts(address _caller, address _to, uint256 _amount) public { + // Ensure the caller is not the bridge + vm.assume(_caller != SUPERCHAIN_TOKEN_BRIDGE); - /// @notice Tests the `sendERC20` function reverts when the `_to` address is the zero address. - function testFuzz_sendERC20_zeroAddressTo_reverts(uint256 _amount, uint256 _chainId) public { - // Expect the revert with `ZeroAddress` selector - vm.expectRevert(ISuperchainERC20Errors.ZeroAddress.selector); + // Expect the revert with `Unauthorized` selector + vm.expectRevert(ISuperchainERC20.Unauthorized.selector); - // Call the `sendERC20` function with the zero address - superchainERC20.sendERC20({ _to: ZERO_ADDRESS, _amount: _amount, _chainId: _chainId }); + // Call the `mint` function with the non-bridge caller + vm.prank(_caller); + superchainERC20.crosschainMint(_to, _amount); } - /// @notice Tests the `sendERC20` function burns the sender tokens, sends the message, and emits the `SendERC20` - /// event. - function testFuzz_sendERC20_succeeds(address _sender, address _to, uint256 _amount, uint256 _chainId) external { - // Ensure `_sender` is not the zero address - vm.assume(_sender != ZERO_ADDRESS); + /// @notice Tests the `mint` succeeds and emits the `Mint` event. + function testFuzz_crosschainMint_succeeds(address _to, uint256 _amount) public { + // Ensure `_to` is not the zero address vm.assume(_to != ZERO_ADDRESS); - // Mint some tokens to the sender so then they can be sent - superchainERC20.mint(_sender, _amount); - - // Get the total supply and balance of `_sender` before the send to compare later on the assertions + // Get the total supply and balance of `_to` before the mint to compare later on the assertions uint256 _totalSupplyBefore = superchainERC20.totalSupply(); - uint256 _senderBalanceBefore = superchainERC20.balanceOf(_sender); + uint256 _toBalanceBefore = superchainERC20.balanceOf(_to); // Look for the emit of the `Transfer` event vm.expectEmit(address(superchainERC20)); - emit IERC20.Transfer(_sender, ZERO_ADDRESS, _amount); + emit IERC20.Transfer(ZERO_ADDRESS, _to, _amount); - // Look for the emit of the `SendERC20` event + // Look for the emit of the `CrosschainMinted` event vm.expectEmit(address(superchainERC20)); - emit ISuperchainERC20Extensions.SendERC20(_sender, _to, _amount, _chainId); - - // Mock the call over the `sendMessage` function and expect it to be called properly - bytes memory _message = abi.encodeCall(superchainERC20.relayERC20, (_sender, _to, _amount)); - _mockAndExpect( - MESSENGER, - abi.encodeWithSelector( - IL2ToL2CrossDomainMessenger.sendMessage.selector, _chainId, address(superchainERC20), _message - ), - abi.encode("") - ); - - // Call the `sendERC20` function - vm.prank(_sender); - superchainERC20.sendERC20(_to, _amount, _chainId); - - // Check the total supply and balance of `_sender` after the send were updated correctly - assertEq(superchainERC20.totalSupply(), _totalSupplyBefore - _amount); - assertEq(superchainERC20.balanceOf(_sender), _senderBalanceBefore - _amount); - } - - /// @notice Tests the `relayERC20` function reverts when the caller is not the L2ToL2CrossDomainMessenger. - function testFuzz_relayERC20_notMessenger_reverts(address _caller, address _to, uint256 _amount) public { - // Ensure the caller is not the messenger - vm.assume(_caller != MESSENGER); - vm.assume(_to != ZERO_ADDRESS); + emit ICrosschainERC20.CrosschainMinted(_to, _amount); - // Expect the revert with `CallerNotL2ToL2CrossDomainMessenger` selector - vm.expectRevert(ISuperchainERC20Errors.CallerNotL2ToL2CrossDomainMessenger.selector); + // Call the `mint` function with the bridge caller + vm.prank(SUPERCHAIN_TOKEN_BRIDGE); + superchainERC20.crosschainMint(_to, _amount); - // Call the `relayERC20` function with the non-messenger caller - vm.prank(_caller); - superchainERC20.relayERC20(_caller, _to, _amount); + // Check the total supply and balance of `_to` after the mint were updated correctly + assertEq(superchainERC20.totalSupply(), _totalSupplyBefore + _amount); + assertEq(superchainERC20.balanceOf(_to), _toBalanceBefore + _amount); } - /// @notice Tests the `relayERC20` function reverts when the `crossDomainMessageSender` that sent the message is not - /// the same SuperchainERC20 address. - function testFuzz_relayERC20_notCrossDomainSender_reverts( - address _crossDomainMessageSender, - address _to, - uint256 _amount - ) - public - { - vm.assume(_to != ZERO_ADDRESS); - vm.assume(_crossDomainMessageSender != address(superchainERC20)); - - // Mock the call over the `crossDomainMessageSender` function setting a wrong sender - vm.mockCall( - MESSENGER, - abi.encodeWithSelector(IL2ToL2CrossDomainMessenger.crossDomainMessageSender.selector), - abi.encode(_crossDomainMessageSender) - ); + /// @notice Tests the `burn` function reverts when the caller is not the bridge. + function testFuzz_crosschainBurn_callerNotBridge_reverts(address _caller, address _from, uint256 _amount) public { + // Ensure the caller is not the bridge + vm.assume(_caller != SUPERCHAIN_TOKEN_BRIDGE); - // Expect the revert with `InvalidCrossDomainSender` selector - vm.expectRevert(ISuperchainERC20Errors.InvalidCrossDomainSender.selector); + // Expect the revert with `Unauthorized` selector + vm.expectRevert(ISuperchainERC20.Unauthorized.selector); - // Call the `relayERC20` function with the sender caller - vm.prank(MESSENGER); - superchainERC20.relayERC20(_crossDomainMessageSender, _to, _amount); + // Call the `burn` function with the non-bridge caller + vm.prank(_caller); + superchainERC20.crosschainBurn(_from, _amount); } - /// @notice Tests the `relayERC20` mints the proper amount and emits the `RelayERC20` event. - function testFuzz_relayERC20_succeeds(address _from, address _to, uint256 _amount, uint256 _source) public { + /// @notice Tests the `burn` burns the amount and emits the `CrosschainBurnt` event. + function testFuzz_crosschainBurn_succeeds(address _from, uint256 _amount) public { + // Ensure `_from` is not the zero address vm.assume(_from != ZERO_ADDRESS); - vm.assume(_to != ZERO_ADDRESS); - // Mock the call over the `crossDomainMessageSender` function setting the same address as value - _mockAndExpect( - MESSENGER, - abi.encodeWithSelector(IL2ToL2CrossDomainMessenger.crossDomainMessageSender.selector), - abi.encode(address(superchainERC20)) - ); - - // Mock the call over the `crossDomainMessageSource` function setting the source chain ID as value - _mockAndExpect( - MESSENGER, - abi.encodeWithSelector(IL2ToL2CrossDomainMessenger.crossDomainMessageSource.selector), - abi.encode(_source) - ); - - // Get the total supply and balance of `_to` before the relay to compare later on the assertions + // Mint some tokens to `_from` so then they can be burned + vm.prank(SUPERCHAIN_TOKEN_BRIDGE); + superchainERC20.crosschainMint(_from, _amount); + + // Get the total supply and balance of `_from` before the burn to compare later on the assertions uint256 _totalSupplyBefore = superchainERC20.totalSupply(); - uint256 _toBalanceBefore = superchainERC20.balanceOf(_to); + uint256 _fromBalanceBefore = superchainERC20.balanceOf(_from); // Look for the emit of the `Transfer` event vm.expectEmit(address(superchainERC20)); - emit IERC20.Transfer(ZERO_ADDRESS, _to, _amount); + emit IERC20.Transfer(_from, ZERO_ADDRESS, _amount); - // Look for the emit of the `RelayERC20` event + // Look for the emit of the `CrosschainBurnt` event vm.expectEmit(address(superchainERC20)); - emit ISuperchainERC20Extensions.RelayERC20(_from, _to, _amount, _source); + emit ICrosschainERC20.CrosschainBurnt(_from, _amount); - // Call the `relayERC20` function with the messenger caller - vm.prank(MESSENGER); - superchainERC20.relayERC20(_from, _to, _amount); + // Call the `burn` function with the bridge caller + vm.prank(SUPERCHAIN_TOKEN_BRIDGE); + superchainERC20.crosschainBurn(_from, _amount); - // Check the total supply and balance of `_to` after the relay were updated correctly - assertEq(superchainERC20.totalSupply(), _totalSupplyBefore + _amount); - assertEq(superchainERC20.balanceOf(_to), _toBalanceBefore + _amount); - } - - /// @notice Tests the `name` function always returns the correct value. - function testFuzz_name_succeeds(string memory _name) public { - SuperchainERC20 _newSuperchainERC20 = new SuperchainERC20Mock(_name, SYMBOL, DECIMALS); - assertEq(_newSuperchainERC20.name(), _name); - } - - /// @notice Tests the `symbol` function always returns the correct value. - function testFuzz_symbol_succeeds(string memory _symbol) public { - SuperchainERC20 _newSuperchainERC20 = new SuperchainERC20Mock(NAME, _symbol, DECIMALS); - assertEq(_newSuperchainERC20.symbol(), _symbol); - } - - /// @notice Tests the `decimals` function always returns the correct value. - function testFuzz_decimals_succeeds(uint8 _decimals) public { - SuperchainERC20 _newSuperchainERC20 = new SuperchainERC20Mock(NAME, SYMBOL, _decimals); - assertEq(_newSuperchainERC20.decimals(), _decimals); + // Check the total supply and balance of `_from` after the burn were updated correctly + assertEq(superchainERC20.totalSupply(), _totalSupplyBefore - _amount); + assertEq(superchainERC20.balanceOf(_from), _fromBalanceBefore - _amount); } } diff --git a/packages/contracts-bedrock/test/L2/SuperchainTokenBridge.t.sol b/packages/contracts-bedrock/test/L2/SuperchainTokenBridge.t.sol new file mode 100644 index 000000000000..8367112a5942 --- /dev/null +++ b/packages/contracts-bedrock/test/L2/SuperchainTokenBridge.t.sol @@ -0,0 +1,202 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Testing utilities +import { Bridge_Initializer } from "test/setup/Bridge_Initializer.sol"; + +// Libraries +import { Predeploys } from "src/libraries/Predeploys.sol"; +import { IL2ToL2CrossDomainMessenger } from "src/L2/interfaces/IL2ToL2CrossDomainMessenger.sol"; + +// Target contract +import { ISuperchainTokenBridge } from "src/L2/interfaces/ISuperchainTokenBridge.sol"; +import { ISuperchainERC20 } from "src/L2/interfaces/ISuperchainERC20.sol"; +import { IOptimismSuperchainERC20Factory } from "src/L2/interfaces/IOptimismSuperchainERC20Factory.sol"; +import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol"; + +/// @title SuperchainTokenBridgeTest +/// @notice Contract for testing the SuperchainTokenBridge contract. +contract SuperchainTokenBridgeTest is Bridge_Initializer { + address internal constant ZERO_ADDRESS = address(0); + string internal constant NAME = "SuperchainERC20"; + string internal constant SYMBOL = "OSE"; + address internal constant REMOTE_TOKEN = address(0x123); + + event Transfer(address indexed from, address indexed to, uint256 value); + + event SendERC20( + address indexed token, address indexed from, address indexed to, uint256 amount, uint256 destination + ); + + event RelayERC20(address indexed token, address indexed from, address indexed to, uint256 amount, uint256 source); + + ISuperchainERC20 public superchainERC20; + + /// @notice Sets up the test suite. + function setUp() public override { + super.enableInterop(); + super.setUp(); + + superchainERC20 = ISuperchainERC20( + IOptimismSuperchainERC20Factory(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY).deploy( + REMOTE_TOKEN, NAME, SYMBOL, 18 + ) + ); + } + + /// @notice Helper function to setup a mock and expect a call to it. + function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal { + vm.mockCall(_receiver, _calldata, _returned); + vm.expectCall(_receiver, _calldata); + } + + /// @notice Tests the `sendERC20` function reverts when the address `_to` is zero. + function testFuzz_sendERC20_zeroAddressTo_reverts(address _sender, uint256 _amount, uint256 _chainId) public { + // Expect the revert with `ZeroAddress` selector + vm.expectRevert(ISuperchainTokenBridge.ZeroAddress.selector); + + // Call the `sendERC20` function with the zero address as `_to` + vm.prank(_sender); + superchainTokenBridge.sendERC20(address(superchainERC20), ZERO_ADDRESS, _amount, _chainId); + } + + /// @notice Tests the `sendERC20` function burns the sender tokens, sends the message, and emits the `SendERC20` + /// event. + function testFuzz_sendERC20_succeeds( + address _sender, + address _to, + uint256 _amount, + uint256 _chainId, + bytes32 _msgHash + ) + external + { + // Ensure `_sender` and `_to` is not the zero address + vm.assume(_sender != ZERO_ADDRESS); + vm.assume(_to != ZERO_ADDRESS); + + // Mint some tokens to the sender so then they can be sent + vm.prank(Predeploys.SUPERCHAIN_TOKEN_BRIDGE); + superchainERC20.crosschainMint(_sender, _amount); + + // Get the total supply and balance of `_sender` before the send to compare later on the assertions + uint256 _totalSupplyBefore = IERC20(address(superchainERC20)).totalSupply(); + uint256 _senderBalanceBefore = IERC20(address(superchainERC20)).balanceOf(_sender); + + // Look for the emit of the `Transfer` event + vm.expectEmit(address(superchainERC20)); + emit Transfer(_sender, ZERO_ADDRESS, _amount); + + // Look for the emit of the `SendERC20` event + vm.expectEmit(address(superchainTokenBridge)); + emit SendERC20(address(superchainERC20), _sender, _to, _amount, _chainId); + + // Mock the call over the `sendMessage` function and expect it to be called properly + bytes memory _message = + abi.encodeCall(superchainTokenBridge.relayERC20, (address(superchainERC20), _sender, _to, _amount)); + _mockAndExpect( + Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, + abi.encodeWithSelector( + IL2ToL2CrossDomainMessenger.sendMessage.selector, _chainId, address(superchainTokenBridge), _message + ), + abi.encode(_msgHash) + ); + + // Call the `sendERC20` function + vm.prank(_sender); + bytes32 _returnedMsgHash = superchainTokenBridge.sendERC20(address(superchainERC20), _to, _amount, _chainId); + + // Check the message hash was generated correctly + assertEq(_msgHash, _returnedMsgHash); + + // Check the total supply and balance of `_sender` after the send were updated correctly + assertEq(IERC20(address(superchainERC20)).totalSupply(), _totalSupplyBefore - _amount); + assertEq(IERC20(address(superchainERC20)).balanceOf(_sender), _senderBalanceBefore - _amount); + } + + /// @notice Tests the `relayERC20` function reverts when the caller is not the L2ToL2CrossDomainMessenger. + function testFuzz_relayERC20_notMessenger_reverts( + address _token, + address _caller, + address _to, + uint256 _amount + ) + public + { + // Ensure the caller is not the messenger + vm.assume(_caller != Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); + + // Expect the revert with `Unauthorized` selector + vm.expectRevert(ISuperchainTokenBridge.Unauthorized.selector); + + // Call the `relayERC20` function with the non-messenger caller + vm.prank(_caller); + superchainTokenBridge.relayERC20(_token, _caller, _to, _amount); + } + + /// @notice Tests the `relayERC20` function reverts when the `crossDomainMessageSender` that sent the message is not + /// the same SuperchainTokenBridge. + function testFuzz_relayERC20_notCrossDomainSender_reverts( + address _token, + address _crossDomainMessageSender, + address _to, + uint256 _amount + ) + public + { + vm.assume(_crossDomainMessageSender != address(superchainTokenBridge)); + + // Mock the call over the `crossDomainMessageSender` function setting a wrong sender + vm.mockCall( + Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, + abi.encodeWithSelector(IL2ToL2CrossDomainMessenger.crossDomainMessageSender.selector), + abi.encode(_crossDomainMessageSender) + ); + + // Expect the revert with `InvalidCrossDomainSender` selector + vm.expectRevert(ISuperchainTokenBridge.InvalidCrossDomainSender.selector); + + // Call the `relayERC20` function with the sender caller + vm.prank(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); + superchainTokenBridge.relayERC20(_token, _crossDomainMessageSender, _to, _amount); + } + + /// @notice Tests the `relayERC20` mints the proper amount and emits the `RelayERC20` event. + function testFuzz_relayERC20_succeeds(address _from, address _to, uint256 _amount, uint256 _source) public { + vm.assume(_to != ZERO_ADDRESS); + + // Mock the call over the `crossDomainMessageSender` function setting the same address as value + _mockAndExpect( + Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, + abi.encodeWithSelector(IL2ToL2CrossDomainMessenger.crossDomainMessageSender.selector), + abi.encode(address(superchainTokenBridge)) + ); + + // Mock the call over the `crossDomainMessageSource` function setting the source chain ID as value + _mockAndExpect( + Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, + abi.encodeWithSelector(IL2ToL2CrossDomainMessenger.crossDomainMessageSource.selector), + abi.encode(_source) + ); + + // Get the total supply and balance of `_to` before the relay to compare later on the assertions + uint256 _totalSupplyBefore = IERC20(address(superchainERC20)).totalSupply(); + uint256 _toBalanceBefore = IERC20(address(superchainERC20)).balanceOf(_to); + + // Look for the emit of the `Transfer` event + vm.expectEmit(address(superchainERC20)); + emit Transfer(ZERO_ADDRESS, _to, _amount); + + // Look for the emit of the `RelayERC20` event + vm.expectEmit(address(superchainTokenBridge)); + emit RelayERC20(address(superchainERC20), _from, _to, _amount, _source); + + // Call the `relayERC20` function with the messenger caller + vm.prank(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); + superchainTokenBridge.relayERC20(address(superchainERC20), _from, _to, _amount); + + // Check the total supply and balance of `_to` after the relay were updated correctly + assertEq(IERC20(address(superchainERC20)).totalSupply(), _totalSupplyBefore + _amount); + assertEq(IERC20(address(superchainERC20)).balanceOf(_to), _toBalanceBefore + _amount); + } +} diff --git a/packages/contracts-bedrock/test/L2/SuperchainWETH.t.sol b/packages/contracts-bedrock/test/L2/SuperchainWETH.t.sol index 361e982d7324..c9c523201c6a 100644 --- a/packages/contracts-bedrock/test/L2/SuperchainWETH.t.sol +++ b/packages/contracts-bedrock/test/L2/SuperchainWETH.t.sol @@ -6,11 +6,12 @@ import { CommonTest } from "test/setup/CommonTest.sol"; // Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; -import { Unauthorized, NotCustomGasToken } from "src/libraries/errors/CommonErrors.sol"; +import { NotCustomGasToken } from "src/libraries/errors/CommonErrors.sol"; // Interfaces import { IL2ToL2CrossDomainMessenger } from "src/L2/interfaces/IL2ToL2CrossDomainMessenger.sol"; import { IETHLiquidity } from "src/L2/interfaces/IETHLiquidity.sol"; +import { ISuperchainWETH } from "src/L2/interfaces/ISuperchainWETH.sol"; /// @title SuperchainWETH_Test /// @notice Contract for testing the SuperchainWETH contract. @@ -320,7 +321,7 @@ contract SuperchainWETH_Test is CommonTest { // Nothing to arrange. // Act - vm.expectRevert(Unauthorized.selector); + vm.expectRevert(ISuperchainWETH.CallerNotL2ToL2CrossDomainMessenger.selector); vm.prank(alice); superchainWeth.relayERC20(_sender, bob, _amount); @@ -345,7 +346,7 @@ contract SuperchainWETH_Test is CommonTest { ); // Act - vm.expectRevert(Unauthorized.selector); + vm.expectRevert(ISuperchainWETH.InvalidCrossDomainSender.selector); vm.prank(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); superchainWeth.relayERC20(_sender, bob, _amount); diff --git a/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20/OptimismSuperchainERC20.t.sol b/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20/OptimismSuperchainERC20.t.sol index 4ecbafb05969..d53d2fd29f93 100644 --- a/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20/OptimismSuperchainERC20.t.sol +++ b/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20/OptimismSuperchainERC20.t.sol @@ -6,7 +6,7 @@ import { Test } from "forge-std/Test.sol"; // Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; -import { OptimismSuperchainERC20 } from "src/L2/OptimismSuperchainERC20.sol"; +import { SuperchainERC20 } from "src/L2/SuperchainERC20.sol"; import { ProtocolGuided } from "./fuzz/Protocol.guided.t.sol"; import { ProtocolUnguided } from "./fuzz/Protocol.unguided.t.sol"; import { HandlerGetters } from "./helpers/HandlerGetters.t.sol"; @@ -38,7 +38,7 @@ contract OptimismSuperchainERC20Properties is Test { for (uint256 validChainId = 0; validChainId < handler.MAX_CHAINS(); validChainId++) { address supertoken = MESSENGER.superTokenAddresses(validChainId, currentSalt); if (supertoken != address(0)) { - totalSupply += OptimismSuperchainERC20(supertoken).totalSupply(); + totalSupply += SuperchainERC20(supertoken).totalSupply(); } } assertEq(trackedSupply, totalSupply + fundsInTransit); @@ -61,7 +61,7 @@ contract OptimismSuperchainERC20Properties is Test { for (uint256 validChainId = 0; validChainId < handler.MAX_CHAINS(); validChainId++) { address supertoken = MESSENGER.superTokenAddresses(validChainId, currentSalt); if (supertoken != address(0)) { - totalSupply += OptimismSuperchainERC20(supertoken).totalSupply(); + totalSupply += SuperchainERC20(supertoken).totalSupply(); } } assertEq(trackedSupply, totalSupply); @@ -69,7 +69,7 @@ contract OptimismSuperchainERC20Properties is Test { } /// @custom:invariant many other assertion mode invariants are also defined under - /// `test/invariants/OptimismSuperchainERC20/fuzz/` . + /// `test/invariants/SuperchainERC20/fuzz/` . /// /// since setting`fail_on_revert=false` also ignores StdAssertion failures, this invariant explicitly asks the /// handler for assertion test failures diff --git a/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20/PROPERTIES.md b/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20/PROPERTIES.md index 18970855ca46..1c8c90bb0301 100644 --- a/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20/PROPERTIES.md +++ b/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20/PROPERTIES.md @@ -1,5 +1,9 @@ # Supertoken advanced testing +## Note + +This campaign will need to be updated the redesign `OptimismSuperchainERC20` redesign. Please delete this comment once the update is done. + ## Milestones The supertoken ecosystem consists of not just the supertoken contract, but the required changes to other contracts for liquidity to reach the former. @@ -12,8 +16,8 @@ Considering only the supertoken contract is merged into the `develop` branch, an ## Definitions -- *legacy token:* an OptimismMintableERC20 or L2StandardERC20 token on the suprechain that has either been deployed by the factory after the liquidity migration upgrade to the latter, or has been deployed before it **but** added to factory’s `deployments` mapping as part of the upgrade. This testing campaign is not concerned with tokens on L1 or not listed in the factory’s `deployments` mapping. -- *supertoken:* a SuperchainERC20 contract deployed by the `OptimismSuperchainERC20Factory` +- _legacy token:_ an OptimismMintableERC20 or L2StandardERC20 token on the suprechain that has either been deployed by the factory after the liquidity migration upgrade to the latter, or has been deployed before it **but** added to factory’s `deployments` mapping as part of the upgrade. This testing campaign is not concerned with tokens on L1 or not listed in the factory’s `deployments` mapping. +- _supertoken:_ a SuperchainERC20 contract deployed by the `OptimismSuperchainERC20Factory` # Ecosystem properties @@ -28,7 +32,7 @@ legend: ## Unit test | id | milestone | description | tested | -| --- | --- | --- | --- | +| --- | ------------------- | ------------------------------------------------------------------------------------------ | ------ | | 0 | Factories | supertoken token address does not depend on the executing chain’s chainID | [ ] | | 1 | Factories | supertoken token address depends on remote token, name, symbol and decimals | [ ] | | 2 | Liquidity Migration | convert() should only allow converting legacy tokens to supertoken and viceversa | [ ] | @@ -40,18 +44,18 @@ legend: ## Valid state | id | milestone | description | tested | -| --- | --- | --- | --- | -| 6 | SupERC20 | calls to sendERC20 succeed as long as caller has enough balance | [x] | -| 7 | SupERC20 | calls to relayERC20 always succeed as long as the cross-domain caller is valid | [~] | +| --- | --------- | ------------------------------------------------------------------------------ | ------ | +| 6 | SupERC20 | calls to sendERC20 succeed as long as caller has enough balance | [] | +| 7 | SupERC20 | calls to relayERC20 always succeed as long as the cross-domain caller is valid | [] | ## Variable transition | id | milestone | description | tested | -| --- | --- | --- | --- | -| 8 | SupERC20 | sendERC20 with a value of zero does not modify accounting | [x] | -| 9 | SupERC20 | relayERC20 with a value of zero does not modify accounting | [x] | -| 10 | SupERC20 | sendERC20 decreases the token's totalSupply in the source chain exactly by the input amount | [x] | -| 26 | SupERC20 | sendERC20 decreases the sender's balance in the source chain exactly by the input amount | [x] | +| --- | ------------------- | ------------------------------------------------------------------------------------------------- | ------ | +| 8 | SupERC20 | sendERC20 with a value of zero does not modify accounting | [] | +| 9 | SupERC20 | relayERC20 with a value of zero does not modify accounting | [] | +| 10 | SupERC20 | sendERC20 decreases the token's totalSupply in the source chain exactly by the input amount | [] | +| 26 | SupERC20 | sendERC20 decreases the sender's balance in the source chain exactly by the input amount | [] | | 27 | SupERC20 | relayERC20 increases sender's balance in the destination chain exactly by the input amount | [x] | | 11 | SupERC20 | relayERC20 increases the token's totalSupply in the destination chain exactly by the input amount | [ ] | | 12 | Liquidity Migration | supertoken total supply only increases on calls to mint() by the L2toL2StandardBridge | [~] | @@ -63,9 +67,9 @@ legend: ## High level | id | milestone | description | tested | -| --- | --- | --- | --- | -| 17 | Liquidity Migration | only calls to convert(legacy, super) can increase a supertoken’s total supply across chains | [ ] | -| 18 | Liquidity Migration | only calls to convert(super, legacy) can decrease a supertoken’s total supply across chains | [ ] | +| --- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | +| 17 | Liquidity Migration | only calls to convert(legacy, super) can increase a supertoken’s total supply across chains | [ ] | +| 18 | Liquidity Migration | only calls to convert(super, legacy) can decrease a supertoken’s total supply across chains | [ ] | | 19 | Liquidity Migration | sum of supertoken total supply across all chains is always <= to convert(legacy, super)- convert(super, legacy) | [~] | | 20 | SupERC20 | tokens sendERC20-ed on a source chain to a destination chain can be relayERC20-ed on it as long as the source chain is in the dependency set of the destination chain | [ ] | | 21 | Liquidity Migration | sum of supertoken total supply across all chains is = to convert(legacy, super)- convert(super, legacy) when all cross-chain messages are processed | [~] | @@ -76,7 +80,7 @@ As another layer of defense, the following properties are defined which assume b It’s worth noting that these properties will not hold for a live system | id | milestone | description | tested | -| --- | --- | --- | --- | -| 22 | SupERC20 | sendERC20 decreases sender balance in source chain and increases receiver balance in destination chain exactly by the input amount | [x] | -| 23 | SupERC20 | sendERC20 decreases total supply in source chain and increases it in destination chain exactly by the input amount | [x] | +| --- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------ | +| 22 | SupERC20 | sendERC20 decreases sender balance in source chain and increases receiver balance in destination chain exactly by the input amount | [] | +| 23 | SupERC20 | sendERC20 decreases total supply in source chain and increases it in destination chain exactly by the input amount | [] | | 24 | Liquidity Migration | sum of supertoken total supply across all chains is always equal to convert(legacy, super)- convert(super, legacy) | [~] | diff --git a/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20/fuzz/Protocol.guided.t.sol b/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20/fuzz/Protocol.guided.t.sol index 536a4ea7025a..236c41873b6c 100644 --- a/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20/fuzz/Protocol.guided.t.sol +++ b/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20/fuzz/Protocol.guided.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.25; import { MockL2ToL2CrossDomainMessenger } from "../helpers/MockL2ToL2CrossDomainMessenger.t.sol"; -import { OptimismSuperchainERC20 } from "src/L2/OptimismSuperchainERC20.sol"; +import { SuperchainERC20 } from "src/L2/SuperchainERC20.sol"; import { ProtocolHandler } from "../handlers/Protocol.t.sol"; import { EnumerableMap } from "@openzeppelin/contracts/utils/structs/EnumerableMap.sol"; import { CompatibleAssert } from "../helpers/CompatibleAssert.t.sol"; @@ -21,7 +21,7 @@ contract ProtocolGuided is ProtocolHandler, CompatibleAssert { validateTokenDeployParams(params) { chainId = bound(chainId, 0, MAX_CHAINS - 1); - OptimismSuperchainERC20 supertoken = _deploySupertoken( + SuperchainERC20 supertoken = _deploySupertoken( remoteTokens[params.remoteTokenIndex], WORDS[params.nameIndex], WORDS[params.symbolIndex], @@ -32,100 +32,6 @@ contract ProtocolGuided is ProtocolHandler, CompatibleAssert { compatibleAssert(supertoken.totalSupply() == 0); } - /// @custom:property-id 6 - /// @custom:property calls to sendERC20 succeed as long as caller has enough balance - /// @custom:property-id 22 - /// @custom:property sendERC20 decreases sender balance in source chain and increases receiver balance in - /// destination chain exactly by the input amount - /// @custom:property-id 23 - /// @custom:property sendERC20 decreases total supply in source chain and increases it in destination chain exactly - /// by the input amount - function fuzz_bridgeSupertokenAtomic( - uint256 fromIndex, - uint256 recipientIndex, - uint256 destinationChainId, - uint256 amount - ) - public - withActor(msg.sender) - { - destinationChainId = bound(destinationChainId, 0, MAX_CHAINS - 1); - fromIndex = bound(fromIndex, 0, allSuperTokens.length - 1); - address recipient = getActorByRawIndex(recipientIndex); - OptimismSuperchainERC20 sourceToken = OptimismSuperchainERC20(allSuperTokens[fromIndex]); - OptimismSuperchainERC20 destinationToken = - MESSENGER.crossChainMessageReceiver(address(sourceToken), destinationChainId); - uint256 sourceBalanceBefore = sourceToken.balanceOf(currentActor()); - uint256 sourceSupplyBefore = sourceToken.totalSupply(); - uint256 destinationBalanceBefore = destinationToken.balanceOf(recipient); - uint256 destinationSupplyBefore = destinationToken.totalSupply(); - - MESSENGER.setAtomic(true); - vm.prank(currentActor()); - try sourceToken.sendERC20(recipient, amount, destinationChainId) { - MESSENGER.setAtomic(false); - uint256 sourceBalanceAfter = sourceToken.balanceOf(currentActor()); - uint256 destinationBalanceAfter = destinationToken.balanceOf(recipient); - // no free mint - compatibleAssert( - sourceBalanceBefore + destinationBalanceBefore == sourceBalanceAfter + destinationBalanceAfter - ); - // 22 - compatibleAssert(sourceBalanceBefore - amount == sourceBalanceAfter); - compatibleAssert(destinationBalanceBefore + amount == destinationBalanceAfter); - uint256 sourceSupplyAfter = sourceToken.totalSupply(); - uint256 destinationSupplyAfter = destinationToken.totalSupply(); - // 23 - compatibleAssert(sourceSupplyBefore - amount == sourceSupplyAfter); - compatibleAssert(destinationSupplyBefore + amount == destinationSupplyAfter); - } catch { - MESSENGER.setAtomic(false); - // 6 - compatibleAssert(address(destinationToken) == address(sourceToken) || sourceBalanceBefore < amount); - } - } - - /// @custom:property-id 6 - /// @custom:property calls to sendERC20 succeed as long as caller has enough balance - /// @custom:property-id 26 - /// @custom:property sendERC20 decreases sender balance in source chain exactly by the input amount - /// @custom:property-id 10 - /// @custom:property sendERC20 decreases total supply in source chain exactly by the input amount - function fuzz_sendERC20( - uint256 fromIndex, - uint256 recipientIndex, - uint256 destinationChainId, - uint256 amount - ) - public - withActor(msg.sender) - { - destinationChainId = bound(destinationChainId, 0, MAX_CHAINS - 1); - fromIndex = bound(fromIndex, 0, allSuperTokens.length - 1); - address recipient = getActorByRawIndex(recipientIndex); - OptimismSuperchainERC20 sourceToken = OptimismSuperchainERC20(allSuperTokens[fromIndex]); - OptimismSuperchainERC20 destinationToken = - MESSENGER.crossChainMessageReceiver(address(sourceToken), destinationChainId); - bytes32 deploySalt = MESSENGER.superTokenInitDeploySalts(address(sourceToken)); - uint256 sourceBalanceBefore = sourceToken.balanceOf(currentActor()); - uint256 sourceSupplyBefore = sourceToken.totalSupply(); - - vm.prank(currentActor()); - try sourceToken.sendERC20(recipient, amount, destinationChainId) { - (, uint256 currentlyInTransit) = ghost_tokensInTransit.tryGet(deploySalt); - ghost_tokensInTransit.set(deploySalt, currentlyInTransit + amount); - // 26 - uint256 sourceBalanceAfter = sourceToken.balanceOf(currentActor()); - compatibleAssert(sourceBalanceBefore - amount == sourceBalanceAfter); - // 10 - uint256 sourceSupplyAfter = sourceToken.totalSupply(); - compatibleAssert(sourceSupplyBefore - amount == sourceSupplyAfter); - } catch { - // 6 - compatibleAssert(address(destinationToken) == address(sourceToken) || sourceBalanceBefore < amount); - } - } - /// @custom:property-id 11 /// @custom:property relayERC20 increases the token's totalSupply in the destination chain exactly by the input /// amount @@ -135,7 +41,7 @@ contract ProtocolGuided is ProtocolHandler, CompatibleAssert { /// @custom:property calls to relayERC20 always succeed as long as the cross-domain caller is valid function fuzz_relayERC20(uint256 messageIndex) external { MockL2ToL2CrossDomainMessenger.CrossChainMessage memory messageToRelay = MESSENGER.messageQueue(messageIndex); - OptimismSuperchainERC20 destinationToken = OptimismSuperchainERC20(messageToRelay.crossDomainMessageSender); + SuperchainERC20 destinationToken = SuperchainERC20(messageToRelay.crossDomainMessageSender); uint256 destinationSupplyBefore = destinationToken.totalSupply(); uint256 destinationBalanceBefore = destinationToken.balanceOf(messageToRelay.recipient); @@ -156,56 +62,4 @@ contract ProtocolGuided is ProtocolHandler, CompatibleAssert { compatibleAssert(false); } } - - /// @custom:property-id 8 - /// @custom:property calls to sendERC20 with a value of zero dont modify accounting - // @notice is a subset of fuzz_sendERC20, so we'll just call it - // instead of re-implementing it. Keeping the function for visibility of the property. - function fuzz_sendZeroDoesNotModifyAccounting( - uint256 fromIndex, - uint256 recipientIndex, - uint256 destinationChainId - ) - external - { - fuzz_sendERC20(fromIndex, recipientIndex, destinationChainId, 0); - } - - /// @custom:property-id 9 - /// @custom:property calls to relayERC20 with a value of zero dont modify accounting - /// @custom:property-id 7 - /// @custom:property calls to relayERC20 always succeed as long as the cross-domain caller is valid - /// @notice cant call fuzz_RelayERC20 internally since that pops a - /// random message, which we cannot guarantee has a value of zero - function fuzz_relayZeroDoesNotModifyAccounting( - uint256 fromIndex, - uint256 recipientIndex - ) - external - withActor(msg.sender) - { - fromIndex = bound(fromIndex, 0, allSuperTokens.length - 1); - address recipient = getActorByRawIndex(recipientIndex); - OptimismSuperchainERC20 token = OptimismSuperchainERC20(allSuperTokens[fromIndex]); - uint256 balanceSenderBefore = token.balanceOf(currentActor()); - uint256 balanceRecipientBefore = token.balanceOf(recipient); - uint256 supplyBefore = token.totalSupply(); - - MESSENGER.setCrossDomainMessageSender(address(token)); - vm.prank(address(MESSENGER)); - try token.relayERC20(currentActor(), recipient, 0) { - MESSENGER.setCrossDomainMessageSender(address(0)); - } catch { - // should not revert because of 7, and if it *does* revert, I want the test suite - // to discard the sequence instead of potentially getting another - // error due to the crossDomainMessageSender being manually set - compatibleAssert(false); - } - uint256 balanceSenderAfter = token.balanceOf(currentActor()); - uint256 balanceRecipeintAfter = token.balanceOf(recipient); - uint256 supplyAfter = token.totalSupply(); - compatibleAssert(balanceSenderBefore == balanceSenderAfter); - compatibleAssert(balanceRecipientBefore == balanceRecipeintAfter); - compatibleAssert(supplyBefore == supplyAfter); - } } diff --git a/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20/fuzz/Protocol.unguided.t.sol b/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20/fuzz/Protocol.unguided.t.sol index 90cad38baa99..aa3eaaa93134 100644 --- a/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20/fuzz/Protocol.unguided.t.sol +++ b/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20/fuzz/Protocol.unguided.t.sol @@ -10,78 +10,6 @@ import { CompatibleAssert } from "../helpers/CompatibleAssert.t.sol"; contract ProtocolUnguided is ProtocolHandler, CompatibleAssert { using EnumerableMap for EnumerableMap.Bytes32ToUintMap; - /// @custom:property-id 7 - /// @custom:property calls to relayERC20 always succeed as long as the cross-domain caller is valid - /// @notice this ensures actors cant simply call relayERC20 and get tokens, no matter the system state - /// but there's still some possible work on how hard we can bork the system state with handlers calling - /// the L2ToL2CrossDomainMessenger or bridge directly (pending on non-atomic bridging) - function fuzz_relayERC20( - uint256 tokenIndex, - address sender, - address crossDomainMessageSender, - address recipient, - uint256 amount - ) - external - { - MESSENGER.setCrossDomainMessageSender(crossDomainMessageSender); - address token = allSuperTokens[bound(tokenIndex, 0, allSuperTokens.length)]; - vm.prank(sender); - try OptimismSuperchainERC20(token).relayERC20(sender, recipient, amount) { - MESSENGER.setCrossDomainMessageSender(address(0)); - compatibleAssert(sender == address(MESSENGER)); - compatibleAssert(crossDomainMessageSender == token); - // this increases the supply across chains without a call to - // `mint` by the MESSENGER, so it kind of breaks an invariant, but - // let's walk around that: - bytes32 salt = MESSENGER.superTokenInitDeploySalts(token); - (, uint256 currentValue) = ghost_totalSupplyAcrossChains.tryGet(salt); - ghost_totalSupplyAcrossChains.set(salt, currentValue + amount); - } catch { - compatibleAssert(sender != address(MESSENGER) || crossDomainMessageSender != token); - MESSENGER.setCrossDomainMessageSender(address(0)); - } - } - - /// @custom:property-id 6 - /// @custom:property calls to sendERC20 succeed as long as caller has enough balance - /// @custom:property-id 26 - /// @custom:property sendERC20 decreases sender balance in source chain exactly by the input amount - /// @custom:property-id 10 - /// @custom:property sendERC20 decreases total supply in source chain exactly by the input amount - function fuzz_sendERC20( - address sender, - address recipient, - uint256 fromIndex, - uint256 destinationChainId, - uint256 amount - ) - public - { - destinationChainId = bound(destinationChainId, 0, MAX_CHAINS - 1); - OptimismSuperchainERC20 sourceToken = OptimismSuperchainERC20(allSuperTokens[fromIndex]); - OptimismSuperchainERC20 destinationToken = - MESSENGER.crossChainMessageReceiver(address(sourceToken), destinationChainId); - bytes32 deploySalt = MESSENGER.superTokenInitDeploySalts(address(sourceToken)); - uint256 sourceBalanceBefore = sourceToken.balanceOf(sender); - uint256 sourceSupplyBefore = sourceToken.totalSupply(); - - vm.prank(sender); - try sourceToken.sendERC20(recipient, amount, destinationChainId) { - (, uint256 currentlyInTransit) = ghost_tokensInTransit.tryGet(deploySalt); - ghost_tokensInTransit.set(deploySalt, currentlyInTransit + amount); - // 26 - uint256 sourceBalanceAfter = sourceToken.balanceOf(sender); - compatibleAssert(sourceBalanceBefore - amount == sourceBalanceAfter); - // 10 - uint256 sourceSupplyAfter = sourceToken.totalSupply(); - compatibleAssert(sourceSupplyBefore - amount == sourceSupplyAfter); - } catch { - // 6 - compatibleAssert(address(destinationToken) == address(sourceToken) || sourceBalanceBefore < amount); - } - } - /// @custom:property-id 12 /// @custom:property supertoken total supply only increases on calls to mint() by the L2toL2StandardBridge function fuzz_mint(uint256 tokenIndex, address to, address sender, uint256 amount) external { @@ -89,7 +17,7 @@ contract ProtocolUnguided is ProtocolHandler, CompatibleAssert { bytes32 salt = MESSENGER.superTokenInitDeploySalts(token); amount = bound(amount, 0, type(uint256).max - OptimismSuperchainERC20(token).totalSupply()); vm.prank(sender); - try OptimismSuperchainERC20(token).mint(to, amount) { + try OptimismSuperchainERC20(token).crosschainMint(to, amount) { compatibleAssert(sender == BRIDGE); (, uint256 currentValue) = ghost_totalSupplyAcrossChains.tryGet(salt); ghost_totalSupplyAcrossChains.set(salt, currentValue + amount); @@ -105,7 +33,7 @@ contract ProtocolUnguided is ProtocolHandler, CompatibleAssert { bytes32 salt = MESSENGER.superTokenInitDeploySalts(token); uint256 senderBalance = OptimismSuperchainERC20(token).balanceOf(sender); vm.prank(sender); - try OptimismSuperchainERC20(token).burn(from, amount) { + try OptimismSuperchainERC20(token).crosschainBurn(from, amount) { compatibleAssert(sender == BRIDGE); (, uint256 currentValue) = ghost_totalSupplyAcrossChains.tryGet(salt); ghost_totalSupplyAcrossChains.set(salt, currentValue - amount); diff --git a/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20/handlers/Protocol.t.sol b/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20/handlers/Protocol.t.sol index 921495b467ab..d4a12c4067bd 100644 --- a/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20/handlers/Protocol.t.sol +++ b/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20/handlers/Protocol.t.sol @@ -79,7 +79,7 @@ contract ProtocolHandler is TestBase, StdUtils, Actors { index = bound(index, 0, allSuperTokens.length - 1); address addr = allSuperTokens[index]; vm.prank(BRIDGE); - OptimismSuperchainERC20(addr).mint(currentActor(), amount); + OptimismSuperchainERC20(addr).crosschainMint(currentActor(), amount); // currentValue will be zero if key is not present (, uint256 currentValue) = ghost_totalSupplyAcrossChains.tryGet(MESSENGER.superTokenInitDeploySalts(addr)); ghost_totalSupplyAcrossChains.set(MESSENGER.superTokenInitDeploySalts(addr), currentValue + amount); @@ -164,7 +164,7 @@ contract ProtocolHandler is TestBase, StdUtils, Actors { bytes32 hackySalt = keccak256(abi.encode(remoteToken, name, symbol, decimals, chainId)); supertoken = OptimismSuperchainERC20( address( - // TODO: Use the SuperchainERC20 Beacon Proxy + // TODO: Use the OptimismSuperchainERC20 Beacon Proxy new ERC1967Proxy{ salt: hackySalt }( address(superchainERC20Impl), abi.encodeCall(OptimismSuperchainERC20.initialize, (remoteToken, name, symbol, decimals)) diff --git a/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20/helpers/MockL2ToL2CrossDomainMessenger.t.sol b/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20/helpers/MockL2ToL2CrossDomainMessenger.t.sol index 6eb1c30e6799..b5d70596ed63 100644 --- a/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20/helpers/MockL2ToL2CrossDomainMessenger.t.sol +++ b/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20/helpers/MockL2ToL2CrossDomainMessenger.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.25; -import { OptimismSuperchainERC20 } from "src/L2/OptimismSuperchainERC20.sol"; +import { SuperchainERC20 } from "src/L2/SuperchainERC20.sol"; import { SafeCall } from "src/libraries/SafeCall.sol"; contract MockL2ToL2CrossDomainMessenger { @@ -41,9 +41,9 @@ contract MockL2ToL2CrossDomainMessenger { ) external view - returns (OptimismSuperchainERC20) + returns (SuperchainERC20) { - return OptimismSuperchainERC20(superTokenAddresses[destinationChainId][superTokenInitDeploySalts[sender]]); + return SuperchainERC20(superTokenAddresses[destinationChainId][superTokenInitDeploySalts[sender]]); } function setCrossDomainMessageSender(address sender) external { diff --git a/packages/contracts-bedrock/test/invariants/SuperchainWETH.t.sol b/packages/contracts-bedrock/test/invariants/SuperchainWETH.t.sol index 1e761b7ea162..e9b8a3be828a 100644 --- a/packages/contracts-bedrock/test/invariants/SuperchainWETH.t.sol +++ b/packages/contracts-bedrock/test/invariants/SuperchainWETH.t.sol @@ -10,7 +10,7 @@ import { CommonTest } from "test/setup/CommonTest.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; // Interfaces -import { ISuperchainWETH } from "src/L2/interfaces/ISuperchainWETH.sol"; +import { ISuperchainWETHERC20 } from "src/L2/interfaces/ISuperchainWETH.sol"; import { IL2ToL2CrossDomainMessenger } from "src/L2/interfaces/IL2ToL2CrossDomainMessenger.sol"; /// @title SuperchainWETH_User @@ -29,7 +29,7 @@ contract SuperchainWETH_User is StdUtils { Vm internal vm; /// @notice The SuperchainWETH contract. - ISuperchainWETH internal weth; + ISuperchainWETHERC20 internal weth; /// @notice Mapping of sent messages. mapping(bytes32 => bool) internal sent; @@ -40,7 +40,7 @@ contract SuperchainWETH_User is StdUtils { /// @param _vm The Vm contract. /// @param _weth The SuperchainWETH contract. /// @param _balance The initial balance of the contract. - constructor(Vm _vm, ISuperchainWETH _weth, uint256 _balance) { + constructor(Vm _vm, ISuperchainWETHERC20 _weth, uint256 _balance) { vm = _vm; weth = _weth; vm.deal(address(this), _balance); diff --git a/packages/contracts-bedrock/test/mocks/SuperchainERC20Implementation.sol b/packages/contracts-bedrock/test/mocks/SuperchainERC20Implementation.sol new file mode 100644 index 000000000000..f0ef5f882f98 --- /dev/null +++ b/packages/contracts-bedrock/test/mocks/SuperchainERC20Implementation.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import { SuperchainERC20 } from "src/L2/SuperchainERC20.sol"; + +/// @title SuperchainERC20Implementation Mock contract +/// @notice Mock contract just to create tests over an implementation of the SuperchainERC20 abstract contract. +contract MockSuperchainERC20Implementation is SuperchainERC20 { + function name() public pure override returns (string memory) { + return "SuperchainERC20"; + } + + function symbol() public pure override returns (string memory) { + return "SCE"; + } +} diff --git a/packages/contracts-bedrock/test/setup/Setup.sol b/packages/contracts-bedrock/test/setup/Setup.sol index 32fe86b66b9f..7e5cebd7f803 100644 --- a/packages/contracts-bedrock/test/setup/Setup.sol +++ b/packages/contracts-bedrock/test/setup/Setup.sol @@ -37,17 +37,18 @@ import { IL2ToL1MessagePasser } from "src/L2/interfaces/IL2ToL1MessagePasser.sol import { IL2ERC721Bridge } from "src/L2/interfaces/IL2ERC721Bridge.sol"; import { IOptimismMintableERC20Factory } from "src/universal/interfaces/IOptimismMintableERC20Factory.sol"; import { IAddressManager } from "src/legacy/interfaces/IAddressManager.sol"; -import { IOptimismERC20Factory } from "src/L2/interfaces/IOptimismERC20Factory.sol"; +import { IOptimismSuperchainERC20Factory } from "src/L2/interfaces/IOptimismSuperchainERC20Factory.sol"; import { IBaseFeeVault } from "src/L2/interfaces/IBaseFeeVault.sol"; import { ISequencerFeeVault } from "src/L2/interfaces/ISequencerFeeVault.sol"; import { IL1FeeVault } from "src/L2/interfaces/IL1FeeVault.sol"; import { IGasPriceOracle } from "src/L2/interfaces/IGasPriceOracle.sol"; import { IL1Block } from "src/L2/interfaces/IL1Block.sol"; -import { ISuperchainWETH } from "src/L2/interfaces/ISuperchainWETH.sol"; +import { ISuperchainWETHERC20 } from "src/L2/interfaces/ISuperchainWETH.sol"; import { IETHLiquidity } from "src/L2/interfaces/IETHLiquidity.sol"; import { IWETH } from "src/universal/interfaces/IWETH.sol"; import { IGovernanceToken } from "src/governance/interfaces/IGovernanceToken.sol"; import { ILegacyMessagePasser } from "src/legacy/interfaces/ILegacyMessagePasser.sol"; +import { ISuperchainTokenBridge } from "src/L2/interfaces/ISuperchainTokenBridge.sol"; /// @title Setup /// @dev This contact is responsible for setting up the contracts in state. It currently @@ -105,12 +106,11 @@ contract Setup { IGovernanceToken governanceToken = IGovernanceToken(Predeploys.GOVERNANCE_TOKEN); ILegacyMessagePasser legacyMessagePasser = ILegacyMessagePasser(Predeploys.LEGACY_MESSAGE_PASSER); IWETH weth = IWETH(payable(Predeploys.WETH)); - ISuperchainWETH superchainWeth = ISuperchainWETH(payable(Predeploys.SUPERCHAIN_WETH)); + ISuperchainWETHERC20 superchainWeth = ISuperchainWETHERC20(payable(Predeploys.SUPERCHAIN_WETH)); IETHLiquidity ethLiquidity = IETHLiquidity(Predeploys.ETH_LIQUIDITY); - - // TODO: Replace with OptimismSuperchainERC20Factory when updating pragmas - IOptimismERC20Factory l2OptimismSuperchainERC20Factory = - IOptimismERC20Factory(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY); + ISuperchainTokenBridge superchainTokenBridge = ISuperchainTokenBridge(Predeploys.SUPERCHAIN_TOKEN_BRIDGE); + IOptimismSuperchainERC20Factory l2OptimismSuperchainERC20Factory = + IOptimismSuperchainERC20Factory(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY); /// @dev Deploys the Deploy contract without including its bytecode in the bytecode /// of this contract by fetching the bytecode dynamically using `vm.getCode()`. @@ -236,6 +236,7 @@ contract Setup { labelPredeploy(Predeploys.ETH_LIQUIDITY); labelPredeploy(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY); labelPredeploy(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_BEACON); + labelPredeploy(Predeploys.SUPERCHAIN_TOKEN_BRIDGE); // L2 Preinstalls labelPreinstall(Preinstalls.MultiCall3); From c19f54d4739c4bb20f3768bc1303605aa888cc1d Mon Sep 17 00:00:00 2001 From: 0xDiscotech <131301107+0xDiscotech@users.noreply.github.com> Date: Tue, 15 Oct 2024 17:45:10 -0300 Subject: [PATCH 31/31] chore: run pre-pr --- packages/contracts-bedrock/semver-lock.json | 6 +++--- .../snapshots/abi/OptimismSuperchainERC20.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/contracts-bedrock/semver-lock.json b/packages/contracts-bedrock/semver-lock.json index 7d98febec072..924b7e1008d7 100644 --- a/packages/contracts-bedrock/semver-lock.json +++ b/packages/contracts-bedrock/semver-lock.json @@ -108,8 +108,8 @@ "sourceCodeHash": "0xfea53344596d735eff3be945ed1300dc75a6f8b7b2c02c0043af5b0036f5f239" }, "src/L2/OptimismSuperchainERC20.sol": { - "initCodeHash": "0xa1d74ae6bc6a0fcc851d18e4147adc4ee151efc0bacfea54aeaae22c72ef3e26", - "sourceCodeHash": "0xcb705d26e63e733051c8bd442ea69ce637a00c16d646ccc37b687b20941366fe" + "initCodeHash": "0x4e25579079d73c93f1d494e1976334b77fc4ec181c67f376d8e2613c7b207f52", + "sourceCodeHash": "0xcace3875e6f24239fc84039d3738a53e442766f1f819d8984f7a55d21610f472" }, "src/L2/OptimismSuperchainERC20Beacon.sol": { "initCodeHash": "0x99ce8095b23c124850d866cbc144fee6cee05dbc6bb5d83acadfe00b90cf42c7", @@ -235,4 +235,4 @@ "initCodeHash": "0x06ae2c0b39c215b7fa450d382916ce6f5c6f9f2d630e572db6b72d688255b3fd", "sourceCodeHash": "0xa014d9c992f439dee8221e065828c3326ca2c4f5db0e83431c64c20f7e51ec14" } -} +} \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json b/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json index b4f89adb61c7..d6ad63fad9c3 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json @@ -595,4 +595,4 @@ "name": "ZeroAddress", "type": "error" } -] +] \ No newline at end of file