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] 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);