diff --git a/.env-sample b/.env-sample index 0fd2718b05..4a215dc6c3 100644 --- a/.env-sample +++ b/.env-sample @@ -1,13 +1,24 @@ +## RPC endpoint +BASECHAIN_RPC="" + +## Deployer key used for deploying creator or creating token bridge +BASECHAIN_DEPLOYER_KEY="" + +## WETH address on the basechain. It will be set and used in the TokenBridgeCreator +BASECHAIN_WETH="" + +## Gas limit for deploying child chain factory needs to be provided to the TokenBridgeCreator when templates are set. +## If this param is not provided then gas limit will be estimated using SDK from child chain (specified by ORBIT_RPC and ROLLUP_ADDRESS) +GAS_LIMIT_FOR_L2_FACTORY_DEPLOYMENT=6000000 + +## Contract verification +ARBISCAN_API_KEY="" + +### Vars for creating token bridge from existing TokenBridgeCreator ## Rollup on top of which token bridge will be created ROLLUP_ADDRESS="" +ORBIT_RPC="" ROLLUP_OWNER="" L1_TOKEN_BRIDGE_CREATOR="" # needed for verification -L1_RETRYABLE_SENDER="" - -## RPC endpoints -BASECHAIN_RPC="" -ORBIT_RPC="" - -## Deployer key used for deploying creator and creating token bridge -BASECHAIN_DEPLOYER_KEY="" +L1_RETRYABLE_SENDER="" \ No newline at end of file diff --git a/.env.arb1.sample b/.env.arb1.sample new file mode 100644 index 0000000000..d24320e04b --- /dev/null +++ b/.env.arb1.sample @@ -0,0 +1,25 @@ +## RPC endpoint +BASECHAIN_RPC="https://arb1.arbitrum.io/rpc" + +## Deployer key used for deploying creator or creating token bridge +BASECHAIN_DEPLOYER_KEY="" + +## WETH address on the basechain. It will be set and used in the TokenBridgeCreator +BASECHAIN_WETH="0x82aF49447D8a07e3bd95BD0d56f35241523fBab1" + +## Gas limit for deploying child chain factory needs to be provided to the TokenBridgeCreator when templates are set. +## If this param is not provided then gas limit will be estimated using SDK from child chain (specified by ORBIT_RPC and ROLLUP_ADDRESS) +GAS_LIMIT_FOR_L2_FACTORY_DEPLOYMENT=6000000 + +## Contract verification +ARBISCAN_API_KEY="" + + +### Vars for creating token bridge from existing TokenBridgeCreator +## Rollup on top of which token bridge will be created +# ROLLUP_ADDRESS="" +# ORBIT_RPC="" +# ROLLUP_OWNER="" +# L1_TOKEN_BRIDGE_CREATOR="" +# # needed for verification +# L1_RETRYABLE_SENDER="" \ No newline at end of file diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index aba7a24a69..9c778f4e64 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -94,3 +94,76 @@ jobs: - name: Test Storage Layouts run: yarn run test:storage + + test-e2e: + name: Test e2e + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - uses: OffchainLabs/actions/run-nitro-test-node@main + with: + nitro-testnode-ref: bump-nitro + l3-node: true + no-token-bridge: true + + - name: Setup node/yarn + uses: actions/setup-node@v3 + with: + node-version: 16 + cache: 'yarn' + cache-dependency-path: '**/yarn.lock' + + - name: Install packages + run: yarn + + - name: Compile contracts + run: yarn build + + - name: Deploy creator and create token bridge + run: yarn deploy:local:token-bridge + + - name: Verify deployed token bridge + run: yarn test:tokenbridge:deployment + + - name: Verify creation code generation + run: yarn test:creation-code + + test-e2e-custom-fee-token: + name: Test e2e on custom fee token chain + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - uses: OffchainLabs/actions/run-nitro-test-node@main + with: + nitro-testnode-ref: bump-nitro + l3-node: true + args: --l3-fee-token + no-token-bridge: true + + - name: Setup node/yarn + uses: actions/setup-node@v3 + with: + node-version: 16 + cache: 'yarn' + cache-dependency-path: '**/yarn.lock' + + - name: Install packages + run: yarn + + - name: Compile contracts + run: yarn build + + - name: Deploy creator and create token bridge + run: yarn deploy:local:token-bridge + + - name: Verify deployed token bridge + run: yarn test:tokenbridge:deployment + + - name: Verify creation code generation + run: yarn test:creation-code \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index e19917d6cd..99139623ee 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,4 +4,4 @@ [submodule "lib/nitro-contracts"] path = lib/nitro-contracts url = git@github.com:OffchainLabs/nitro-contracts.git - branch = feature-orbit-bridge + branch = v1.1.0 diff --git a/README.md b/README.md index f244316179..cabc4a1656 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,9 @@ See security audit reports [here](./audits). This repository is offered under the Apache 2.0 license. See LICENSE for details. +## Deployment +Check [this doc](./docs/deployment.md) for instructions on deployment and verification of token bridge. + ## Contact Discord - [Arbitrum](https://discord.com/invite/5KE54JwyTs) diff --git a/contracts/tokenbridge/arbitrum/L2AtomicTokenBridgeFactory.sol b/contracts/tokenbridge/arbitrum/L2AtomicTokenBridgeFactory.sol index 4859535812..2b09c40b44 100644 --- a/contracts/tokenbridge/arbitrum/L2AtomicTokenBridgeFactory.sol +++ b/contracts/tokenbridge/arbitrum/L2AtomicTokenBridgeFactory.sol @@ -7,6 +7,7 @@ import {L2CustomGateway} from "./gateway/L2CustomGateway.sol"; import {L2WethGateway} from "./gateway/L2WethGateway.sol"; import {StandardArbERC20} from "./StandardArbERC20.sol"; import {IUpgradeExecutor} from "@offchainlabs/upgrade-executor/src/IUpgradeExecutor.sol"; +import {CreationCodeHelper} from "../libraries/CreationCodeHelper.sol"; import {BeaconProxyFactory} from "../libraries/ClonableBeaconProxy.sol"; import {aeWETH} from "../libraries/aeWETH.sol"; import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; @@ -25,6 +26,12 @@ import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; contract L2AtomicTokenBridgeFactory { error L2AtomicTokenBridgeFactory_AlreadyExists(); + // In order to avoid having uninitialized logic contracts, `initialize` function will be called + // on all logic contracts which don't have initializers disabled. This dummy non-zero address + // will be provided to those initializers, as values written to the logic contract's storage + // are not used. + address private constant ADDRESS_DEAD = address(0x000000000000000000000000000000000000dEaD); + function deployL2Contracts( L2RuntimeCode calldata l2Code, address l1Router, @@ -47,8 +54,7 @@ contract L2AtomicTokenBridgeFactory { revert L2AtomicTokenBridgeFactory_AlreadyExists(); } } - address proxyAdmin = - address(new ProxyAdmin{ salt: _getL2Salt(OrbitSalts.L2_PROXY_ADMIN) }()); + address proxyAdmin = address(new ProxyAdmin{salt: _getL2Salt(OrbitSalts.L2_PROXY_ADMIN)}()); // deploy router/gateways/executor address upgradeExecutor = _deployUpgradeExecutor( @@ -69,7 +75,11 @@ contract L2AtomicTokenBridgeFactory { } // deploy multicall - Create2.deploy(0, _getL2Salt(OrbitSalts.L2_MULTICALL), _creationCodeFor(l2Code.multicall)); + Create2.deploy( + 0, + _getL2Salt(OrbitSalts.L2_MULTICALL), + CreationCodeHelper.getCreationCodeFor(l2Code.multicall) + ); // transfer ownership to L2 upgradeExecutor ProxyAdmin(proxyAdmin).transferOwnership(upgradeExecutor); @@ -82,18 +92,23 @@ contract L2AtomicTokenBridgeFactory { address aliasedL1UpgradeExecutor ) internal returns (address) { // canonical L2 upgrade executor with dummy logic - address canonicalUpgradeExecutor = _deploySeedProxy( - proxyAdmin, _getL2Salt(OrbitSalts.L2_EXECUTOR), _getL2Salt(OrbitSalts.L2_EXECUTOR_LOGIC) - ); + address canonicalUpgradeExecutor = _deploySeedProxy(proxyAdmin, OrbitSalts.L2_EXECUTOR); - // create UpgradeExecutor logic and upgrade to it + // Create UpgradeExecutor logic and upgrade to it. address upExecutorLogic = Create2.deploy( - 0, _getL2Salt(OrbitSalts.L2_EXECUTOR_LOGIC), _creationCodeFor(runtimeCode) + 0, + _getL2Salt(OrbitSalts.L2_EXECUTOR), + CreationCodeHelper.getCreationCodeFor(runtimeCode) ); + ProxyAdmin(proxyAdmin).upgrade( ITransparentUpgradeableProxy(canonicalUpgradeExecutor), upExecutorLogic ); + // init logic contract with dummy values + address[] memory empty = new address[](0); + IUpgradeExecutor(upExecutorLogic).initialize(ADDRESS_DEAD, empty); + // init upgrade executor address[] memory executors = new address[](2); executors[0] = rollupOwner; @@ -110,15 +125,17 @@ contract L2AtomicTokenBridgeFactory { address proxyAdmin ) internal returns (address) { // canonical L2 router with dummy logic - address canonicalRouter = _deploySeedProxy( - proxyAdmin, _getL2Salt(OrbitSalts.L2_ROUTER), _getL2Salt(OrbitSalts.L2_ROUTER_LOGIC) - ); + address canonicalRouter = _deploySeedProxy(proxyAdmin, OrbitSalts.L2_ROUTER); // create L2 router logic and upgrade - address routerLogic = - Create2.deploy(0, _getL2Salt(OrbitSalts.L2_ROUTER_LOGIC), _creationCodeFor(runtimeCode)); + address routerLogic = Create2.deploy( + 0, _getL2Salt(OrbitSalts.L2_ROUTER), CreationCodeHelper.getCreationCodeFor(runtimeCode) + ); ProxyAdmin(proxyAdmin).upgrade(ITransparentUpgradeableProxy(canonicalRouter), routerLogic); + // init logic contract with dummy values. + L2GatewayRouter(routerLogic).initialize(ADDRESS_DEAD, ADDRESS_DEAD); + // init L2GatewayRouter(canonicalRouter).initialize(l1Router, l2StandardGatewayCanonicalAddress); @@ -133,30 +150,29 @@ contract L2AtomicTokenBridgeFactory { address upgradeExecutor ) internal { // canonical L2 standard gateway with dummy logic - address canonicalStdGateway = _deploySeedProxy( - proxyAdmin, - _getL2Salt(OrbitSalts.L2_STANDARD_GATEWAY), - _getL2Salt(OrbitSalts.L2_STANDARD_GATEWAY_LOGIC) - ); + address canonicalStdGateway = _deploySeedProxy(proxyAdmin, OrbitSalts.L2_STANDARD_GATEWAY); // create L2 standard gateway logic and upgrade address stdGatewayLogic = Create2.deploy( - 0, _getL2Salt(OrbitSalts.L2_STANDARD_GATEWAY_LOGIC), _creationCodeFor(runtimeCode) + 0, + _getL2Salt(OrbitSalts.L2_STANDARD_GATEWAY), + CreationCodeHelper.getCreationCodeFor(runtimeCode) ); ProxyAdmin(proxyAdmin).upgrade( ITransparentUpgradeableProxy(canonicalStdGateway), stdGatewayLogic ); + // init logic contract with dummy values + L2ERC20Gateway(stdGatewayLogic).initialize(ADDRESS_DEAD, ADDRESS_DEAD, ADDRESS_DEAD); + // create beacon - StandardArbERC20 standardArbERC20 = new StandardArbERC20{ - salt: _getL2Salt(OrbitSalts.L2_STANDARD_ERC20) - }(); + StandardArbERC20 standardArbERC20 = + new StandardArbERC20{salt: _getL2Salt(OrbitSalts.BEACON_PROXY_FACTORY)}(); UpgradeableBeacon beacon = new UpgradeableBeacon{ - salt: _getL2Salt(OrbitSalts.UPGRADEABLE_BEACON) - }(address(standardArbERC20)); - BeaconProxyFactory beaconProxyFactory = new BeaconProxyFactory{ salt: _getL2Salt(OrbitSalts.BEACON_PROXY_FACTORY) - }(); + }(address(standardArbERC20)); + BeaconProxyFactory beaconProxyFactory = + new BeaconProxyFactory{salt: _getL2Salt(OrbitSalts.BEACON_PROXY_FACTORY)}(); // init contracts beaconProxyFactory.initialize(address(beacon)); @@ -175,22 +191,23 @@ contract L2AtomicTokenBridgeFactory { address proxyAdmin ) internal { // canonical L2 custom gateway with dummy logic - address canonicalCustomGateway = _deploySeedProxy( - proxyAdmin, - _getL2Salt(OrbitSalts.L2_CUSTOM_GATEWAY), - _getL2Salt(OrbitSalts.L2_CUSTOM_GATEWAY_LOGIC) - ); + address canonicalCustomGateway = _deploySeedProxy(proxyAdmin, OrbitSalts.L2_CUSTOM_GATEWAY); // create L2 custom gateway logic and upgrade address customGatewayLogicAddress = Create2.deploy( - 0, _getL2Salt(OrbitSalts.L2_CUSTOM_GATEWAY_LOGIC), _creationCodeFor(runtimeCode) + 0, + _getL2Salt(OrbitSalts.L2_CUSTOM_GATEWAY), + CreationCodeHelper.getCreationCodeFor(runtimeCode) ); ProxyAdmin(proxyAdmin).upgrade( ITransparentUpgradeableProxy(canonicalCustomGateway), customGatewayLogicAddress ); + // init logic contract with dummy values + L2CustomGateway(customGatewayLogicAddress).initialize(ADDRESS_DEAD, ADDRESS_DEAD); + // init - L2GatewayRouter(canonicalCustomGateway).initialize(l1CustomGateway, router); + L2CustomGateway(canonicalCustomGateway).initialize(l1CustomGateway, router); } function _deployWethGateway( @@ -202,38 +219,42 @@ contract L2AtomicTokenBridgeFactory { address proxyAdmin ) internal { // canonical L2 WETH with dummy logic - address canonicalL2Weth = _deploySeedProxy( - proxyAdmin, _getL2Salt(OrbitSalts.L2_WETH), _getL2Salt(OrbitSalts.L2_WETH_LOGIC) - ); + address canonicalL2Weth = _deploySeedProxy(proxyAdmin, OrbitSalts.L2_WETH); - // create L2WETH logic and upgrade + // Create L2WETH logic and upgrade address l2WethLogic = Create2.deploy( - 0, _getL2Salt(OrbitSalts.L2_WETH_LOGIC), _creationCodeFor(aeWethRuntimeCode) + 0, + _getL2Salt(OrbitSalts.L2_WETH), + CreationCodeHelper.getCreationCodeFor(aeWethRuntimeCode) ); ProxyAdmin(proxyAdmin).upgrade(ITransparentUpgradeableProxy(canonicalL2Weth), l2WethLogic); // canonical L2 WETH gateway with dummy logic - address canonicalL2WethGateway = _deploySeedProxy( - proxyAdmin, - _getL2Salt(OrbitSalts.L2_WETH_GATEWAY), - _getL2Salt(OrbitSalts.L2_WETH_GATEWAY_LOGIC) - ); + address canonicalL2WethGateway = _deploySeedProxy(proxyAdmin, OrbitSalts.L2_WETH_GATEWAY); // create L2WETH gateway logic and upgrade address l2WethGatewayLogic = Create2.deploy( 0, - _getL2Salt(OrbitSalts.L2_WETH_GATEWAY_LOGIC), - _creationCodeFor(wethGatewayRuntimeCode) + _getL2Salt(OrbitSalts.L2_WETH_GATEWAY), + CreationCodeHelper.getCreationCodeFor(wethGatewayRuntimeCode) ); ProxyAdmin(proxyAdmin).upgrade( ITransparentUpgradeableProxy(canonicalL2WethGateway), l2WethGatewayLogic ); + // init logic contract with dummy values + L2WethGateway(payable(l2WethGatewayLogic)).initialize( + ADDRESS_DEAD, ADDRESS_DEAD, ADDRESS_DEAD, ADDRESS_DEAD + ); + // init gateway L2WethGateway(payable(canonicalL2WethGateway)).initialize( l1WethGateway, router, l1Weth, address(canonicalL2Weth) ); + // init logic contract with dummy values + aeWETH(payable(l2WethLogic)).initialize("", "", 0, ADDRESS_DEAD, ADDRESS_DEAD); + // init L2Weth aeWETH(payable(canonicalL2Weth)).initialize( "WETH", "WETH", 18, canonicalL2WethGateway, l1Weth @@ -250,48 +271,18 @@ contract L2AtomicTokenBridgeFactory { } /** - * Deploys a proxy with empty logic contract in order to get deterministic address which does not depend on actual logic contract. + * Deploys a proxy with address(this) as logic in order to get deterministic address + * the proxy is salted using a salt derived from the prefix, the chainId and the sender */ - function _deploySeedProxy(address proxyAdmin, bytes32 proxySalt, bytes32 logicSalt) - internal - returns (address) - { + function _deploySeedProxy(address proxyAdmin, bytes memory prefix) internal returns (address) { return address( - new TransparentUpgradeableProxy{ salt: proxySalt }( - address(new CanonicalAddressSeed{ salt: logicSalt}()), - proxyAdmin, - bytes("") + new TransparentUpgradeableProxy{salt: _getL2Salt(prefix)}( + address(this), proxyAdmin, bytes("") ) ); } - - /** - * @notice Generate a creation code that results on a contract with `code` as bytecode. - * Source - https://github.com/0xsequence/sstore2/blob/master/contracts/utils/Bytecode.sol - * @param code The returning value of the resulting `creationCode` - * @return creationCode (constructor) for new contract - */ - function _creationCodeFor(bytes memory code) internal pure returns (bytes memory) { - /* - 0x00 0x63 0x63XXXXXX PUSH4 _code.length size - 0x01 0x80 0x80 DUP1 size size - 0x02 0x60 0x600e PUSH1 14 14 size size - 0x03 0x60 0x6000 PUSH1 00 0 14 size size - 0x04 0x39 0x39 CODECOPY size - 0x05 0x60 0x6000 PUSH1 00 0 size - 0x06 0xf3 0xf3 RETURN - - */ - - return abi.encodePacked(hex"63", uint32(code.length), hex"80600E6000396000F3", code); - } } -/** - * Dummy contract used as initial logic contract for proxies, in order to get canonical (CREATE2 based) address. Then we can upgrade to any logic without having canonical addresses impacted. - */ -contract CanonicalAddressSeed {} - /** * Placeholder for bytecode of token bridge contracts which is sent from L1 to L2 through retryable ticket. */ @@ -307,29 +298,21 @@ struct L2RuntimeCode { /** * Collection of salts used in CREATE2 deployment of L2 token bridge contracts. + * Logic contracts are deployed using the same salt as the proxy, it's fine as they have different code */ library OrbitSalts { - bytes public constant L1_PROXY_ADMIN = bytes("OrbitL1ProxyAdmin"); - bytes public constant L1_ROUTER = bytes("OrbitL1GatewayRouterProxy"); - bytes public constant L1_STANDARD_GATEWAY = bytes("OrbitL1StandardGatewayProxy"); - bytes public constant L1_CUSTOM_GATEWAY = bytes("OrbitL1CustomGatewayProxy"); - bytes public constant L1_WETH_GATEWAY = bytes("OrbitL1WethGatewayProxy"); - - bytes public constant L2_PROXY_ADMIN = bytes("OrbitL2ProxyAdmin"); - bytes public constant L2_ROUTER_LOGIC = bytes("OrbitL2GatewayRouterLogic"); - bytes public constant L2_ROUTER = bytes("OrbitL2GatewayRouterProxy"); - bytes public constant L2_STANDARD_GATEWAY_LOGIC = bytes("OrbitL2StandardGatewayLogic"); - bytes public constant L2_STANDARD_GATEWAY = bytes("OrbitL2StandardGatewayProxy"); - bytes public constant L2_CUSTOM_GATEWAY_LOGIC = bytes("OrbitL2CustomGatewayLogic"); - bytes public constant L2_CUSTOM_GATEWAY = bytes("OrbitL2CustomGatewayProxy"); - bytes public constant L2_WETH_GATEWAY_LOGIC = bytes("OrbitL2WethGatewayLogic"); - bytes public constant L2_WETH_GATEWAY = bytes("OrbitL2WethGatewayProxy"); - bytes public constant L2_WETH_LOGIC = bytes("OrbitL2WETH"); - bytes public constant L2_WETH = bytes("OrbitL2WETHProxy"); - bytes public constant L2_STANDARD_ERC20 = bytes("OrbitStandardArbERC20"); - bytes public constant UPGRADEABLE_BEACON = bytes("OrbitUpgradeableBeacon"); - bytes public constant BEACON_PROXY_FACTORY = bytes("OrbitBeaconProxyFactory"); - bytes public constant L2_EXECUTOR_LOGIC = bytes("OrbitL2UpgradeExecutorLogic"); - bytes public constant L2_EXECUTOR = bytes("OrbitL2UpgradeExecutorProxy"); - bytes public constant L2_MULTICALL = bytes("OrbitL2Multicall"); + bytes internal constant L1_ROUTER = bytes("L1R"); + bytes internal constant L1_STANDARD_GATEWAY = bytes("L1SGW"); + bytes internal constant L1_CUSTOM_GATEWAY = bytes("L1CGW"); + bytes internal constant L1_WETH_GATEWAY = bytes("L1WGW"); + + bytes internal constant L2_PROXY_ADMIN = bytes("L2PA"); + bytes internal constant L2_ROUTER = bytes("L2R"); + bytes internal constant L2_STANDARD_GATEWAY = bytes("L2SGW"); + bytes internal constant L2_CUSTOM_GATEWAY = bytes("L2CGW"); + bytes internal constant L2_WETH_GATEWAY = bytes("L2WGW"); + bytes internal constant L2_WETH = bytes("L2W"); + bytes internal constant BEACON_PROXY_FACTORY = bytes("L2BPF"); + bytes internal constant L2_EXECUTOR = bytes("L2E"); + bytes internal constant L2_MULTICALL = bytes("L2MC"); } diff --git a/contracts/tokenbridge/ethereum/L1AtomicTokenBridgeCreator.sol b/contracts/tokenbridge/ethereum/L1AtomicTokenBridgeCreator.sol index 9a2fa9ddec..bf70d9e55e 100644 --- a/contracts/tokenbridge/ethereum/L1AtomicTokenBridgeCreator.sol +++ b/contracts/tokenbridge/ethereum/L1AtomicTokenBridgeCreator.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.4; import { L1TokenBridgeRetryableSender, L1DeploymentAddresses, + L2DeploymentAddresses, RetryableParams, L2TemplateAddresses, IERC20Inbox, @@ -19,19 +20,17 @@ import {L1OrbitERC20Gateway} from "./gateway/L1OrbitERC20Gateway.sol"; import {L1OrbitCustomGateway} from "./gateway/L1OrbitCustomGateway.sol"; import { L2AtomicTokenBridgeFactory, - CanonicalAddressSeed, OrbitSalts, L2RuntimeCode, ProxyAdmin } from "../arbitrum/L2AtomicTokenBridgeFactory.sol"; -import {BytesLib} from "../libraries/BytesLib.sol"; +import {CreationCodeHelper} from "../libraries/CreationCodeHelper.sol"; import { IUpgradeExecutor, UpgradeExecutor } from "@offchainlabs/upgrade-executor/src/UpgradeExecutor.sol"; import {AddressAliasHelper} from "../libraries/AddressAliasHelper.sol"; import {IInbox, IBridge, IOwnable} from "@arbitrum/nitro-contracts/src/bridge/IInbox.sol"; -import {AddressAliasHelper} from "../libraries/AddressAliasHelper.sol"; import {ArbMulticall2} from "../../rpc-utils/MulticallV2.sol"; import {BeaconProxyFactory, ClonableBeaconProxy} from "../libraries/ClonableBeaconProxy.sol"; import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; @@ -46,30 +45,30 @@ import {IAccessControlUpgradeable} from /** * @title Layer1 token bridge creator - * @notice This contract is used to deploy token bridge on custom L2 chains. - * @dev Throughout the contract terms L1 and L2 are used, but those can be considered as base (N) chain and child (N+1) chain + * @notice This contract is used to deploy token bridge on custom Orbit chains. + * @dev Throughout the contract terms L1 and L2 are used, but those can be considered as parent (N) chain and child (N+1) chain. */ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { using SafeERC20 for IERC20; error L1AtomicTokenBridgeCreator_OnlyRollupOwner(); - error L1AtomicTokenBridgeCreator_InvalidRouterAddr(); error L1AtomicTokenBridgeCreator_TemplatesNotSet(); error L1AtomicTokenBridgeCreator_RollupOwnershipMisconfig(); error L1AtomicTokenBridgeCreator_ProxyAdminNotFound(); + error L1AtomicTokenBridgeCreator_L2FactoryCannotBeChanged(); event OrbitTokenBridgeCreated( address indexed inbox, address indexed owner, - address router, - address standardGateway, - address customGateway, - address wethGateway, + L1DeploymentAddresses l1Deployment, + L2DeploymentAddresses l2Deployment, address proxyAdmin, address upgradeExecutor ); event OrbitTokenBridgeTemplatesUpdated(); - event NonCanonicalRouterSet(address indexed inbox, address indexed router); + event OrbitTokenBridgeDeploymentSet( + address indexed inbox, L1DeploymentAddresses l1, L2DeploymentAddresses l2 + ); struct L1Templates { L1GatewayRouter routerTemplate; @@ -82,8 +81,10 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { IUpgradeExecutor upgradeExecutor; } - // non-canonical router registry - mapping(address => address) public inboxToNonCanonicalRouter; + // use separate mapping to allow appending to the struct in the future + // and workaround some stack too deep issues + mapping(address => L1DeploymentAddresses) public inboxToL1Deployment; + mapping(address => L2DeploymentAddresses) public inboxToL2Deployment; // Hard-code gas to make sure gas limit is big enough for L2 factory deployment to succeed. // If retryable would've reverted due to too low gas limit, nonce 0 would be burned and @@ -103,6 +104,7 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { address public l2CustomGatewayTemplate; address public l2WethGatewayTemplate; address public l2WethTemplate; + address public l2MulticallTemplate; // WETH address on L1 address public l1Weth; @@ -111,18 +113,10 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { address public l1Multicall; // immutable canonical address for L2 factory - // other canonical addresses (dependent on L2 template implementations) can be fetched through `getCanonicalL2***Address` functions + // other canonical addresses (dependent on L2 template implementations) can be fetched through `_predictL2***Address` functions address public canonicalL2FactoryAddress; - // immutable ArbMulticall2 template deployed on L1 - // Note - due to contract size limits, multicall template and its bytecode hash are set in constructor as immutables - address public immutable l2MulticallTemplate; - // code hash used for calculation of L2 multicall address - bytes32 public immutable ARB_MULTICALL_CODE_HASH; - - constructor(address _l2MulticallTemplate) { - l2MulticallTemplate = _l2MulticallTemplate; - ARB_MULTICALL_CODE_HASH = keccak256(_creationCodeFor(l2MulticallTemplate.code)); + constructor() { _disableInitializers(); } @@ -134,7 +128,7 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { retryableSender.initialize(); canonicalL2FactoryAddress = - _computeAddress(AddressAliasHelper.applyL1ToL2Alias(address(this)), 0); + _computeAddressAtNonce0(AddressAliasHelper.applyL1ToL2Alias(address(this))); } /** @@ -150,18 +144,26 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { address _l2CustomGatewayTemplate, address _l2WethGatewayTemplate, address _l2WethTemplate, + address _l2MulticallTemplate, address _l1Weth, address _l1Multicall, uint256 _gasLimitForL2FactoryDeployment ) external onlyOwner { l1Templates = _l1Templates; + if ( + l2TokenBridgeFactoryTemplate != address(0) + && l2TokenBridgeFactoryTemplate != _l2TokenBridgeFactoryTemplate + ) { + revert L1AtomicTokenBridgeCreator_L2FactoryCannotBeChanged(); + } l2TokenBridgeFactoryTemplate = _l2TokenBridgeFactoryTemplate; l2RouterTemplate = _l2RouterTemplate; l2StandardGatewayTemplate = _l2StandardGatewayTemplate; l2CustomGatewayTemplate = _l2CustomGatewayTemplate; l2WethGatewayTemplate = _l2WethGatewayTemplate; l2WethTemplate = _l2WethTemplate; + l2MulticallTemplate = _l2MulticallTemplate; l1Weth = _l1Weth; l1Multicall = _l1Multicall; @@ -178,9 +180,13 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { * is called to issue 2nd retryable which deploys and inits the rest of the contracts. L2 chain is determined * by `inbox` parameter. * - * Token bridge can be deployed only once for certain inbox. Any further calls to `createTokenBridge` will revert - * because L1 salts are already used at that point and L1 contracts are already deployed at canonical addresses - * for that inbox. + * In addition to deploying token bridge contracts, L2 factory will also deploy UpgradeExector on L2 side. + * L2 UpgradeExecutor will set 2 accounts to have EXECUTOR role - rollupOwner and alias of L1UpgradeExecutor. + * 'rollupOwner' can be either EOA or a contract. If it is a contract, address will be aliased before sending to L2 + * in order to be usable. + * + * Warning: Due to asynchronous communication between parent and child chain, always check child chain contracts are + * fully deployed and initialized before sending tokens to the bridge. Otherwise tokens might be permanently lost. */ function createTokenBridge( address inbox, @@ -204,216 +210,216 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { revert L1AtomicTokenBridgeCreator_RollupOwnershipMisconfig(); } - uint256 rollupChainId = IRollupCore(address(IInbox(inbox).bridge().rollup())).chainId(); + // we allow resending l2 deployment calls + // this is useful to recover from expired or out-of-order retryables + // in case of resend, we assume L1 contracts already exist and we just need to deploy L2 contracts + // deployment mappings should not be updated in case of resend + bool isResend = (inboxToL1Deployment[inbox].router != address(0)); - /// deploy L1 side of token bridge bool isUsingFeeToken = _getFeeToken(inbox) != address(0); - L1DeploymentAddresses memory l1DeploymentAddresses = - _deployL1Contracts(inbox, rollupOwner, upgradeExecutor, isUsingFeeToken, rollupChainId); - /// deploy factory and then L2 contracts through L2 factory, using 2 retryables calls - if (isUsingFeeToken) { - _deployL2Factory(inbox, gasPriceBid, isUsingFeeToken); - _deployL2ContractsUsingFeeToken( - l1DeploymentAddresses, - inbox, - maxGasForContracts, - gasPriceBid, - rollupOwner, - upgradeExecutor, - rollupChainId - ); - } else { - uint256 valueSpentForFactory = _deployL2Factory(inbox, gasPriceBid, isUsingFeeToken); - uint256 fundsRemaining = msg.value - valueSpentForFactory; - _deployL2ContractsUsingEth( - l1DeploymentAddresses, - inbox, - maxGasForContracts, - gasPriceBid, - fundsRemaining, - rollupOwner, - upgradeExecutor, - rollupChainId - ); - } - } + // store L2 addresses before deployments + L1DeploymentAddresses memory l1Deployment; + L2DeploymentAddresses memory l2Deployment; - /** - * @notice Rollup owner can override canonical router address by registering other non-canonical router. - * @dev Non-canonical router can be unregistered by re-setting it to address(0) - it makes canonical router the valid one. - */ - function setNonCanonicalRouter(address inbox, address nonCanonicalRouter) external { - if (msg.sender != IInbox(inbox).bridge().rollup().owner()) { - revert L1AtomicTokenBridgeCreator_OnlyRollupOwner(); + // if resend, we use the existing l1 deployment + if (isResend) { + l1Deployment = inboxToL1Deployment[inbox]; } - if (nonCanonicalRouter == getCanonicalL1RouterAddress(inbox)) { - revert L1AtomicTokenBridgeCreator_InvalidRouterAddr(); - } - - inboxToNonCanonicalRouter[inbox] = nonCanonicalRouter; - emit NonCanonicalRouterSet(inbox, nonCanonicalRouter); - } - function getRouter(address inbox) public view returns (address) { - address nonCanonicalRouter = inboxToNonCanonicalRouter[inbox]; - - if (nonCanonicalRouter != address(0)) { - return nonCanonicalRouter; + { + // store L2 addresses which are proxies + uint256 chainId = IRollupCore(address(IInbox(inbox).bridge().rollup())).chainId(); + l2Deployment.router = _getProxyAddress(OrbitSalts.L2_ROUTER, chainId); + l2Deployment.standardGateway = _getProxyAddress(OrbitSalts.L2_STANDARD_GATEWAY, chainId); + l2Deployment.customGateway = _getProxyAddress(OrbitSalts.L2_CUSTOM_GATEWAY, chainId); + if (!isUsingFeeToken) { + l2Deployment.wethGateway = _getProxyAddress(OrbitSalts.L2_WETH_GATEWAY, chainId); + l2Deployment.weth = _getProxyAddress(OrbitSalts.L2_WETH, chainId); + } + l2Deployment.upgradeExecutor = _getProxyAddress(OrbitSalts.L2_EXECUTOR, chainId); + + // store L2 addresses which are not proxies + l2Deployment.proxyAdmin = _predictL2ProxyAdminAddress(chainId); + l2Deployment.beaconProxyFactory = _predictL2BeaconProxyFactoryAddress(chainId); + l2Deployment.multicall = _predictL2Multicall(chainId); } - return getCanonicalL1RouterAddress(inbox); - } - - function _deployL1Contracts( - address inbox, - address rollupOwner, - address upgradeExecutor, - bool isUsingFeeToken, - uint256 chainId - ) internal returns (L1DeploymentAddresses memory l1Addresses) { + // deploy L1 side of token bridge // get existing proxy admin and upgrade executor - address proxyAdmin = IInbox_ProxyAdmin(inbox).getProxyAdmin(); + address proxyAdmin = IInboxProxyAdmin(inbox).getProxyAdmin(); if (proxyAdmin == address(0)) { revert L1AtomicTokenBridgeCreator_ProxyAdminNotFound(); } - // deploy router - address routerTemplate = isUsingFeeToken - ? address(l1Templates.feeTokenBasedRouterTemplate) - : address(l1Templates.routerTemplate); - l1Addresses.router = address( - new TransparentUpgradeableProxy{ salt: _getL1Salt(OrbitSalts.L1_ROUTER, inbox) }( - routerTemplate, - proxyAdmin, - bytes("") - ) - ); + // if resend, we assume L1 contracts already exist + if (!isResend) { + // l1 router deployment block + { + address routerTemplate = isUsingFeeToken + ? address(l1Templates.feeTokenBasedRouterTemplate) + : address(l1Templates.routerTemplate); + l1Deployment.router = _deployProxyWithSalt( + _getL1Salt(OrbitSalts.L1_ROUTER, inbox), routerTemplate, proxyAdmin + ); + } + + // l1 standard gateway deployment block + { + address template = isUsingFeeToken + ? address(l1Templates.feeTokenBasedStandardGatewayTemplate) + : address(l1Templates.standardGatewayTemplate); + + L1ERC20Gateway standardGateway = L1ERC20Gateway( + _deployProxyWithSalt( + _getL1Salt(OrbitSalts.L1_STANDARD_GATEWAY, inbox), template, proxyAdmin + ) + ); + + standardGateway.initialize( + l2Deployment.standardGateway, + l1Deployment.router, + inbox, + keccak256(type(ClonableBeaconProxy).creationCode), + l2Deployment.beaconProxyFactory + ); + + l1Deployment.standardGateway = address(standardGateway); + } + + // l1 custom gateway deployment block + { + address template = isUsingFeeToken + ? address(l1Templates.feeTokenBasedCustomGatewayTemplate) + : address(l1Templates.customGatewayTemplate); + + L1CustomGateway customGateway = L1CustomGateway( + _deployProxyWithSalt( + _getL1Salt(OrbitSalts.L1_CUSTOM_GATEWAY, inbox), template, proxyAdmin + ) + ); + + customGateway.initialize( + l2Deployment.customGateway, l1Deployment.router, inbox, upgradeExecutor + ); + + l1Deployment.customGateway = address(customGateway); + } + + // l1 weth gateway deployment block + if (!isUsingFeeToken) { + L1WethGateway wethGateway = L1WethGateway( + payable( + _deployProxyWithSalt( + _getL1Salt(OrbitSalts.L1_WETH_GATEWAY, inbox), + address(l1Templates.wethGatewayTemplate), + proxyAdmin + ) + ) + ); - // deploy and init gateways - l1Addresses.standardGateway = _deployL1StandardGateway( - proxyAdmin, l1Addresses.router, inbox, isUsingFeeToken, chainId - ); - l1Addresses.customGateway = _deployL1CustomGateway( - proxyAdmin, l1Addresses.router, inbox, upgradeExecutor, isUsingFeeToken, chainId - ); - l1Addresses.wethGateway = isUsingFeeToken - ? address(0) - : _deployL1WethGateway(proxyAdmin, l1Addresses.router, inbox, chainId); - l1Addresses.weth = isUsingFeeToken ? address(0) : l1Weth; + wethGateway.initialize( + l2Deployment.wethGateway, l1Deployment.router, inbox, l1Weth, l2Deployment.weth + ); - // init router - L1GatewayRouter(l1Addresses.router).initialize( - upgradeExecutor, - l1Addresses.standardGateway, - address(0), - getCanonicalL2RouterAddress(chainId), - inbox - ); + l1Deployment.wethGateway = address(wethGateway); + l1Deployment.weth = l1Weth; + } - // emit it - emit OrbitTokenBridgeCreated( - inbox, - rollupOwner, - l1Addresses.router, - l1Addresses.standardGateway, - l1Addresses.customGateway, - l1Addresses.wethGateway, - proxyAdmin, - upgradeExecutor - ); - } - - function _deployL1StandardGateway( - address proxyAdmin, - address router, - address inbox, - bool isUsingFeeToken, - uint256 chainId - ) internal returns (address) { - address template = isUsingFeeToken - ? address(l1Templates.feeTokenBasedStandardGatewayTemplate) - : address(l1Templates.standardGatewayTemplate); - - L1ERC20Gateway standardGateway = L1ERC20Gateway( - address( - new TransparentUpgradeableProxy{ - salt: _getL1Salt(OrbitSalts.L1_STANDARD_GATEWAY, inbox) - }(template, proxyAdmin, bytes("")) - ) - ); - - standardGateway.initialize( - getCanonicalL2StandardGatewayAddress(chainId), - router, - inbox, - keccak256(type(ClonableBeaconProxy).creationCode), - getCanonicalL2BeaconProxyFactoryAddress(chainId) - ); + // init router + L1GatewayRouter(l1Deployment.router).initialize( + upgradeExecutor, + l1Deployment.standardGateway, + address(0), + l2Deployment.router, + inbox + ); + } - return address(standardGateway); - } + // deploy factory and then L2 contracts through L2 factory, using 2 retryables calls + // we do not care if it is a resend or not, if the L2 deployment already exists it will simply fail on L2 + _deployL2Factory(inbox, gasPriceBid, isUsingFeeToken); + if (isUsingFeeToken) { + // transfer fee tokens to inbox to pay for 2nd retryable + address feeToken = _getFeeToken(inbox); + uint256 fee = maxGasForContracts * gasPriceBid; + IERC20(feeToken).safeTransferFrom(msg.sender, inbox, fee); + } - function _deployL1CustomGateway( - address proxyAdmin, - address router, - address inbox, - address upgradeExecutor, - bool isUsingFeeToken, - uint256 chainId - ) internal returns (address) { - address template = isUsingFeeToken - ? address(l1Templates.feeTokenBasedCustomGatewayTemplate) - : address(l1Templates.customGatewayTemplate); - - L1CustomGateway customGateway = L1CustomGateway( - address( - new TransparentUpgradeableProxy{ - salt: _getL1Salt(OrbitSalts.L1_CUSTOM_GATEWAY, inbox) - }(template, proxyAdmin, bytes("")) - ) - ); + // alias rollup owner if it is a contract + address l2RollupOwner = rollupOwner.code.length == 0 + ? rollupOwner + : AddressAliasHelper.applyL1ToL2Alias(rollupOwner); - customGateway.initialize( - getCanonicalL2CustomGatewayAddress(chainId), router, inbox, upgradeExecutor + // sweep the balance to send the retryable and refund the difference + // it is known that any eth previously in this contract can be extracted + // tho it is not expected that this contract will have any eth + retryableSender.sendRetryable{value: isUsingFeeToken ? 0 : address(this).balance}( + RetryableParams( + inbox, + canonicalL2FactoryAddress, + msg.sender, + msg.sender, + maxGasForContracts, + gasPriceBid + ), + L2TemplateAddresses( + l2RouterTemplate, + l2StandardGatewayTemplate, + l2CustomGatewayTemplate, + isUsingFeeToken ? address(0) : l2WethGatewayTemplate, + isUsingFeeToken ? address(0) : l2WethTemplate, + address(l1Templates.upgradeExecutor), + l2MulticallTemplate + ), + l1Deployment, + l2Deployment.standardGateway, + l2RollupOwner, + msg.sender, + upgradeExecutor, + isUsingFeeToken ); - return address(customGateway); + // deployment mappings should not be updated in case of resend + if (!isResend) { + emit OrbitTokenBridgeCreated( + inbox, rollupOwner, l1Deployment, l2Deployment, proxyAdmin, upgradeExecutor + ); + inboxToL1Deployment[inbox] = l1Deployment; + inboxToL2Deployment[inbox] = l2Deployment; + } } - function _deployL1WethGateway( - address proxyAdmin, - address router, + /** + * @notice Rollup owner can override deployment + */ + function setDeployment( address inbox, - uint256 chainId - ) internal returns (address) { - L1WethGateway wethGateway = L1WethGateway( - payable( - address( - new TransparentUpgradeableProxy{ - salt: _getL1Salt(OrbitSalts.L1_WETH_GATEWAY, inbox) - }(address(l1Templates.wethGatewayTemplate), proxyAdmin, bytes("")) - ) - ) - ); + L1DeploymentAddresses memory l1Deployment, + L2DeploymentAddresses memory l2Deployment + ) external { + if (msg.sender != IInbox(inbox).bridge().rollup().owner()) { + revert L1AtomicTokenBridgeCreator_OnlyRollupOwner(); + } - wethGateway.initialize( - getCanonicalL2WethGatewayAddress(chainId), - router, - inbox, - l1Weth, - getCanonicalL2WethAddress(chainId) - ); + inboxToL1Deployment[inbox] = l1Deployment; + inboxToL2Deployment[inbox] = l2Deployment; + emit OrbitTokenBridgeDeploymentSet(inbox, l1Deployment, l2Deployment); + } - return address(wethGateway); + /** + * @notice Get the L1 router address for a given inbox + * @dev This is kept since its cheaper than accessing the mapping getter + * and is useful enough for most onchain purposes + */ + function getRouter(address inbox) public view returns (address) { + return inboxToL1Deployment[inbox].router; } - function _deployL2Factory(address inbox, uint256 gasPriceBid, bool isUsingFeeToken) - internal - returns (uint256) - { + function _deployL2Factory(address inbox, uint256 gasPriceBid, bool isUsingFeeToken) internal { // encode L2 factory bytecode - bytes memory deploymentData = _creationCodeFor(l2TokenBridgeFactoryTemplate.code); + bytes memory deploymentData = + CreationCodeHelper.getCreationCodeFor(l2TokenBridgeFactoryTemplate.code); if (isUsingFeeToken) { // transfer fee tokens to inbox to pay for 1st retryable @@ -432,7 +438,6 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { retryableFee, deploymentData ); - return 0; } else { uint256 maxSubmissionCost = IInbox(inbox).calculateRetryableSubmissionFee(deploymentData.length, 0); @@ -448,140 +453,10 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { gasPriceBid, deploymentData ); - return retryableFee; } } - function _deployL2ContractsUsingEth( - L1DeploymentAddresses memory l1Addresses, - address inbox, - uint256 maxGas, - uint256 gasPriceBid, - uint256 availableFunds, - address rollupOwner, - address upgradeExecutor, - uint256 chainId - ) internal { - retryableSender.sendRetryableUsingEth{value: availableFunds}( - RetryableParams( - inbox, canonicalL2FactoryAddress, msg.sender, msg.sender, maxGas, gasPriceBid - ), - L2TemplateAddresses( - l2RouterTemplate, - l2StandardGatewayTemplate, - l2CustomGatewayTemplate, - l2WethGatewayTemplate, - l2WethTemplate, - address(l1Templates.upgradeExecutor), - l2MulticallTemplate - ), - l1Addresses, - getCanonicalL2StandardGatewayAddress(chainId), - rollupOwner, - msg.sender, - AddressAliasHelper.applyL1ToL2Alias(upgradeExecutor) - ); - } - - function _deployL2ContractsUsingFeeToken( - L1DeploymentAddresses memory l1Addresses, - address inbox, - uint256 maxGas, - uint256 gasPriceBid, - address rollupOwner, - address upgradeExecutor, - uint256 chainId - ) internal { - // transfer fee tokens to inbox to pay for 2nd retryable - address feeToken = _getFeeToken(inbox); - uint256 fee = maxGas * gasPriceBid; - IERC20(feeToken).safeTransferFrom(msg.sender, inbox, fee); - - retryableSender.sendRetryableUsingFeeToken( - RetryableParams( - inbox, canonicalL2FactoryAddress, msg.sender, msg.sender, maxGas, gasPriceBid - ), - L2TemplateAddresses( - l2RouterTemplate, - l2StandardGatewayTemplate, - l2CustomGatewayTemplate, - address(0), - address(0), - address(l1Templates.upgradeExecutor), - l2MulticallTemplate - ), - l1Addresses, - getCanonicalL2StandardGatewayAddress(chainId), - rollupOwner, - AddressAliasHelper.applyL1ToL2Alias(upgradeExecutor) - ); - } - - function getCanonicalL1RouterAddress(address inbox) public view returns (address) { - address expectedL1ProxyAdminAddress = Create2.computeAddress( - _getL1Salt(OrbitSalts.L1_PROXY_ADMIN, inbox), - keccak256(type(ProxyAdmin).creationCode), - address(this) - ); - - bool isUsingFeeToken = _getFeeToken(inbox) != address(0); - address template = isUsingFeeToken - ? address(l1Templates.feeTokenBasedRouterTemplate) - : address(l1Templates.routerTemplate); - - return Create2.computeAddress( - _getL1Salt(OrbitSalts.L1_ROUTER, inbox), - keccak256( - abi.encodePacked( - type(TransparentUpgradeableProxy).creationCode, - abi.encode(template, expectedL1ProxyAdminAddress, bytes("")) - ) - ), - address(this) - ); - } - - function getCanonicalL2RouterAddress(uint256 chainId) public view returns (address) { - return _getProxyAddress( - _getL2Salt(OrbitSalts.L2_ROUTER_LOGIC, chainId), - _getL2Salt(OrbitSalts.L2_ROUTER, chainId), - chainId - ); - } - - function getCanonicalL2StandardGatewayAddress(uint256 chainId) public view returns (address) { - return _getProxyAddress( - _getL2Salt(OrbitSalts.L2_STANDARD_GATEWAY_LOGIC, chainId), - _getL2Salt(OrbitSalts.L2_STANDARD_GATEWAY, chainId), - chainId - ); - } - - function getCanonicalL2CustomGatewayAddress(uint256 chainId) public view returns (address) { - return _getProxyAddress( - _getL2Salt(OrbitSalts.L2_CUSTOM_GATEWAY_LOGIC, chainId), - _getL2Salt(OrbitSalts.L2_CUSTOM_GATEWAY, chainId), - chainId - ); - } - - function getCanonicalL2WethGatewayAddress(uint256 chainId) public view returns (address) { - return _getProxyAddress( - _getL2Salt(OrbitSalts.L2_WETH_GATEWAY_LOGIC, chainId), - _getL2Salt(OrbitSalts.L2_WETH_GATEWAY, chainId), - chainId - ); - } - - function getCanonicalL2WethAddress(uint256 chainId) public view returns (address) { - return _getProxyAddress( - _getL2Salt(OrbitSalts.L2_WETH_LOGIC, chainId), - _getL2Salt(OrbitSalts.L2_WETH, chainId), - chainId - ); - } - - function getCanonicalL2ProxyAdminAddress(uint256 chainId) public view returns (address) { + function _predictL2ProxyAdminAddress(uint256 chainId) internal view returns (address) { return Create2.computeAddress( _getL2Salt(OrbitSalts.L2_PROXY_ADMIN, chainId), keccak256(type(ProxyAdmin).creationCode), @@ -589,11 +464,7 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { ); } - function getCanonicalL2BeaconProxyFactoryAddress(uint256 chainId) - public - view - returns (address) - { + function _predictL2BeaconProxyFactoryAddress(uint256 chainId) internal view returns (address) { return Create2.computeAddress( _getL2Salt(OrbitSalts.BEACON_PROXY_FACTORY, chainId), keccak256(type(BeaconProxyFactory).creationCode), @@ -601,37 +472,25 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { ); } - function getCanonicalL2UpgradeExecutorAddress(uint256 chainId) public view returns (address) { - return _getProxyAddress( - _getL2Salt(OrbitSalts.L2_EXECUTOR_LOGIC, chainId), - _getL2Salt(OrbitSalts.L2_EXECUTOR, chainId), - chainId - ); - } - - function getCanonicalL2Multicall(uint256 chainId) public view returns (address) { + function _predictL2Multicall(uint256 chainId) internal view returns (address) { return Create2.computeAddress( _getL2Salt(OrbitSalts.L2_MULTICALL, chainId), - ARB_MULTICALL_CODE_HASH, + keccak256(CreationCodeHelper.getCreationCodeFor(l2MulticallTemplate.code)), canonicalL2FactoryAddress ); } function _getFeeToken(address inbox) internal view returns (address) { address bridge = address(IInbox(inbox).bridge()); - - (bool success, bytes memory feeTokenAddressData) = - bridge.staticcall(abi.encodeWithSelector(IERC20Bridge.nativeToken.selector)); - - if (!success || feeTokenAddressData.length < 32) { + try IERC20Bridge(bridge).nativeToken() returns (address feeToken) { + return feeToken; + } catch { return address(0); } - - return BytesLib.toAddress(feeTokenAddressData, 12); } /** - * @notice Compute address of contract deployed using CREATE opcode + * @notice Compute address of contract deployed using CREATE opcode at nonce 0 * @dev The contract address is derived by RLP encoding the deployer's address and the nonce using the Keccak-256 hashing algorithm. * More formally: keccak256(rlp.encode([origin, nonce])[12:] * @@ -641,67 +500,36 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { * - prefix of the whole list is 0xc0 + lenInBytes(RLP(list)) * After we have RLP encoding in place last step is to hash it, take last 20 bytes and cast is to an address. * + * This function is an codesize optimized version to only calculate the address for nonce 0. * @return computed address */ - function _computeAddress(address origin, uint256 nonce) internal pure returns (address) { - bytes memory data; - if (nonce == 0x00) { - data = abi.encodePacked(bytes1(0xd6), bytes1(0x94), origin, bytes1(0x80)); - } else if (nonce <= 0x7f) { - data = abi.encodePacked(bytes1(0xd6), bytes1(0x94), origin, uint8(nonce)); - } else if (nonce <= 0xff) { - data = abi.encodePacked(bytes1(0xd7), bytes1(0x94), origin, bytes1(0x81), uint8(nonce)); - } else if (nonce <= 0xffff) { - data = abi.encodePacked(bytes1(0xd8), bytes1(0x94), origin, bytes1(0x82), uint16(nonce)); - } else if (nonce <= 0xffffff) { - data = abi.encodePacked(bytes1(0xd9), bytes1(0x94), origin, bytes1(0x83), uint24(nonce)); - } else { - data = abi.encodePacked(bytes1(0xda), bytes1(0x94), origin, bytes1(0x84), uint32(nonce)); - } - return address(uint160(uint256(keccak256(data)))); - } - - /** - * @notice Generate a creation code that results on a contract with `code` as bytecode. - * Source - https://github.com/0xsequence/sstore2/blob/master/contracts/utils/Bytecode.sol - * @param code The returning value of the resulting `creationCode` - * @return creationCode (constructor) for new contract - */ - function _creationCodeFor(bytes memory code) internal pure returns (bytes memory) { - /* - 0x00 0x63 0x63XXXXXX PUSH4 _code.length size - 0x01 0x80 0x80 DUP1 size size - 0x02 0x60 0x600e PUSH1 14 14 size size - 0x03 0x60 0x6000 PUSH1 00 0 14 size size - 0x04 0x39 0x39 CODECOPY size - 0x05 0x60 0x6000 PUSH1 00 0 size - 0x06 0xf3 0xf3 RETURN - - */ - - return abi.encodePacked(hex"63", uint32(code.length), hex"80600E6000396000F3", code); + function _computeAddressAtNonce0(address origin) internal pure returns (address) { + return address( + uint160( + uint256( + keccak256(abi.encodePacked(bytes1(0xd6), bytes1(0x94), origin, bytes1(0x80))) + ) + ) + ); } /** * @notice L2 contracts are deployed as proxy with dummy seed logic contracts using CREATE2. That enables - * us to upfront calculate the expected canonical addresses. + * us to upfront calculate the expected canonical addresses. This proxy should be upgraded to the + * intended logic implementation immediately. */ - function _getProxyAddress(bytes32 logicSalt, bytes32 proxySalt, uint256 chainId) + function _getProxyAddress(bytes memory prefix, uint256 chainId) internal view returns (address) { - address logicSeedAddress = Create2.computeAddress( - logicSalt, keccak256(type(CanonicalAddressSeed).creationCode), canonicalL2FactoryAddress - ); - return Create2.computeAddress( - proxySalt, + _getL2Salt(prefix, chainId), keccak256( abi.encodePacked( type(TransparentUpgradeableProxy).creationCode, abi.encode( - logicSeedAddress, getCanonicalL2ProxyAdminAddress(chainId), bytes("") + canonicalL2FactoryAddress, _predictL2ProxyAdminAddress(chainId), bytes("") ) ) ), @@ -731,13 +559,23 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { ) ); } + + /** + * @notice Internal method to deploy TransparentUpgradeableProxy with CREATE2 opcode. + */ + function _deployProxyWithSalt(bytes32 salt, address logic, address admin) + internal + returns (address) + { + return address(new TransparentUpgradeableProxy{salt: salt}(logic, admin, bytes(""))); + } } interface IERC20Bridge { function nativeToken() external view returns (address); } -interface IInbox_ProxyAdmin { +interface IInboxProxyAdmin { function getProxyAdmin() external view returns (address); } diff --git a/contracts/tokenbridge/ethereum/L1TokenBridgeRetryableSender.sol b/contracts/tokenbridge/ethereum/L1TokenBridgeRetryableSender.sol index bde3de250b..06411905e7 100644 --- a/contracts/tokenbridge/ethereum/L1TokenBridgeRetryableSender.sol +++ b/contracts/tokenbridge/ethereum/L1TokenBridgeRetryableSender.sol @@ -7,6 +7,7 @@ import { L2RuntimeCode, ProxyAdmin } from "../arbitrum/L2AtomicTokenBridgeFactory.sol"; +import {AddressAliasHelper} from "../libraries/AddressAliasHelper.sol"; import { Initializable, OwnableUpgradeable @@ -28,6 +29,7 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol */ contract L1TokenBridgeRetryableSender is Initializable, OwnableUpgradeable { error L1TokenBridgeRetryableSender_RefundFailed(); + error L1TokenBridgeRetryableSender_EthReceivedForFeeToken(); function initialize() public initializer { __Ownable_init(); @@ -36,17 +38,51 @@ contract L1TokenBridgeRetryableSender is Initializable, OwnableUpgradeable { /** * @notice Creates retryable which deploys L2 side of the token bridge. * @dev Function will build retryable data, calculate submission cost and retryable value, create retryable - * and then refund the remaining funds to original delpoyer. + * and then refund the remaining funds to original delpoyer if excess eth was sent. */ - function sendRetryableUsingEth( + function sendRetryable( RetryableParams calldata retryableParams, L2TemplateAddresses calldata l2, L1DeploymentAddresses calldata l1, address l2StandardGatewayAddress, address rollupOwner, address deployer, - address aliasedL1UpgradeExecutor + address l1UpgradeExecutor, + bool isUsingFeeToken ) external payable onlyOwner { + address aliasedL1UpgradeExecutor = AddressAliasHelper.applyL1ToL2Alias(l1UpgradeExecutor); + if (!isUsingFeeToken) { + _sendRetryableUsingEth( + retryableParams, + l2, + l1, + l2StandardGatewayAddress, + rollupOwner, + deployer, + aliasedL1UpgradeExecutor + ); + } else { + if (msg.value > 0) revert L1TokenBridgeRetryableSender_EthReceivedForFeeToken(); + _sendRetryableUsingFeeToken( + retryableParams, + l2, + l1, + l2StandardGatewayAddress, + rollupOwner, + aliasedL1UpgradeExecutor + ); + } + } + + function _sendRetryableUsingEth( + RetryableParams calldata retryableParams, + L2TemplateAddresses calldata l2, + L1DeploymentAddresses calldata l1, + address l2StandardGatewayAddress, + address rollupOwner, + address deployer, + address aliasedL1UpgradeExecutor + ) internal { bytes memory data = abi.encodeCall( L2AtomicTokenBridgeFactory.deployL2Contracts, ( @@ -77,24 +113,20 @@ contract L1TokenBridgeRetryableSender is Initializable, OwnableUpgradeable { _createRetryableUsingEth(retryableParams, maxSubmissionCost, retryableValue, data); // refund excess value to the deployer - uint256 refund = msg.value - retryableValue; - (bool success,) = deployer.call{value: refund}(""); + // it is known that any eth previously in this contract can be extracted + // tho it is not expected that this contract will have any eth + (bool success,) = deployer.call{value: address(this).balance}(""); if (!success) revert L1TokenBridgeRetryableSender_RefundFailed(); } - /** - * @notice Creates retryable which deploys L2 side of the token bridge. - * @dev Function will build retryable data, calculate submission cost and retryable value, create retryable - * and then refund the remaining funds to original delpoyer. - */ - function sendRetryableUsingFeeToken( + function _sendRetryableUsingFeeToken( RetryableParams calldata retryableParams, L2TemplateAddresses calldata l2, L1DeploymentAddresses calldata l1, address l2StandardGatewayAddress, address rollupOwner, address aliasedL1UpgradeExecutor - ) external payable onlyOwner { + ) internal { bytes memory data = abi.encodeCall( L2AtomicTokenBridgeFactory.deployL2Contracts, ( @@ -196,6 +228,18 @@ struct L1DeploymentAddresses { address weth; } +struct L2DeploymentAddresses { + address router; + address standardGateway; + address customGateway; + address wethGateway; + address weth; + address proxyAdmin; + address beaconProxyFactory; + address upgradeExecutor; + address multicall; +} + interface IERC20Inbox { function createRetryableTicket( address to, diff --git a/contracts/tokenbridge/libraries/CreationCodeHelper.sol b/contracts/tokenbridge/libraries/CreationCodeHelper.sol new file mode 100644 index 0000000000..1e3596ba2c --- /dev/null +++ b/contracts/tokenbridge/libraries/CreationCodeHelper.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +library CreationCodeHelper { + /** + * @notice Generate a creation code that results with a contract with `code` as deployed code. + * Generated creation code shall match the one generated by Solidity compiler with an empty constructor. + * @dev Prepended constructor bytecode consists of: + * - 608060405234801561001057600080fd5b50 - store free memory pointer, then check no callvalue is provided + * - 61xxxx - push 2 bytes of `code` length + * - 806100206000396000f3fe - copy deployed code to memory and return the location of it + * @param runtimeCode Deployed bytecode to which constructor bytecode will be prepended + * @return Creation code of a new contract + */ + function getCreationCodeFor(bytes memory runtimeCode) internal pure returns (bytes memory) { + return abi.encodePacked( + hex"608060405234801561001057600080fd5b50", + hex"61", + uint16(runtimeCode.length), + hex"806100206000396000f3fe", + runtimeCode + ); + } +} diff --git a/contracts/tokenbridge/test/CreationCodeTest.sol b/contracts/tokenbridge/test/CreationCodeTest.sol new file mode 100644 index 0000000000..ac83ebc3e8 --- /dev/null +++ b/contracts/tokenbridge/test/CreationCodeTest.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import {CreationCodeHelper} from "../libraries/CreationCodeHelper.sol"; + +contract CreationCodeTest { + /** + * @dev Wrapper function around CreationCodeHelper.getCreationCodeFor used for testing convenience. + */ + function creationCodeFor(bytes memory code) external pure returns (bytes memory) { + return CreationCodeHelper.getCreationCodeFor(code); + } +} diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 0000000000..b58bc42dd9 --- /dev/null +++ b/docs/deployment.md @@ -0,0 +1,102 @@ +# How to deploy RollupCreator and TokenBridgeCreator? + +## Deploy RollupCreator +RollupCreator is in nitro-contracts repo +``` +cd nitro-contracts +``` + +Checkout target code, ie. +``` +git checkout v1.1.0 +``` + +Install dependencies and build +``` +yarn install +yarn build +``` + +Populate .env +``` +DEVNET_PRIVKEY or MAINNET_PRIVKEY +ARBISCAN_API_KEY +``` + +Finally deploy it, using `--network` flag to specify network. + +Ie. to deploy to Arbitrum Sepolia +``` +yarn run deploy-factory --network arbSepolia +``` + +To deploy to Arbitrum One +``` +yarn run deploy-factory --network arb1 +``` + +Script output will contain all deployed addresses. + + +## Deploy TokenBridgeCreator +Checkout target code, install dependencies and build +``` +cd token-bridge-contracts +yarn install +yarn build +``` + + +Populate .env +``` +BASECHAIN_RPC +BASECHAIN_DEPLOYER_KEY +BASECHAIN_WETH +GAS_LIMIT_FOR_L2_FACTORY_DEPLOYMENT +ARBISCAN_API_KEY +``` + +Note: Gas limit for deploying child chain factory via retryable needs to be provided to the TokenBridgeCreator when templates are set. This value can be obtained in 2 ways - 1st is to provide `ORBIT_RPC` and `ROLLUP_ADDRESS` env vars, and script will then use Arbitrum SDK to estimate gas needed for deploying L2 factory. Other way to do it is much simpler - provide hardcoded value by setting the `GAS_LIMIT_FOR_L2_FACTORY_DEPLOYMENT`. Previous deployments showed that gas needed is ~5140000. Adding a bit of buffer on top, we can set this value to `GAS_LIMIT_FOR_L2_FACTORY_DEPLOYMENT=6000000`. + + +Finally, deploy token bridge creator. Target chain is defined by `BASECHAIN_RPC` env var (no need to provide `--network` flag). +``` +yarn run deploy:token-bridge-creator +``` + +Script outputs `L1TokenBridgeCreator` and `L1TokenBridgeRetryableSender` addresses. All deployed addresses can be obtained through `L1TokenBridgeCreator` contract. + + +## Ownership +These contracts will be owned by deployer: +- RollupCreator (owner can set templates) +- L1AtomicTokenBridgeCreator (owner can set templates) +- ProxyAdmin of L1AtomicTokenBridgeCreator and L1TokenBridgeRetryableSender (owner can do upgrades) + + +## Verify token bridge deployment +There is a verification script which checks that token bridge contracts have been properly deployed and initialized. Here are steps for running it. + +Checkout target code, install dependencies and build +``` +cd token-bridge-contracts +yarn install +yarn build +``` + +Populate .env +``` +ROLLUP_ADDRESS +L1_TOKEN_BRIDGE_CREATOR +L1_RETRYABLE_SENDER +BASECHAIN_DEPLOYER_KEY +BASECHAIN_RPC +ORBIT_RPC +``` +(`L1_RETRYABLE_SENDER` address can be obtained by calling `retryableSender()` on the L1 token bridge creator) + + +Run the script +``` +yarn run test:tokenbridge:deployment +``` diff --git a/lib/nitro-contracts b/lib/nitro-contracts index d5d33c2b8d..1a94dabd80 160000 --- a/lib/nitro-contracts +++ b/lib/nitro-contracts @@ -1 +1 @@ -Subproject commit d5d33c2b8d5615563b8c553ca2a1bb936c039924 +Subproject commit 1a94dabd805673e4c85e4071662814a142b20893 diff --git a/package.json b/package.json index df01055d5f..f0a78cef5c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@arbitrum/token-bridge-contracts", - "version": "1.1.0", + "version": "1.2.0", "license": "Apache-2.0", "scripts": { "prepublishOnly": "hardhat clean && hardhat compile", @@ -21,9 +21,10 @@ "test:e2e:local-env": "yarn hardhat test test-e2e/*", "test:storage": "./scripts/storage_layout_test.bash", "deploy:local:token-bridge": "ts-node ./scripts/local-deployment/deployCreatorAndCreateTokenBridge.ts", - "deploy:goerli:token-bridge-creator": "ts-node ./scripts/goerli-deployment/deployTokenBridgeCreator.ts", - "create:goerli:token-bridge": "ts-node ./scripts/goerli-deployment/createTokenBridge.ts", + "deploy:token-bridge-creator": "ts-node ./scripts/deployment/deployTokenBridgeCreator.ts", + "create:token-bridge": "ts-node ./scripts/deployment/createTokenBridge.ts", "test:tokenbridge:deployment": "hardhat test test-e2e/tokenBridgeDeploymentTest.ts", + "test:creation-code": "hardhat test test-e2e/creationCodeTest.ts", "typechain": "hardhat typechain", "deploy:tokenbridge": "hardhat run scripts/deploy_token_bridge_l1.ts --network mainnet", "gen:uml": "sol2uml ./contracts/tokenbridge/arbitrum,./contracts/tokenbridge/ethereum,./contracts/tokenbridge/libraries -o ./gatewayUML.svg", diff --git a/scripts/atomicTokenBridgeDeployer.ts b/scripts/atomicTokenBridgeDeployer.ts index db70ea55b7..cadbee9492 100644 --- a/scripts/atomicTokenBridgeDeployer.ts +++ b/scripts/atomicTokenBridgeDeployer.ts @@ -25,6 +25,7 @@ import { IRollupCore__factory, IBridge__factory, Multicall2__factory, + IInboxProxyAdmin__factory, } from '../build/types' import { abi as UpgradeExecutorABI, @@ -41,6 +42,11 @@ import { getBaseFee } from '@arbitrum/sdk/dist/lib/utils/lib' import { RollupAdminLogic__factory } from '@arbitrum/sdk/dist/lib/abi/factories/RollupAdminLogic__factory' import { ContractVerifier } from './contractVerifier' +/** + * Dummy non-zero address which is provided to logic contracts initializers + */ +const ADDRESS_DEAD = '0x000000000000000000000000000000000000dEaD' + /** * Use already deployed L1TokenBridgeCreator to create and init token bridge contracts. * Function first gets estimates for 2 retryable tickets - one for deploying L2 factory and @@ -172,73 +178,18 @@ export const createTokenBridge = async ( ) console.log('L2AtomicTokenBridgeFactory', l2AtomicTokenBridgeFactory.address) - /// pick up L1 contracts from events - const { - router: l1Router, - standardGateway: l1StandardGateway, - customGateway: l1CustomGateway, - wethGateway: l1WethGateway, - proxyAdmin: l1ProxyAdmin, - } = getParsedLogs( - receipt.logs, - l1TokenBridgeCreator.interface, - 'OrbitTokenBridgeCreated' - )[0].args - - const rollup = await IBridge__factory.connect( - await IInbox__factory.connect(inbox, l1Signer).bridge(), - l1Signer - ).rollup() - const chainId = await IRollupCore__factory.connect(rollup, l1Signer).chainId() + /// fetch deployment addresses from registry + const l1Deployment = await l1TokenBridgeCreator.inboxToL1Deployment(inbox) + const l2Deployment = await l1TokenBridgeCreator.inboxToL2Deployment(inbox) - /// pick up L2 contracts - const l2Router = await l1TokenBridgeCreator.getCanonicalL2RouterAddress( - chainId - ) - const l2StandardGateway = L2ERC20Gateway__factory.connect( - await l1TokenBridgeCreator.getCanonicalL2StandardGatewayAddress(chainId), - l2Provider - ) - const beaconProxyFactory = await l2StandardGateway.beaconProxyFactory() - const l2CustomGateway = - await l1TokenBridgeCreator.getCanonicalL2CustomGatewayAddress(chainId) - - const isUsingFeeToken = feeToken != ethers.constants.AddressZero - const l2WethGateway = isUsingFeeToken - ? ethers.constants.AddressZero - : L2WethGateway__factory.connect( - await l1TokenBridgeCreator.getCanonicalL2WethGatewayAddress(chainId), - l2Provider - ).address - const l1Weth = await l1TokenBridgeCreator.l1Weth() - const l2Weth = isUsingFeeToken - ? ethers.constants.AddressZero - : await l1TokenBridgeCreator.getCanonicalL2WethAddress(chainId) - const l2ProxyAdmin = - await l1TokenBridgeCreator.getCanonicalL2ProxyAdminAddress(chainId) - - const l1Multicall = await l1TokenBridgeCreator.l1Multicall() - const l2Multicall = await l1TokenBridgeCreator.getCanonicalL2Multicall( - chainId - ) + /// fetch l1 multicall and l1 proxy admin from creator + const l1MultiCall = await l1TokenBridgeCreator.l1Multicall() + const l1ProxyAdmin = await IInboxProxyAdmin__factory.connect( + inbox, + l1Signer.provider! + ).getProxyAdmin() - return { - l1Router, - l1StandardGateway, - l1CustomGateway, - l1WethGateway, - l1ProxyAdmin, - l1Multicall, - l2Router, - l2StandardGateway: l2StandardGateway.address, - l2CustomGateway, - l2WethGateway, - l1Weth, - l2Weth, - beaconProxyFactory, - l2ProxyAdmin, - l2Multicall, - } + return { l1Deployment, l2Deployment, l1MultiCall, l1ProxyAdmin } } /** @@ -250,8 +201,8 @@ export const createTokenBridge = async ( */ export const deployL1TokenBridgeCreator = async ( l1Deployer: Signer, - l2Provider: ethers.providers.Provider, l1WethAddress: string, + gasLimitForL2FactoryDeployment: BigNumber, verifyContracts: boolean = false ) => { /// deploy creator behind proxy @@ -266,9 +217,7 @@ export const deployL1TokenBridgeCreator = async ( await l1TokenBridgeCreatorProxyAdmin.deployed() const l1TokenBridgeCreatorLogic = - await new L1AtomicTokenBridgeCreator__factory(l1Deployer).deploy( - l2MulticallAddressOnL1.address - ) + await new L1AtomicTokenBridgeCreator__factory(l1Deployer).deploy() await l1TokenBridgeCreatorLogic.deployed() const l1TokenBridgeCreatorProxy = @@ -304,40 +253,104 @@ export const deployL1TokenBridgeCreator = async ( l1Deployer ) + // initialize retryable sender logic contract + await (await retryableSenderLogic.initialize()).wait() + /// init creator await (await l1TokenBridgeCreator.initialize(retryableSender.address)).wait() - /// deploy L1 logic contracts + /// deploy L1 logic contracts. Initialize them with dummy data const routerTemplate = await new L1GatewayRouter__factory(l1Deployer).deploy() await routerTemplate.deployed() + await ( + await routerTemplate.initialize( + ADDRESS_DEAD, + ADDRESS_DEAD, + ADDRESS_DEAD, + ADDRESS_DEAD, + ADDRESS_DEAD + ) + ).wait() const standardGatewayTemplate = await new L1ERC20Gateway__factory( l1Deployer ).deploy() await standardGatewayTemplate.deployed() + await ( + await standardGatewayTemplate.initialize( + ADDRESS_DEAD, + ADDRESS_DEAD, + ADDRESS_DEAD, + ethers.utils.hexZeroPad('0x01', 32), + ADDRESS_DEAD + ) + ).wait() const customGatewayTemplate = await new L1CustomGateway__factory( l1Deployer ).deploy() await customGatewayTemplate.deployed() + await ( + await customGatewayTemplate.initialize( + ADDRESS_DEAD, + ADDRESS_DEAD, + ADDRESS_DEAD, + ADDRESS_DEAD + ) + ).wait() const wethGatewayTemplate = await new L1WethGateway__factory( l1Deployer ).deploy() await wethGatewayTemplate.deployed() + await ( + await wethGatewayTemplate.initialize( + ADDRESS_DEAD, + ADDRESS_DEAD, + ADDRESS_DEAD, + ADDRESS_DEAD, + ADDRESS_DEAD + ) + ).wait() const feeTokenBasedRouterTemplate = await new L1OrbitGatewayRouter__factory( l1Deployer ).deploy() await feeTokenBasedRouterTemplate.deployed() + await ( + await feeTokenBasedRouterTemplate.initialize( + ADDRESS_DEAD, + ADDRESS_DEAD, + ADDRESS_DEAD, + ADDRESS_DEAD, + ADDRESS_DEAD + ) + ).wait() const feeTokenBasedStandardGatewayTemplate = await new L1OrbitERC20Gateway__factory(l1Deployer).deploy() await feeTokenBasedStandardGatewayTemplate.deployed() + await ( + await feeTokenBasedStandardGatewayTemplate.initialize( + ADDRESS_DEAD, + ADDRESS_DEAD, + ADDRESS_DEAD, + ethers.utils.hexZeroPad('0x01', 32), + ADDRESS_DEAD + ) + ).wait() const feeTokenBasedCustomGatewayTemplate = await new L1OrbitCustomGateway__factory(l1Deployer).deploy() await feeTokenBasedCustomGatewayTemplate.deployed() + await ( + await feeTokenBasedCustomGatewayTemplate.initialize( + ADDRESS_DEAD, + ADDRESS_DEAD, + ADDRESS_DEAD, + ADDRESS_DEAD + ) + ).wait() const upgradeExecutorFactory = new ethers.ContractFactory( UpgradeExecutorABI, @@ -359,7 +372,7 @@ export const deployL1TokenBridgeCreator = async ( upgradeExecutor: upgradeExecutor.address, } - /// deploy L2 contracts as placeholders on L1 + /// deploy L2 contracts as placeholders on L1. Initialize them with dummy data const l2TokenBridgeFactoryOnL1 = await new L2AtomicTokenBridgeFactory__factory(l1Deployer).deploy() await l2TokenBridgeFactoryOnL1.deployed() @@ -368,21 +381,42 @@ export const deployL1TokenBridgeCreator = async ( l1Deployer ).deploy() await l2GatewayRouterOnL1.deployed() + await ( + await l2GatewayRouterOnL1.initialize(ADDRESS_DEAD, ADDRESS_DEAD) + ).wait() const l2StandardGatewayAddressOnL1 = await new L2ERC20Gateway__factory( l1Deployer ).deploy() await l2StandardGatewayAddressOnL1.deployed() + await ( + await l2StandardGatewayAddressOnL1.initialize( + ADDRESS_DEAD, + ADDRESS_DEAD, + ADDRESS_DEAD + ) + ).wait() const l2CustomGatewayAddressOnL1 = await new L2CustomGateway__factory( l1Deployer ).deploy() await l2CustomGatewayAddressOnL1.deployed() + await ( + await l2CustomGatewayAddressOnL1.initialize(ADDRESS_DEAD, ADDRESS_DEAD) + ).wait() const l2WethGatewayAddressOnL1 = await new L2WethGateway__factory( l1Deployer ).deploy() await l2WethGatewayAddressOnL1.deployed() + await ( + await l2WethGatewayAddressOnL1.initialize( + ADDRESS_DEAD, + ADDRESS_DEAD, + ADDRESS_DEAD, + ADDRESS_DEAD + ) + ).wait() const l2WethAddressOnL1 = await new AeWETH__factory(l1Deployer).deploy() await l2WethAddressOnL1.deployed() @@ -390,12 +424,6 @@ export const deployL1TokenBridgeCreator = async ( const l1Multicall = await new Multicall2__factory(l1Deployer).deploy() await l1Multicall.deployed() - //// run retryable estimate for deploying L2 factory - const deployFactoryGasParams = await getEstimateForDeployingFactory( - l1Deployer, - l2Provider - ) - await ( await l1TokenBridgeCreator.setTemplates( l1Templates, @@ -405,9 +433,10 @@ export const deployL1TokenBridgeCreator = async ( l2CustomGatewayAddressOnL1.address, l2WethGatewayAddressOnL1.address, l2WethAddressOnL1.address, + l2MulticallAddressOnL1.address, l1WethAddress, l1Multicall.address, - deployFactoryGasParams.gasLimit + gasLimitForL2FactoryDeployment ) ).wait() @@ -426,7 +455,8 @@ export const deployL1TokenBridgeCreator = async ( ) await l1Verifier.verifyWithAddress( 'l1TokenBridgeCreatorLogic', - l1TokenBridgeCreatorLogic.address + l1TokenBridgeCreatorLogic.address, + abi.encode(['address'], [l2MulticallAddressOnL1.address]) ) await l1Verifier.verifyWithAddress( 'l1TokenBridgeCreatorProxy', diff --git a/scripts/goerli-deployment/createTokenBridge.ts b/scripts/deployment/createTokenBridge.ts similarity index 81% rename from scripts/goerli-deployment/createTokenBridge.ts rename to scripts/deployment/createTokenBridge.ts index b2a6039300..0e30a8cf73 100644 --- a/scripts/goerli-deployment/createTokenBridge.ts +++ b/scripts/deployment/createTokenBridge.ts @@ -31,7 +31,7 @@ export const envVars = { * @param l2Url * @returns */ -export const createTokenBridgeOnGoerli = async () => { +export const createTokenBridgeOnTargetChain = async () => { if (envVars.rollupAddress == undefined) throw new Error('Missing ROLLUP_ADDRESS in env vars') if (envVars.rollupOwner == undefined) @@ -51,7 +51,7 @@ export const createTokenBridgeOnGoerli = async () => { const l1Deployer = getSigner(l1Provider, envVars.baseChainDeployerKey) const l2Provider = new JsonRpcProvider(envVars.childChainRpc) - const { l1Network, l2Network: corel2Network } = await registerGoerliNetworks( + const { l1Network, l2Network: corel2Network } = await registerNetworks( l1Provider, l2Provider, envVars.rollupAddress @@ -63,32 +63,33 @@ export const createTokenBridgeOnGoerli = async () => { ) // create token bridge - const deployedContracts = await createTokenBridge( - l1Deployer, - l2Provider, - l1TokenBridgeCreator, - envVars.rollupAddress, - envVars.rollupOwner - ) + const { l1Deployment, l2Deployment, l1MultiCall, l1ProxyAdmin } = + await createTokenBridge( + l1Deployer, + l2Provider, + l1TokenBridgeCreator, + envVars.rollupAddress, + envVars.rollupOwner + ) const l2Network = { ...corel2Network, tokenBridge: { - l1CustomGateway: deployedContracts.l1CustomGateway, - l1ERC20Gateway: deployedContracts.l1StandardGateway, - l1GatewayRouter: deployedContracts.l1Router, - l1MultiCall: '', - l1ProxyAdmin: deployedContracts.l1ProxyAdmin, - l1Weth: deployedContracts.l1Weth, - l1WethGateway: deployedContracts.l1WethGateway, - - l2CustomGateway: deployedContracts.l2CustomGateway, - l2ERC20Gateway: deployedContracts.l2StandardGateway, - l2GatewayRouter: deployedContracts.l2Router, - l2Multicall: '', - l2ProxyAdmin: deployedContracts.l2ProxyAdmin, - l2Weth: deployedContracts.l2Weth, - l2WethGateway: deployedContracts.l2WethGateway, + l1CustomGateway: l1Deployment.customGateway, + l1ERC20Gateway: l1Deployment.standardGateway, + l1GatewayRouter: l1Deployment.router, + l1MultiCall: l1MultiCall, + l1ProxyAdmin: l1ProxyAdmin, + l1Weth: l1Deployment.weth, + l1WethGateway: l1Deployment.wethGateway, + + l2CustomGateway: l2Deployment.customGateway, + l2ERC20Gateway: l2Deployment.standardGateway, + l2GatewayRouter: l2Deployment.router, + l2Multicall: l2Deployment.multicall, + l2ProxyAdmin: l2Deployment.proxyAdmin, + l2Weth: l2Deployment.weth, + l2WethGateway: l2Deployment.wethGateway, }, } @@ -98,7 +99,7 @@ export const createTokenBridgeOnGoerli = async () => { } } -const registerGoerliNetworks = async ( +const registerNetworks = async ( l1Provider: JsonRpcProvider, l2Provider: JsonRpcProvider, rollupAddress: string @@ -170,7 +171,7 @@ const registerGoerliNetworks = async ( } async function main() { - const { l1Network, l2Network } = await createTokenBridgeOnGoerli() + const { l1Network, l2Network } = await createTokenBridgeOnTargetChain() const NETWORK_FILE = 'network.json' fs.writeFileSync( NETWORK_FILE, diff --git a/scripts/goerli-deployment/deployTokenBridgeCreator.ts b/scripts/deployment/deployTokenBridgeCreator.ts similarity index 66% rename from scripts/goerli-deployment/deployTokenBridgeCreator.ts rename to scripts/deployment/deployTokenBridgeCreator.ts index c5a17a01e7..569ce10716 100644 --- a/scripts/goerli-deployment/deployTokenBridgeCreator.ts +++ b/scripts/deployment/deployTokenBridgeCreator.ts @@ -3,9 +3,11 @@ import { L1Network, L2Network, addCustomNetwork } from '@arbitrum/sdk' import { RollupAdminLogic__factory } from '@arbitrum/sdk/dist/lib/abi/factories/RollupAdminLogic__factory' import { deployL1TokenBridgeCreator, + getEstimateForDeployingFactory, getSigner, } from '../atomicTokenBridgeDeployer' import dotenv from 'dotenv' +import { BigNumber } from 'ethers' dotenv.config() @@ -13,10 +15,12 @@ export const envVars = { baseChainRpc: process.env['BASECHAIN_RPC'] as string, baseChainDeployerKey: process.env['BASECHAIN_DEPLOYER_KEY'] as string, childChainRpc: process.env['ORBIT_RPC'] as string, + baseChainWeth: process.env['BASECHAIN_WETH'] as string, + rollupAddress: process.env['ROLLUP_ADDRESS'] as string, + gasLimitForL2FactoryDeployment: + process.env['GAS_LIMIT_FOR_L2_FACTORY_DEPLOYMENT'], } -const ARB_GOERLI_WETH = '0xEe01c0CD76354C383B8c7B4e65EA88D00B06f36f' - /** * Steps: * - read network info from local container and register networks @@ -30,28 +34,58 @@ const ARB_GOERLI_WETH = '0xEe01c0CD76354C383B8c7B4e65EA88D00B06f36f' * @param l2Url * @returns */ -export const deployTokenBridgeCreator = async (rollupAddress: string) => { - if (envVars.baseChainRpc == undefined) +export const deployTokenBridgeCreator = async () => { + if (!envVars.baseChainRpc) { throw new Error('Missing BASECHAIN_RPC in env vars') - if (envVars.baseChainDeployerKey == undefined) + } + if (!envVars.baseChainDeployerKey) { throw new Error('Missing BASECHAIN_DEPLOYER_KEY in env vars') - if (envVars.childChainRpc == undefined) - throw new Error('Missing ORBIT_RPC in env vars') + } + if (!envVars.baseChainWeth) { + throw new Error('Missing BASECHAIN_WETH in env vars') + } + if ( + !(envVars.rollupAddress && envVars.childChainRpc) && + !envVars.gasLimitForL2FactoryDeployment + ) { + throw new Error( + 'Either GAS_LIMIT_FOR_L2_FACTORY_DEPLOYMENT or (ROLLUP_ADDRESS and ORBIT_RPC) must be set in env vars' + ) + } const l1Provider = new JsonRpcProvider(envVars.baseChainRpc) const l1Deployer = getSigner(l1Provider, envVars.baseChainDeployerKey) - const l2Provider = new JsonRpcProvider(envVars.childChainRpc) - await registerGoerliNetworks(l1Provider, l2Provider, rollupAddress) + // get gas limit for L2 factory deployment from env var or do retryable estimate + let gasLimitForL2FactoryDeployment: BigNumber + if (envVars.gasLimitForL2FactoryDeployment) { + gasLimitForL2FactoryDeployment = BigNumber.from( + envVars.gasLimitForL2FactoryDeployment + ) + } else { + const l2Provider = new JsonRpcProvider(envVars.childChainRpc) + await registerNetworks(l1Provider, l2Provider, envVars.rollupAddress) + //// run retryable estimate for deploying L2 factory + const deployFactoryGasParams = await getEstimateForDeployingFactory( + l1Deployer, + l2Provider + ) + gasLimitForL2FactoryDeployment = deployFactoryGasParams.gasLimit + } // deploy L1 creator and set templates const { l1TokenBridgeCreator, retryableSender } = - await deployL1TokenBridgeCreator(l1Deployer, l2Provider, ARB_GOERLI_WETH, true) + await deployL1TokenBridgeCreator( + l1Deployer, + envVars.baseChainWeth, + gasLimitForL2FactoryDeployment, + true + ) return { l1TokenBridgeCreator, retryableSender } } -const registerGoerliNetworks = async ( +const registerNetworks = async ( l1Provider: JsonRpcProvider, l2Provider: JsonRpcProvider, rollupAddress: string @@ -123,10 +157,9 @@ const registerGoerliNetworks = async ( } async function main() { - // this is just random Orbit rollup that will be used to estimate gas needed to deploy L2 token bridge factory via retryable - const rollupAddress = '0x8223bd899C6643483872ed2A7b105b2aC9C8aBEc' + console.log('Deploying token bridge creator...') const { l1TokenBridgeCreator, retryableSender } = - await deployTokenBridgeCreator(rollupAddress) + await deployTokenBridgeCreator() console.log('Token bridge creator deployed!') console.log('L1TokenBridgeCreator:', l1TokenBridgeCreator.address) diff --git a/scripts/local-deployment/localDeploymentLib.ts b/scripts/local-deployment/localDeploymentLib.ts index 0a835417f8..fcc40a08d3 100644 --- a/scripts/local-deployment/localDeploymentLib.ts +++ b/scripts/local-deployment/localDeploymentLib.ts @@ -8,6 +8,7 @@ import { execSync } from 'child_process' import { createTokenBridge, deployL1TokenBridgeCreator, + getEstimateForDeployingFactory, } from '../atomicTokenBridgeDeployer' import { l2Networks } from '@arbitrum/sdk/dist/lib/dataEntities/networks' @@ -106,43 +107,55 @@ export const setupTokenBridgeInLocalEnv = async () => { console.log('Deploying L1TokenBridgeCreator') // a random address for l1Weth const l1Weth = '0x05EcEffc7CBA4e43a410340E849052AD43815aCA' + + //// run retryable estimate for deploying L2 factory + const deployFactoryGasParams = await getEstimateForDeployingFactory( + parentDeployer, + childDeployer.provider! + ) + const gasLimitForL2FactoryDeployment = deployFactoryGasParams.gasLimit + const { l1TokenBridgeCreator, retryableSender } = await deployL1TokenBridgeCreator( parentDeployer, - childDeployer.provider!, - l1Weth + l1Weth, + gasLimitForL2FactoryDeployment ) console.log('L1TokenBridgeCreator', l1TokenBridgeCreator.address) console.log('L1TokenBridgeRetryableSender', retryableSender.address) // create token bridge - console.log('Creating token bridge') - const deployedContracts = await createTokenBridge( - parentDeployer, - childDeployer.provider!, - l1TokenBridgeCreator, - coreL2Network.ethBridge.rollup, - rollupOwner + console.log( + '\nCreating token bridge for rollup', + coreL2Network.ethBridge.rollup ) + const { l1Deployment, l2Deployment, l1MultiCall, l1ProxyAdmin } = + await createTokenBridge( + parentDeployer, + childDeployer.provider!, + l1TokenBridgeCreator, + coreL2Network.ethBridge.rollup, + rollupOwner + ) const l2Network: L2Network = { ...coreL2Network, tokenBridge: { - l1CustomGateway: deployedContracts.l1CustomGateway, - l1ERC20Gateway: deployedContracts.l1StandardGateway, - l1GatewayRouter: deployedContracts.l1Router, - l1MultiCall: deployedContracts.l1Multicall, - l1ProxyAdmin: deployedContracts.l1ProxyAdmin, - l1Weth: deployedContracts.l1Weth, - l1WethGateway: deployedContracts.l1WethGateway, - - l2CustomGateway: deployedContracts.l2CustomGateway, - l2ERC20Gateway: deployedContracts.l2StandardGateway, - l2GatewayRouter: deployedContracts.l2Router, - l2Multicall: deployedContracts.l2Multicall, - l2ProxyAdmin: deployedContracts.l2ProxyAdmin, - l2Weth: deployedContracts.l2Weth, - l2WethGateway: deployedContracts.l2WethGateway, + l1CustomGateway: l1Deployment.customGateway, + l1ERC20Gateway: l1Deployment.standardGateway, + l1GatewayRouter: l1Deployment.router, + l1MultiCall: l1MultiCall, + l1ProxyAdmin: l1ProxyAdmin, + l1Weth: l1Deployment.weth, + l1WethGateway: l1Deployment.wethGateway, + + l2CustomGateway: l2Deployment.customGateway, + l2ERC20Gateway: l2Deployment.standardGateway, + l2GatewayRouter: l2Deployment.router, + l2Multicall: l2Deployment.multicall, + l2ProxyAdmin: l2Deployment.proxyAdmin, + l2Weth: l2Deployment.weth, + l2WethGateway: l2Deployment.wethGateway, }, } diff --git a/scripts/upgradeTemplate.ts b/scripts/upgradeTemplate.ts new file mode 100644 index 0000000000..59fb2b8839 --- /dev/null +++ b/scripts/upgradeTemplate.ts @@ -0,0 +1,33 @@ +import { JsonRpcProvider } from '@ethersproject/providers' +import { L2AtomicTokenBridgeFactory__factory } from '../build/types' +import dotenv from 'dotenv' +import { Wallet } from 'ethers' + +dotenv.config() + +async function main() { + const deployRpc = process.env['BASECHAIN_RPC'] as string + if (deployRpc == undefined) { + throw new Error("Env var 'BASECHAIN_RPC' not set") + } + const rpc = new JsonRpcProvider(deployRpc) + + const deployKey = process.env['BASECHAIN_DEPLOYER_KEY'] as string + if (deployKey == undefined) { + throw new Error("Env var 'BASECHAIN_DEPLOYER_KEY' not set") + } + const deployer = new Wallet(deployKey).connect(rpc) + + console.log( + 'Deploying L2AtomicTokenBridgeFactory to chain', + await deployer.getChainId() + ) + const l2TokenBridgeFactory = await new L2AtomicTokenBridgeFactory__factory( + deployer + ).deploy() + await l2TokenBridgeFactory.deployed() + + console.log('l2TokenBridgeFactory:', l2TokenBridgeFactory.address) +} + +main().then(() => console.log('Done.')) diff --git a/tasks/compareBytecode.ts b/tasks/compareBytecode.ts new file mode 100644 index 0000000000..8590838c89 --- /dev/null +++ b/tasks/compareBytecode.ts @@ -0,0 +1,57 @@ +import { task } from "hardhat/config"; +import { ethers } from "ethers"; +import "@nomiclabs/hardhat-etherscan" +import { Bytecode } from "@nomiclabs/hardhat-etherscan/dist/src/solc/bytecode" +import { TASK_VERIFY_GET_CONTRACT_INFORMATION, TASK_VERIFY_GET_COMPILER_VERSIONS, TASK_VERIFY_GET_LIBRARIES } from "@nomiclabs/hardhat-etherscan/dist/src/constants" +import fs from "fs"; + +task("compareBytecode", "Compares deployed bytecode with local builds") + .addParam("contractAddrs", "A comma-separated list of deployed contract addresses") + .setAction(async ({ contractAddrs }, hre) => { + const addresses = contractAddrs.split(','); + + // Get all local contract artifact paths + const artifactPaths = await hre.artifacts.getArtifactPaths(); + + for (const contractAddr of addresses) { + + // Fetch deployed contract bytecode + const deployedBytecode = await hre.ethers.provider.getCode(contractAddr.trim()); + const deployedCodeHash = ethers.utils.keccak256(deployedBytecode); + let matchFound = false; + + for (const artifactPath of artifactPaths) { + const artifact = JSON.parse(fs.readFileSync(artifactPath, "utf8")); + if (artifact.deployedBytecode) { + const localCodeHash = ethers.utils.keccak256(artifact.deployedBytecode); + + // Compare codehashes + if (deployedCodeHash === localCodeHash) { + console.log(`Contract Address ${contractAddr.trim()} matches with ${artifact.contractName}`); + matchFound = true; + break; + } + } + } + + if (!matchFound) { + const deployedBytecodeHex = deployedBytecode.startsWith("0x") + ? deployedBytecode.slice(2) + : deployedBytecode; + try { + const info = await hre.run(TASK_VERIFY_GET_CONTRACT_INFORMATION, { + deployedBytecode: new Bytecode(deployedBytecodeHex), + matchingCompilerVersions: await hre.run( + TASK_VERIFY_GET_COMPILER_VERSIONS + ), + libraries: await hre.run(TASK_VERIFY_GET_LIBRARIES), + }) + console.log(`Contract Address ${contractAddr.trim()} matches with ${info.contractName} without checking constructor arguments`); + } catch (error) { + console.log(`No matching contract found for address ${contractAddr.trim()}`); + } + } + } + }); + +export default {}; diff --git a/test-e2e/creationCodeTest.ts b/test-e2e/creationCodeTest.ts new file mode 100644 index 0000000000..30b6048c76 --- /dev/null +++ b/test-e2e/creationCodeTest.ts @@ -0,0 +1,198 @@ +import hre, { ethers } from 'hardhat' +import { expect } from 'chai' +import { JsonRpcProvider } from '@ethersproject/providers' +import { + CreationCodeTest, + CreationCodeTest__factory, + L1AtomicTokenBridgeCreator, + L1AtomicTokenBridgeCreator__factory, +} from '../build/types' +import path from 'path' +import fs from 'fs' + +const LOCALHOST_L2_RPC = 'http://localhost:8547' + +const AE_WETH_EXPECTED_CONSTRUCTOR_SIZE = 348 +const UPGRADE_EXECUTOR_EXPECTED_CONSTRUCTOR_SIZE = 242 + +let provider: JsonRpcProvider +let creationCodeTester: CreationCodeTest +let l1TokenBridgeCreator: L1AtomicTokenBridgeCreator + +/** + * This test ensures that the Solidity lib generates the same creation code as the + * compiler for the contracts which are deployed to the child chain. + * + * The reason why we perform constructor check is due to atomic token bridge creator + * implementation. Due to contract size limits, we deploy child chain templates to the + * parent chain. When token bridge is being created, parent chain creator will fetch the + * runtime bytecode of the templates and send it to the child chain via retryable tickets. + * Child chain factory will then prepend the empty-constructor bytecode to the runtime code + * and use resulting bytecode for deployment. That's why we need to ensure that those + * impacted contracts don't have any logic in their constructors, as that logic can't be + * executed when deploying to the child chain. + * + * All impacted contracts have 32 bytes of constructor bytecode which look like this: + * 608060405234801561001057600080fd5b50615e7c806100206000396000f3fe + * This constructor checks that there's no callvalue, copies the contract code to memory + * and returns it. The only place where constructor bytecode differs between contracts + * is in 61xxxx80 where xxxx is the length of the contract's bytecode. + * + * Exception are aeWETH and UpgradeExecutor contracts. Their constructors are not empty as they + * contain logic to set the logic contract to the initialized state. In our system we need to + * perform this initialization by chaild chain factory. It is important though that constructor + * for these contracts never changes. That's why we check the constructor size matches the + * expected hardcoded size. + */ +describe('creationCodeTest', () => { + before(async function () { + /// get default deployer params in local test env + provider = new ethers.providers.JsonRpcProvider(LOCALHOST_L2_RPC) + const deployerKey = ethers.utils.sha256( + ethers.utils.toUtf8Bytes('user_token_bridge_deployer') + ) + const deployer = new ethers.Wallet(deployerKey, provider) + + /// tester which implements the 'getCreationCode' lib function + const testerFactory = await new CreationCodeTest__factory(deployer).deploy() + creationCodeTester = await testerFactory.deployed() + + /// token bridge creator which has the templates stored + l1TokenBridgeCreator = await _getTokenBridgeCreator(provider) + }) + + it('compiler generated and solidity lib generated creation code should match for L2 templates', async function () { + expect(await _getCompilerGeneratedCreationCode('L2GatewayRouter')).to.be.eq( + await _getSolidityLibGeneratedCreationCode( + provider, + creationCodeTester, + await l1TokenBridgeCreator.l2RouterTemplate() + ) + ) + + expect(await _getCompilerGeneratedCreationCode('L2ERC20Gateway')).to.be.eq( + await _getSolidityLibGeneratedCreationCode( + provider, + creationCodeTester, + await l1TokenBridgeCreator.l2StandardGatewayTemplate() + ) + ) + + expect(await _getCompilerGeneratedCreationCode('L2CustomGateway')).to.be.eq( + await _getSolidityLibGeneratedCreationCode( + provider, + creationCodeTester, + await l1TokenBridgeCreator.l2CustomGatewayTemplate() + ) + ) + + expect(await _getCompilerGeneratedCreationCode('L2WethGateway')).to.be.eq( + await _getSolidityLibGeneratedCreationCode( + provider, + creationCodeTester, + await l1TokenBridgeCreator.l2WethGatewayTemplate() + ) + ) + + expect(await _getCompilerGeneratedCreationCode('ArbMulticall2')).to.be.eq( + await _getSolidityLibGeneratedCreationCode( + provider, + creationCodeTester, + await l1TokenBridgeCreator.l2MulticallTemplate() + ) + ) + + expect( + await _getCompilerGeneratedCreationCode('L2AtomicTokenBridgeFactory') + ).to.be.eq( + await _getSolidityLibGeneratedCreationCode( + provider, + creationCodeTester, + await l1TokenBridgeCreator.l2TokenBridgeFactoryTemplate() + ) + ) + }) + + it('aeWETH constructor has expected size', async function () { + const constructorBytecode = await _getConstructorBytecode('aeWETH') + const constructorBytecodeLength = _lengthInBytes(constructorBytecode) + + expect(constructorBytecodeLength).to.be.eq( + AE_WETH_EXPECTED_CONSTRUCTOR_SIZE + ) + }) + + it('UpgradeExecutor constructor has expected size', async function () { + const constructorBytecode = await _getConstructorBytecode('UpgradeExecutor') + const constructorBytecodeLength = _lengthInBytes(constructorBytecode) + + expect(constructorBytecodeLength).to.be.eq( + UPGRADE_EXECUTOR_EXPECTED_CONSTRUCTOR_SIZE + ) + }) +}) + +async function _getCompilerGeneratedCreationCode( + contractName: string +): Promise { + // get creation code generated by the compiler + const artifact = await hre.artifacts.readArtifact(contractName) + return artifact.bytecode +} + +async function _getSolidityLibGeneratedCreationCode( + provider: JsonRpcProvider, + creationCodeTester: CreationCodeTest, + templateAddress: string +) { + const runtimeCode = await provider.getCode(templateAddress) + const solidityLibGeneratedCreationCode = + await creationCodeTester.creationCodeFor(runtimeCode) + + return solidityLibGeneratedCreationCode +} + +async function _getTokenBridgeCreator( + provider: JsonRpcProvider +): Promise { + const localNetworkFile = path.join(__dirname, '..', 'network.json') + if (!fs.existsSync(localNetworkFile)) { + throw new Error("Can't find network.json file") + } + const data = JSON.parse(fs.readFileSync(localNetworkFile).toString()) + return L1AtomicTokenBridgeCreator__factory.connect( + data['l1TokenBridgeCreator'], + provider + ) +} + +/** + * Get constructor bytecode as a difference between creation and deployed bytecode + * @param contractName + * @returns + */ +async function _getConstructorBytecode(contractName: string): Promise { + const artifact = await hre.artifacts.readArtifact(contractName) + + // remove '0x' + const creationCode = artifact.bytecode.substring(2) + const runtimeCode = artifact.deployedBytecode.substring(2) + + if (!creationCode.includes(runtimeCode)) { + throw new Error( + `Error while extracting constructor bytecode for contract ${contractName}.` + ) + } + + // extract the constructor code + return creationCode.replace(runtimeCode, '') +} + +/** + * Every byte in the constructor bytecode is represented by 2 characters in hex + * @param hex + * @returns + */ +function _lengthInBytes(hex: string): number { + return hex.length / 2 +} diff --git a/test-e2e/tokenBridgeDeploymentTest.ts b/test-e2e/tokenBridgeDeploymentTest.ts index 9a65708964..9b8d221876 100644 --- a/test-e2e/tokenBridgeDeploymentTest.ts +++ b/test-e2e/tokenBridgeDeploymentTest.ts @@ -1,10 +1,14 @@ import { JsonRpcProvider, Provider, Filter } from '@ethersproject/providers' import { + AeWETH__factory, + ArbMulticall2, + ArbMulticall2__factory, BeaconProxyFactory__factory, IERC20Bridge__factory, + IInboxProxyAdmin__factory, IInbox__factory, IOwnable__factory, - IRollupCore__factory, + L1AtomicTokenBridgeCreator, L1AtomicTokenBridgeCreator__factory, L1CustomGateway, L1CustomGateway__factory, @@ -22,6 +26,8 @@ import { L2GatewayRouter__factory, L2WethGateway, L2WethGateway__factory, + StandardArbERC20__factory, + UpgradeableBeacon__factory, } from '../build/types' import { abi as UpgradeExecutorABI } from '@offchainlabs/upgrade-executor/build/contracts/src/UpgradeExecutor.sol/UpgradeExecutor.json' import { RollupCore__factory } from '@arbitrum/sdk/dist/lib/abi/factories/RollupCore__factory' @@ -40,6 +46,9 @@ const config = { let l1Provider: JsonRpcProvider let l2Provider: JsonRpcProvider +// when code at address is empty, ethers.js returns '0x' +const EMPTY_CODE_LENGTH = 2 + describe('tokenBridge', () => { it('should have deployed and initialized token bridge contracts', async function () { l1Provider = new JsonRpcProvider(config.l1Url) @@ -67,8 +76,12 @@ describe('tokenBridge', () => { } } - /// get addresses - const { l1, l2 } = await _getTokenBridgeAddresses( + console.log( + `Testing token bridge deployment for rollup ${rollupAddress} deployed by creator ${l1TokenBridgeCreator}` + ) + + /// get core contract and token bridge addresses + const { rollupAddresses, l1Deployment, l2Deployment } = await _getAddresses( rollupAddress, l1TokenBridgeCreator ) @@ -86,68 +99,93 @@ describe('tokenBridge', () => { ) await checkL1RouterInitialization( - L1GatewayRouter__factory.connect(l1.router, l1Provider), - l1, - l2 + L1GatewayRouter__factory.connect(l1Deployment.router, l1Provider), + l1Deployment, + l2Deployment, + rollupAddresses ) await checkL1StandardGatewayInitialization( - L1ERC20Gateway__factory.connect(l1.standardGateway, l1Provider), - l1, - l2 + L1ERC20Gateway__factory.connect(l1Deployment.standardGateway, l1Provider), + l1Deployment, + l2Deployment, + rollupAddresses ) await checkL1CustomGatewayInitialization( - L1CustomGateway__factory.connect(l1.customGateway, l1Provider), - l1, - l2 + L1CustomGateway__factory.connect(l1Deployment.customGateway, l1Provider), + l1Deployment, + l2Deployment, + rollupAddresses ) - const usingFeeToken = await isUsingFeeToken(l1.inbox, l1Provider) - if (!usingFeeToken) + const usingFeeToken = await _isUsingFeeToken( + rollupAddresses.inbox, + l1Provider + ) + if (!usingFeeToken) { await checkL1WethGatewayInitialization( - L1WethGateway__factory.connect(l1.wethGateway, l1Provider), - l1, - l2 + L1WethGateway__factory.connect(l1Deployment.wethGateway, l1Provider), + l1Deployment, + l2Deployment, + rollupAddresses ) + } else { + expect(l1Deployment.wethGateway).to.be.eq(ethers.constants.AddressZero) + expect(l1Deployment.weth).to.be.eq(ethers.constants.AddressZero) + expect(l2Deployment.wethGateway).to.be.eq(ethers.constants.AddressZero) + expect(l2Deployment.weth).to.be.eq(ethers.constants.AddressZero) + } //// L2 checks await checkL2RouterInitialization( - L2GatewayRouter__factory.connect(l2.router, l2Provider), - l1, - l2 + L2GatewayRouter__factory.connect(l2Deployment.router, l2Provider), + l1Deployment, + l2Deployment ) await checkL2StandardGatewayInitialization( - L2ERC20Gateway__factory.connect(l2.standardGateway, l2Provider), - l1, - l2 + L2ERC20Gateway__factory.connect(l2Deployment.standardGateway, l2Provider), + l1Deployment, + l2Deployment ) await checkL2CustomGatewayInitialization( - L2CustomGateway__factory.connect(l2.customGateway, l2Provider), - l1, - l2 + L2CustomGateway__factory.connect(l2Deployment.customGateway, l2Provider), + l1Deployment, + l2Deployment + ) + + await checkL2MulticallInitialization( + ArbMulticall2__factory.connect(l2Deployment.multicall, l2Provider) ) if (!usingFeeToken) { await checkL2WethGatewayInitialization( - L2WethGateway__factory.connect(l2.wethGateway, l2Provider), - l1, - l2 + L2WethGateway__factory.connect(l2Deployment.wethGateway, l2Provider), + l1Deployment, + l2Deployment ) } - const upgExecutor = new ethers.Contract( - l2.upgradeExecutor, + const l1UpgradeExecutor = new ethers.Contract( + rollupAddresses.upgradeExecutor, + UpgradeExecutorABI, + l1Provider + ) + await checkL1UpgradeExecutorInitialization(l1UpgradeExecutor, rollupAddresses); + + const l2UpgradeExecutor = new ethers.Contract( + l2Deployment.upgradeExecutor, UpgradeExecutorABI, l2Provider ) - await checkL2UpgradeExecutorInitialization(upgExecutor, l1) + await checkL2UpgradeExecutorInitialization(l2UpgradeExecutor, rollupAddresses) - await checkL1Ownership(l1) - await checkL2Ownership(l2) + await checkL1Ownership(l1Deployment, rollupAddresses) + await checkL2Ownership(l2Deployment, usingFeeToken) + await checkLogicContracts(usingFeeToken, l2Deployment) }) }) @@ -155,40 +193,42 @@ describe('tokenBridge', () => { async function checkL1RouterInitialization( l1Router: L1GatewayRouter, - l1: L1, - l2: L2 + l1Deployment: L1DeploymentAddresses, + l2Deployment: L2DeploymentAddresses, + rollupAddresses: RollupAddresses ) { console.log('checkL1RouterInitialization') expect((await l1Router.defaultGateway()).toLowerCase()).to.be.eq( - l1.standardGateway.toLowerCase() + l1Deployment.standardGateway.toLowerCase() ) expect((await l1Router.inbox()).toLowerCase()).to.be.eq( - l1.inbox.toLowerCase() + rollupAddresses.inbox.toLowerCase() ) expect((await l1Router.router()).toLowerCase()).to.be.eq( ethers.constants.AddressZero ) expect((await l1Router.counterpartGateway()).toLowerCase()).to.be.eq( - l2.router.toLowerCase() + l2Deployment.router.toLowerCase() ) } async function checkL1StandardGatewayInitialization( l1ERC20Gateway: L1ERC20Gateway, - l1: L1, - l2: L2 + l1Deployment: L1DeploymentAddresses, + l2Deployment: L2DeploymentAddresses, + rollupAddresses: RollupAddresses ) { console.log('checkL1StandardGatewayInitialization') expect((await l1ERC20Gateway.counterpartGateway()).toLowerCase()).to.be.eq( - l2.standardGateway.toLowerCase() + l2Deployment.standardGateway.toLowerCase() ) expect((await l1ERC20Gateway.router()).toLowerCase()).to.be.eq( - l1.router.toLowerCase() + l1Deployment.router.toLowerCase() ) expect((await l1ERC20Gateway.inbox()).toLowerCase()).to.be.eq( - l1.inbox.toLowerCase() + rollupAddresses.inbox.toLowerCase() ) expect((await l1ERC20Gateway.l2BeaconProxyFactory()).toLowerCase()).to.be.eq( ( @@ -213,21 +253,22 @@ async function checkL1StandardGatewayInitialization( async function checkL1CustomGatewayInitialization( l1CustomGateway: L1CustomGateway, - l1: L1, - l2: L2 + l1Deployment: L1DeploymentAddresses, + l2Deployment: L2DeploymentAddresses, + rollupAddresses: RollupAddresses ) { console.log('checkL1CustomGatewayInitialization') expect((await l1CustomGateway.counterpartGateway()).toLowerCase()).to.be.eq( - l2.customGateway.toLowerCase() + l2Deployment.customGateway.toLowerCase() ) expect((await l1CustomGateway.router()).toLowerCase()).to.be.eq( - l1.router.toLowerCase() + l1Deployment.router.toLowerCase() ) expect((await l1CustomGateway.inbox()).toLowerCase()).to.be.eq( - l1.inbox.toLowerCase() + rollupAddresses.inbox.toLowerCase() ) expect((await l1CustomGateway.whitelist()).toLowerCase()).to.be.eq( @@ -237,21 +278,22 @@ async function checkL1CustomGatewayInitialization( async function checkL1WethGatewayInitialization( l1WethGateway: L1WethGateway, - l1: L1, - l2: L2 + l1Deployment: L1DeploymentAddresses, + l2Deployment: L2DeploymentAddresses, + rollupAddresses: RollupAddresses ) { console.log('checkL1WethGatewayInitialization') expect((await l1WethGateway.counterpartGateway()).toLowerCase()).to.be.eq( - l2.wethGateway.toLowerCase() + l2Deployment.wethGateway.toLowerCase() ) expect((await l1WethGateway.router()).toLowerCase()).to.be.eq( - l1.router.toLowerCase() + l1Deployment.router.toLowerCase() ) expect((await l1WethGateway.inbox()).toLowerCase()).to.be.eq( - l1.inbox.toLowerCase() + rollupAddresses.inbox.toLowerCase() ) expect((await l1WethGateway.l1Weth()).toLowerCase()).to.not.be.eq( @@ -263,9 +305,23 @@ async function checkL1WethGatewayInitialization( ) } +async function checkL1UpgradeExecutorInitialization( + l1Executor: Contract, + rollupAddresses: RollupAddresses +) { + console.log('checkL1UpgradeExecutorInitialization') + + //// check assigned/revoked roles are correctly set + const adminRole = await l1Executor.ADMIN_ROLE() + const executorRole = await l1Executor.EXECUTOR_ROLE() + + expect(await l1Executor.hasRole(adminRole, l1Executor.address)).to.be.true + expect(await l1Executor.hasRole(executorRole, rollupAddresses.rollupOwner)).to.be.true +} + async function checkL2UpgradeExecutorInitialization( l2Executor: Contract, - l1: L1 + rollupAddresses: RollupAddresses ) { console.log('checkL2UpgradeExecutorInitialization') @@ -274,8 +330,17 @@ async function checkL2UpgradeExecutorInitialization( const executorRole = await l2Executor.EXECUTOR_ROLE() expect(await l2Executor.hasRole(adminRole, l2Executor.address)).to.be.true - expect(await l2Executor.hasRole(executorRole, l1.rollupOwner)).to.be.true - const aliasedL1Executor = applyAlias(l1.upgradeExecutor) + + const isL1RollupOwnerContract = + (await l1Provider.getCode(rollupAddresses.rollupOwner)).length > + EMPTY_CODE_LENGTH + + const l2RollupOwner = isL1RollupOwnerContract + ? applyAlias(rollupAddresses.rollupOwner) + : rollupAddresses.rollupOwner + + expect(await l2Executor.hasRole(executorRole, l2RollupOwner)).to.be.true + const aliasedL1Executor = applyAlias(rollupAddresses.upgradeExecutor) expect(await l2Executor.hasRole(executorRole, aliasedL1Executor)).to.be.true } @@ -283,13 +348,13 @@ async function checkL2UpgradeExecutorInitialization( async function checkL2RouterInitialization( l2Router: L2GatewayRouter, - l1: L1, - l2: L2 + l1Deployment: L1DeploymentAddresses, + l2Deployment: L2DeploymentAddresses ) { console.log('checkL2RouterInitialization') expect((await l2Router.defaultGateway()).toLowerCase()).to.be.eq( - l2.standardGateway.toLowerCase() + l2Deployment.standardGateway.toLowerCase() ) expect((await l2Router.router()).toLowerCase()).to.be.eq( @@ -297,26 +362,30 @@ async function checkL2RouterInitialization( ) expect((await l2Router.counterpartGateway()).toLowerCase()).to.be.eq( - l1.router.toLowerCase() + l1Deployment.router.toLowerCase() ) } async function checkL2StandardGatewayInitialization( l2ERC20Gateway: L2ERC20Gateway, - l1: L1, - l2: L2 + l1Deployment: L1DeploymentAddresses, + l2Deployment: L2DeploymentAddresses ) { console.log('checkL2StandardGatewayInitialization') expect((await l2ERC20Gateway.counterpartGateway()).toLowerCase()).to.be.eq( - l1.standardGateway.toLowerCase() + l1Deployment.standardGateway.toLowerCase() ) expect((await l2ERC20Gateway.router()).toLowerCase()).to.be.eq( - l2.router.toLowerCase() + l2Deployment.router.toLowerCase() ) - expect((await l2ERC20Gateway.beaconProxyFactory()).toLowerCase()).to.be.eq( + const beaconProxyFactory = BeaconProxyFactory__factory.connect( + await l2ERC20Gateway.beaconProxyFactory(), + l2Provider + ) + expect(beaconProxyFactory.address.toLowerCase()).to.be.eq( ( await L1ERC20Gateway__factory.connect( await l2ERC20Gateway.counterpartGateway(), @@ -333,37 +402,49 @@ async function checkL2StandardGatewayInitialization( ).cloneableProxyHash() ).toLowerCase() ) + + const beacon = UpgradeableBeacon__factory.connect( + await beaconProxyFactory.beacon(), + l2Provider + ) + expect(await beacon.owner()).to.be.eq(l2Deployment.upgradeExecutor) + + const standardArbERC20 = StandardArbERC20__factory.connect( + await beacon.implementation(), + l2Provider + ) + expect(await _isInitialized(standardArbERC20.address, l2Provider)).to.be.true } async function checkL2CustomGatewayInitialization( l2CustomGateway: L2CustomGateway, - l1: L1, - l2: L2 + l1Deployment: L1DeploymentAddresses, + l2Deployment: L2DeploymentAddresses ) { console.log('checkL2CustomGatewayInitialization') expect((await l2CustomGateway.counterpartGateway()).toLowerCase()).to.be.eq( - l1.customGateway.toLowerCase() + l1Deployment.customGateway.toLowerCase() ) expect((await l2CustomGateway.router()).toLowerCase()).to.be.eq( - l2.router.toLowerCase() + l2Deployment.router.toLowerCase() ) } async function checkL2WethGatewayInitialization( l2WethGateway: L2WethGateway, - l1: L1, - l2: L2 + l1Deployment: L1DeploymentAddresses, + l2Deployment: L2DeploymentAddresses ) { console.log('checkL2WethGatewayInitialization') expect((await l2WethGateway.counterpartGateway()).toLowerCase()).to.be.eq( - l1.wethGateway.toLowerCase() + l1Deployment.wethGateway.toLowerCase() ) expect((await l2WethGateway.router()).toLowerCase()).to.be.eq( - l2.router.toLowerCase() + l2Deployment.router.toLowerCase() ) expect((await l2WethGateway.l1Weth()).toLowerCase()).to.not.be.eq( @@ -375,65 +456,147 @@ async function checkL2WethGatewayInitialization( ) } -async function checkL1Ownership(l1: L1) { +async function checkL2MulticallInitialization(l2Multicall: ArbMulticall2) { + // check l2Multicall is deployed + const l2MulticallCode = await l2Provider.getCode(l2Multicall.address) + expect(l2MulticallCode.length).to.be.gt(EMPTY_CODE_LENGTH) +} + +async function checkL1Ownership( + l1Deployment: L1DeploymentAddresses, + rollupAddresses: RollupAddresses +) { console.log('checkL1Ownership') // check proxyAdmins - expect(await _getProxyAdmin(l1.router, l1Provider)).to.be.eq(l1.proxyAdmin) - expect(await _getProxyAdmin(l1.standardGateway, l1Provider)).to.be.eq( - l1.proxyAdmin + + expect(await _getProxyAdmin(l1Deployment.router, l1Provider)).to.be.eq( + rollupAddresses.proxyAdmin ) - expect(await _getProxyAdmin(l1.customGateway, l1Provider)).to.be.eq( - l1.proxyAdmin + expect( + await _getProxyAdmin(l1Deployment.standardGateway, l1Provider) + ).to.be.eq(rollupAddresses.proxyAdmin) + expect(await _getProxyAdmin(l1Deployment.customGateway, l1Provider)).to.be.eq( + rollupAddresses.proxyAdmin ) - if (l1.wethGateway !== ethers.constants.AddressZero) { - expect(await _getProxyAdmin(l1.wethGateway, l1Provider)).to.be.eq( - l1.proxyAdmin + if (l1Deployment.wethGateway !== ethers.constants.AddressZero) { + expect(await _getProxyAdmin(l1Deployment.wethGateway, l1Provider)).to.be.eq( + rollupAddresses.proxyAdmin ) } - expect(await _getProxyAdmin(l1.upgradeExecutor, l1Provider)).to.be.eq( - l1.proxyAdmin - ) + expect( + await _getProxyAdmin(rollupAddresses.upgradeExecutor, l1Provider) + ).to.be.eq(rollupAddresses.proxyAdmin) // check ownables - expect(await _getOwner(l1.proxyAdmin, l1Provider)).to.be.eq( - l1.upgradeExecutor + expect(await _getOwner(rollupAddresses.proxyAdmin, l1Provider)).to.be.eq( + rollupAddresses.upgradeExecutor ) - expect(await _getOwner(l1.router, l1Provider)).to.be.eq(l1.upgradeExecutor) - expect(await _getOwner(l1.customGateway, l1Provider)).to.be.eq( - l1.upgradeExecutor + expect(await _getOwner(l1Deployment.router, l1Provider)).to.be.eq( + rollupAddresses.upgradeExecutor + ) + expect(await _getOwner(l1Deployment.customGateway, l1Provider)).to.be.eq( + rollupAddresses.upgradeExecutor ) } -async function checkL2Ownership(l2: L2) { +async function checkL2Ownership( + l2Deployment: L2DeploymentAddresses, + usingFeeToken: boolean +) { console.log('checkL2Ownership') - const l2ProxyAdmin = await _getProxyAdmin(l2.router, l2Provider) + const l2ProxyAdmin = await _getProxyAdmin(l2Deployment.router, l2Provider) // check proxyAdmins - expect(await _getProxyAdmin(l2.router, l2Provider)).to.be.eq(l2ProxyAdmin) - expect(await _getProxyAdmin(l2.standardGateway, l2Provider)).to.be.eq( + expect(await _getProxyAdmin(l2Deployment.router, l2Provider)).to.be.eq( l2ProxyAdmin ) - expect(await _getProxyAdmin(l2.customGateway, l2Provider)).to.be.eq( + expect( + await _getProxyAdmin(l2Deployment.standardGateway, l2Provider) + ).to.be.eq(l2ProxyAdmin) + expect(await _getProxyAdmin(l2Deployment.customGateway, l2Provider)).to.be.eq( l2ProxyAdmin ) - if (l2.wethGateway != ethers.constants.AddressZero) { - expect(await _getProxyAdmin(l2.wethGateway, l2Provider)).to.be.eq( + if (!usingFeeToken) { + expect(await _getProxyAdmin(l2Deployment.wethGateway, l2Provider)).to.be.eq( l2ProxyAdmin ) } - expect(await _getProxyAdmin(l2.upgradeExecutor, l2Provider)).to.be.eq( - l2ProxyAdmin - ) + expect( + await _getProxyAdmin(l2Deployment.upgradeExecutor, l2Provider) + ).to.be.eq(l2ProxyAdmin) // check ownables - expect(await _getOwner(l2ProxyAdmin, l2Provider)).to.be.eq(l2.upgradeExecutor) + expect(await _getOwner(l2ProxyAdmin, l2Provider)).to.be.eq( + l2Deployment.upgradeExecutor.toLowerCase() + ) +} + +async function checkLogicContracts( + usingFeeToken: boolean, + l2Deployment: L2DeploymentAddresses +) { + console.log('checkLogicContracts') + + const upgExecutorLogic = await _getLogicAddress( + l2Deployment.upgradeExecutor, + l2Provider + ) + expect(await _isInitialized(upgExecutorLogic, l2Provider)).to.be.true + + const routerLogic = await _getLogicAddress(l2Deployment.router, l2Provider) + expect( + await L2GatewayRouter__factory.connect( + routerLogic, + l2Provider + ).counterpartGateway() + ).to.be.not.eq(ethers.constants.AddressZero) + + const standardGatewayLogic = await _getLogicAddress( + l2Deployment.standardGateway, + l2Provider + ) + expect( + await L2ERC20Gateway__factory.connect( + standardGatewayLogic, + l2Provider + ).counterpartGateway() + ).to.be.not.eq(ethers.constants.AddressZero) + + const customGatewayLogic = await _getLogicAddress( + l2Deployment.customGateway, + l2Provider + ) + expect( + await L2CustomGateway__factory.connect( + customGatewayLogic, + l2Provider + ).counterpartGateway() + ).to.be.not.eq(ethers.constants.AddressZero) + + if (!usingFeeToken) { + const wethGatewayLogic = await _getLogicAddress( + l2Deployment.wethGateway, + l2Provider + ) + expect( + await L2WethGateway__factory.connect( + wethGatewayLogic, + l2Provider + ).counterpartGateway() + ).to.be.not.eq(ethers.constants.AddressZero) + + const wethLogic = await _getLogicAddress(l2Deployment.weth, l2Provider) + expect( + await AeWETH__factory.connect(wethLogic, l2Provider).l2Gateway() + ).to.be.not.eq(ethers.constants.AddressZero) + } } //// utils -async function isUsingFeeToken(inbox: string, l1Provider: JsonRpcProvider) { +async function _isUsingFeeToken(inbox: string, l1Provider: JsonRpcProvider) { const bridge = await IInbox__factory.connect(inbox, l1Provider).bridge() try { @@ -445,107 +608,50 @@ async function isUsingFeeToken(inbox: string, l1Provider: JsonRpcProvider) { return true } -async function _getTokenBridgeAddresses( +async function _getAddresses( rollupAddress: string, l1TokenBridgeCreatorAddress: string ) { - const inboxAddress = await RollupCore__factory.connect( - rollupAddress, - l1Provider - ).inbox() - const l1TokenBridgeCreator = L1AtomicTokenBridgeCreator__factory.connect( l1TokenBridgeCreatorAddress, l1Provider ) - //// L1 - // find all the events emitted by this address - - const filter: Filter = { - address: l1TokenBridgeCreatorAddress, - topics: [ - ethers.utils.id( - 'OrbitTokenBridgeCreated(address,address,address,address,address,address,address,address)' - ), - ethers.utils.hexZeroPad(inboxAddress, 32), - ], - } - - const currentBlock = await l1Provider.getBlockNumber() - const fromBlock = currentBlock - 100000 // ~last 24h on - const logs = await l1Provider.getLogs({ - ...filter, - fromBlock: fromBlock, - toBlock: 'latest', - }) + /// get core contracts addresses + const inbox = await RollupCore__factory.connect( + rollupAddress, + l1Provider + ).inbox() - if (logs.length === 0) { - throw new Error( - "Couldn't find any OrbitTokenBridgeCreated events in block range[" + - fromBlock + - ',latest]' - ) - } + const multicall = await l1TokenBridgeCreator.l1Multicall() + const proxyAdmin = await IInboxProxyAdmin__factory.connect( + inbox, + l1Provider + ).getProxyAdmin() - const logData = l1TokenBridgeCreator.interface.parseLog(logs[0]) + const upgradeExecutor = await IOwnable__factory.connect( + rollupAddress, + l1Provider + ).owner() - const { - inbox, - owner, - router, - standardGateway, - customGateway, - wethGateway, - proxyAdmin, - upgradeExecutor, - } = logData.args - const l1 = { + const rollupAddresses = { + rollup: rollupAddress.toLowerCase(), inbox: inbox.toLowerCase(), - rollupOwner: owner.toLowerCase(), - router: router.toLowerCase(), - standardGateway: standardGateway.toLowerCase(), - customGateway: customGateway.toLowerCase(), - wethGateway: wethGateway.toLowerCase(), + rollupOwner: await _getRollupOwnerFromLogs( + l1Provider, + l1TokenBridgeCreator, + inbox + ), proxyAdmin: proxyAdmin.toLowerCase(), upgradeExecutor: upgradeExecutor.toLowerCase(), + multicall: multicall.toLowerCase(), } - const usingFeeToken = await isUsingFeeToken(l1.inbox, l1Provider) + /// fetch deployment addresses from registry + const l1Deployment = await l1TokenBridgeCreator.inboxToL1Deployment(inbox) + const l2Deployment = await l1TokenBridgeCreator.inboxToL2Deployment(inbox) - const chainId = await IRollupCore__factory.connect( - rollupAddress, - l1Provider - ).chainId() - - //// L2 - const l2 = { - router: ( - await l1TokenBridgeCreator.getCanonicalL2RouterAddress(chainId) - ).toLowerCase(), - standardGateway: ( - await l1TokenBridgeCreator.getCanonicalL2StandardGatewayAddress(chainId) - ).toLowerCase(), - customGateway: ( - await l1TokenBridgeCreator.getCanonicalL2CustomGatewayAddress(chainId) - ).toLowerCase(), - wethGateway: (usingFeeToken - ? ethers.constants.AddressZero - : await l1TokenBridgeCreator.getCanonicalL2WethGatewayAddress(chainId) - ).toLowerCase(), - weth: (usingFeeToken - ? ethers.constants.AddressZero - : await l1TokenBridgeCreator.getCanonicalL2WethAddress(chainId) - ).toLowerCase(), - upgradeExecutor: ( - await l1TokenBridgeCreator.getCanonicalL2UpgradeExecutorAddress(chainId) - ).toLowerCase(), - } - - return { - l1, - l2, - } + return { rollupAddresses, l1Deployment, l2Deployment } } async function _getProxyAdmin( @@ -561,6 +667,19 @@ async function _getProxyAdmin( ).toLowerCase() } +async function _getLogicAddress( + contractAddress: string, + provider: Provider +): Promise { + return ( + await _getAddressAtStorageSlot( + contractAddress, + provider, + '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' + ) + ).toLowerCase() +} + async function _getOwner( contractAddress: string, provider: Provider @@ -592,22 +711,78 @@ async function _getAddressAtStorageSlot( return ethers.utils.getAddress(formatAddress) } -interface L1 { +async function _getRollupOwnerFromLogs( + provider: JsonRpcProvider, + l1TokenBridgeCreator: L1AtomicTokenBridgeCreator, + inboxAddress: string +): Promise { + const filter: Filter = { + address: l1TokenBridgeCreator.address, + topics: [ + ethers.utils.id( + 'OrbitTokenBridgeCreated(address,address,(address,address,address,address,address),(address,address,address,address,address,address,address,address,address),address,address)' + ), + ethers.utils.hexZeroPad(inboxAddress, 32), + ], + } + + // Fetch the logs + const logs = await provider.getLogs({ + ...filter, + fromBlock: '0x1', + toBlock: 'latest', + }) + if (logs.length === 0) { + throw new Error( + `Couldn't find any OrbitTokenBridgeCreated events for inbox ${inboxAddress}` + ) + } + + const logData = l1TokenBridgeCreator.interface.parseLog(logs[logs.length - 1]) + return logData.args.owner +} + +/** + * Return if contracts is initialized or not. Applicable for contracts which use OpenZeppelin Initializable pattern, + * so state of initialization is stored as uint8 in storage slot 0, offset 0. + */ +async function _isInitialized( + contractAddress: string, + provider: Provider +): Promise { + const storageSlot = 0 + const storageValue = await provider.getStorageAt(contractAddress, storageSlot) + const bigNumberValue = ethers.BigNumber.from(storageValue) + + // Ethereum storage slots are 32 bytes and a uint8 is 1 byte, we mask the lower 8 bits to convert it to uint8. + const maskedValue = bigNumberValue.and(255) + return maskedValue.toNumber() == 1 +} + +interface RollupAddresses { + rollup: string inbox: string rollupOwner: string + proxyAdmin: string + upgradeExecutor: string + multicall: string +} + +interface L1DeploymentAddresses { router: string standardGateway: string customGateway: string wethGateway: string - proxyAdmin: string - upgradeExecutor: string + weth: string } - -interface L2 { +interface L2DeploymentAddresses { router: string standardGateway: string customGateway: string wethGateway: string weth: string + proxyAdmin: string + beaconProxyFactory: string upgradeExecutor: string + multicall: string } diff --git a/test-foundry/AtomicTokenBridgeFactory.t.sol b/test-foundry/AtomicTokenBridgeFactory.t.sol new file mode 100644 index 0000000000..f0662ccb63 --- /dev/null +++ b/test-foundry/AtomicTokenBridgeFactory.t.sol @@ -0,0 +1,266 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; + +import "../contracts/tokenbridge/ethereum/L1AtomicTokenBridgeCreator.sol"; +import "../contracts/tokenbridge/arbitrum/L2AtomicTokenBridgeFactory.sol"; +import "../contracts/tokenbridge/libraries/AddressAliasHelper.sol"; + +import {L1TokenBridgeRetryableSender} from + "../contracts/tokenbridge/ethereum/L1TokenBridgeRetryableSender.sol"; +import {TestWETH9} from "../contracts/tokenbridge/test/TestWETH9.sol"; +import {Multicall2} from "../contracts/rpc-utils/MulticallV2.sol"; + +import {TransparentUpgradeableProxy} from + "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +// // Check that the rollupOwner account has EXECUTOR role +// // on the upgrade executor which is the owner of the rollup +// address upgradeExecutor = IInbox(inbox).bridge().rollup().owner(); +// if ( +// !IAccessControlUpgradeable(upgradeExecutor).hasRole( +// UpgradeExecutor(upgradeExecutor).EXECUTOR_ROLE(), rollupOwner +// ) +// ) { +// revert L1AtomicTokenBridgeCreator_RollupOwnershipMisconfig(); +// } + +/// @dev This inbox mock is used to bypass sanity checks in the L1AtomicTokenBridgeCreator +contract MockInbox is Test { + bytes32 public constant EXECUTOR_ROLE = keccak256("EXECUTOR_ROLE"); + address public constant nativeToken = address(0); + uint256 public immutable chainId; + uint256 public mode; + + constructor(uint256 _mode) { + chainId = block.chainid; + mode = _mode; + } + + function setMode(uint256 _mode) external { + mode = _mode; + } + + function bridge() external view returns (address) { + return address(this); + } + + function rollup() external view returns (address) { + return address(this); + } + + function owner() external view returns (address) { + return address(this); + } + + function hasRole(bytes32, address) external view returns (bool) { + return true; + } + + function getProxyAdmin() external view returns (address) { + return address(this); + } + + function calculateRetryableSubmissionFee(uint256, uint256) external view returns (uint256) { + return 0; + } + + function createRetryableTicket( + address to, + uint256 l2CallValue, + uint256, + address, + address, + uint256, + uint256 maxFeePerGas, + bytes memory data + ) external payable returns (uint256) { + if (mode == 1) { + // mode 1: frontrun the call + if (to != address(0)) { + (bool success,) = to.call{value: l2CallValue}(data); + if (!success) { + revert("frontrun failed"); + } + } + } + vm.startPrank(AddressAliasHelper.applyL1ToL2Alias(msg.sender)); + if (to == address(0)) { + if (mode == 2) { + // mode 2: fail the deployment + vm.stopPrank(); + return 0; + } + address addr; + assembly { + addr := create(0, add(data, 0x20), mload(data)) + if iszero(extcodesize(addr)) { revert(0, 0) } + } + } else { + (bool success,) = to.call{value: l2CallValue}(data); + if (!success) { + revert(); + } + } + vm.stopPrank(); + } +} + +contract AtomicTokenBridgeCreatorTest is Test { + L1AtomicTokenBridgeCreator.L1Templates public l1Templates; + + address public l2TokenBridgeFactoryTemplate; + address public l2RouterTemplate; + address public l2StandardGatewayTemplate; + address public l2CustomGatewayTemplate; + address public l2WethGatewayTemplate; + address public l2WethTemplate; + address public l2MulticallTemplate; + + address public l1Weth; + address public l1MultiCall; + + L1AtomicTokenBridgeCreator public factory; + + uint256 public constant MAX_DEPLOYMENT_GAS = 30 * 1024 * 16; // 30 bytes, 16 gas per byte + address public constant PROXY_ADMIN = address(111); + + receive() external payable {} + + function setUp() public { + l1Templates = L1AtomicTokenBridgeCreator.L1Templates( + L1GatewayRouter(address(new L1GatewayRouter())), + L1ERC20Gateway(address(new L1ERC20Gateway())), + L1CustomGateway(address(new L1CustomGateway())), + L1WethGateway(payable(new L1WethGateway())), + L1OrbitGatewayRouter(address(new L1OrbitGatewayRouter())), + L1OrbitERC20Gateway(address(new L1OrbitERC20Gateway())), + L1OrbitCustomGateway(address(new L1OrbitCustomGateway())), + IUpgradeExecutor(address(new UpgradeExecutor())) + ); + l2TokenBridgeFactoryTemplate = address(new L2AtomicTokenBridgeFactory()); + l2RouterTemplate = address(new L2GatewayRouter()); + l2StandardGatewayTemplate = address(new L2ERC20Gateway()); + l2CustomGatewayTemplate = address(new L2CustomGateway()); + l2WethGatewayTemplate = address(new L2WethGateway()); + l2WethTemplate = address(new aeWETH()); + l2MulticallTemplate = address(new ArbMulticall2()); + + l1Weth = address(new TestWETH9("wethl1", "wl1")); + l1MultiCall = address(new Multicall2()); + + L1TokenBridgeRetryableSender sender = new L1TokenBridgeRetryableSender(); + address factorylogic = address(new L1AtomicTokenBridgeCreator()); + factory = L1AtomicTokenBridgeCreator( + address(new TransparentUpgradeableProxy(factorylogic, PROXY_ADMIN, "")) + ); + factory.initialize(sender); + factory.setTemplates( + l1Templates, + l2TokenBridgeFactoryTemplate, + l2RouterTemplate, + l2StandardGatewayTemplate, + l2CustomGatewayTemplate, + l2WethGatewayTemplate, + l2WethTemplate, + l2MulticallTemplate, + l1Weth, + l1MultiCall, + MAX_DEPLOYMENT_GAS + ); + } + + function testDeployment() public { + MockInbox inbox = new MockInbox(0); + _testDeployment(address(inbox)); + } + + function testDeploymentFrontrun() public { + MockInbox inbox = new MockInbox(1); + _testDeployment(address(inbox)); + } + + function testDeploymentFailDeploy() public { + // although the deployment must have enough gas to deploy it can still fail due to gas price + // in such case the 2 retryable can be executed out-of-order + // Mode 2 simulate this case where the deployment fails and the call is executed first + MockInbox inbox = new MockInbox(2); + factory.createTokenBridge({ + inbox: address(inbox), + rollupOwner: address(this), + maxGasForContracts: 0, + gasPriceBid: 0 + }); + + // L2 Factory is not deployed in this case + address l2factory = factory.canonicalL2FactoryAddress(); + assertEq(l2factory, 0x20011A455c9eBBeD73CA307539D3e9Baff600fBD); + assertEq(l2factory.code.length, 0); + + inbox.setMode(0); // set back to normal mode + _testDeployment(address(inbox)); + } + + function _testDeployment(address inbox) internal { + factory.createTokenBridge({ + inbox: address(inbox), + rollupOwner: address(this), + maxGasForContracts: 0, + gasPriceBid: 0 + }); + { + address l2factory = factory.canonicalL2FactoryAddress(); + assertEq(l2factory, 0x20011A455c9eBBeD73CA307539D3e9Baff600fBD); + assertTrue(l2factory.code.length > 0); + } + + { + (address l1r, address l1sgw, address l1cgw, address l1wgw, address l1w) = + factory.inboxToL1Deployment(address(inbox)); + assertEq(l1r, 0xcB37BCa7042A10FfA75Ff95Ad8B361A13bbAA63A, "l1r"); + assertTrue(l1r.code.length > 0, "l1r code"); + assertEq(l1sgw, 0x013b54d88f76fb9D05b8382747beb1B4Df313507, "l1sgw"); + assertTrue(l1sgw.code.length > 0, "l1sgw code"); + assertEq(l1cgw, 0xf8663294698E0623de82B9791906454A2036575F, "l1cgw"); + assertTrue(l1cgw.code.length > 0, "l1cgw code"); + assertEq(l1wgw, 0x79eF26bE05C5643D5AdC81B8c7e49b0898A74428, "l1wgw"); + assertTrue(l1wgw.code.length > 0, "l1wgw code"); + assertEq(l1w, 0x96d3F6c20EEd2697647F543fE6C08bC2Fbf39758, "l1w"); + assertTrue(l1w.code.length > 0, "l1w code"); + } + { + ( + address l2r, + address l2sgw, + address l2cgw, + address l2wgw, + address l2w, + address l2pa, + address l2bpf, + address l2ue, + address l2mc + ) = factory.inboxToL2Deployment(address(inbox)); + + assertEq(l2r, 0xdB4050B663976d45E810B7C0E3B8B25564bD620d, "l2r"); + assertTrue(l2r.code.length > 0, "l2r code"); + assertEq(l2sgw, 0x25F753b06E1e092292e6773E119D00BEe5A1b8D4, "l2sgw"); + assertTrue(l2sgw.code.length > 0, "l2sgw code"); + assertEq(l2cgw, 0x4Ca25428D90D0813EC134b5160eb6301909B4A9B, "l2cgw"); + assertTrue(l2cgw.code.length > 0, "l2cgw code"); + assertEq(l2wgw, 0x29B1Fa62Af163E550Cb4173BE58787fa2d6456fF, "l2wgw"); + assertTrue(l2wgw.code.length > 0, "l2wgw code"); + assertEq(l2w, 0x7C9c18AE0EeA13600496D1222E8Ec22738b29C61, "l2w"); + assertTrue(l2w.code.length > 0, "l2w code"); + assertEq(l2pa, 0xf789F48Bc2c9ee6E98E564E6383B394ba6F9378c, "l2pa"); + assertTrue(l2pa.code.length > 0, "l2pa code"); + assertEq(l2bpf, 0x9446B15B1128aD326Ccf310a68F2FFB652D31934, "l2bpf"); + assertTrue(l2bpf.code.length > 0, "l2bpf code"); + assertEq(l2ue, 0xC85c71251E9354Cd6a8992BC02d968B04F4b55e6, "l2ue"); + assertTrue(l2ue.code.length > 0, "l2ue code"); + assertEq(l2mc, 0x4572E7101b8A6d889680dA7CC35D6076e651e9fC, "l2mc"); + assertTrue(l2mc.code.length > 0, "l2mc code"); + } + } +} diff --git a/test-foundry/L1AtomicTokenBridgeCreator.t.sol b/test-foundry/L1AtomicTokenBridgeCreator.t.sol new file mode 100644 index 0000000000..95ddaf2e1a --- /dev/null +++ b/test-foundry/L1AtomicTokenBridgeCreator.t.sol @@ -0,0 +1,864 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import { + L1AtomicTokenBridgeCreator, + L1DeploymentAddresses, + L2DeploymentAddresses, + TransparentUpgradeableProxy, + ProxyAdmin, + ClonableBeaconProxy, + BeaconProxyFactory +} from "contracts/tokenbridge/ethereum/L1AtomicTokenBridgeCreator.sol"; +import {L1TokenBridgeRetryableSender} from + "contracts/tokenbridge/ethereum/L1TokenBridgeRetryableSender.sol"; +import {TestUtil} from "./util/TestUtil.sol"; +import {AddressAliasHelper} from "contracts/tokenbridge/libraries/AddressAliasHelper.sol"; +import {L1GatewayRouter} from "contracts/tokenbridge/ethereum/gateway/L1GatewayRouter.sol"; +import {L1ERC20Gateway} from "contracts/tokenbridge/ethereum/gateway/L1ERC20Gateway.sol"; +import {L1CustomGateway} from "contracts/tokenbridge/ethereum/gateway/L1CustomGateway.sol"; +import {L1WethGateway} from "contracts/tokenbridge/ethereum/gateway/L1WethGateway.sol"; +import {L1OrbitGatewayRouter} from "contracts/tokenbridge/ethereum/gateway/L1OrbitGatewayRouter.sol"; +import {L1OrbitERC20Gateway} from "contracts/tokenbridge/ethereum/gateway/L1OrbitERC20Gateway.sol"; +import {L1OrbitCustomGateway} from "contracts/tokenbridge/ethereum/gateway/L1OrbitCustomGateway.sol"; +import { + IUpgradeExecutor, + UpgradeExecutor +} from "@offchainlabs/upgrade-executor/src/UpgradeExecutor.sol"; +import {Inbox, IInboxBase} from "lib/nitro-contracts/src/bridge/Inbox.sol"; +import {ERC20Inbox} from "lib/nitro-contracts/src/bridge/ERC20Inbox.sol"; +import {IOutbox} from "lib/nitro-contracts/src/bridge/IOutbox.sol"; +import {Bridge, IBridge, IOwnable} from "lib/nitro-contracts/src/bridge/Bridge.sol"; +import {ERC20Bridge} from "lib/nitro-contracts/src/bridge/ERC20Bridge.sol"; +import { + RollupProxy, + IRollupUser, + IOutbox, + IRollupEventInbox, + IChallengeManager +} from "lib/nitro-contracts/src/rollup/RollupProxy.sol"; +import {RollupAdminLogic} from "lib/nitro-contracts/src/rollup/RollupAdminLogic.sol"; +import {RollupUserLogic} from "lib/nitro-contracts/src/rollup/RollupUserLogic.sol"; +import {Config, ContractDependencies} from "lib/nitro-contracts/src/rollup/Config.sol"; +import {ISequencerInbox} from "lib/nitro-contracts/src/bridge/ISequencerInbox.sol"; +import {ERC20PresetMinterPauser} from + "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; + +contract L1AtomicTokenBridgeCreatorTest is Test { + L1AtomicTokenBridgeCreator public l1Creator; + address public deployer = makeAddr("deployer"); + + function setUp() public { + l1Creator = L1AtomicTokenBridgeCreator( + TestUtil.deployProxy(address(new L1AtomicTokenBridgeCreator())) + ); + L1TokenBridgeRetryableSender sender = L1TokenBridgeRetryableSender( + TestUtil.deployProxy(address(new L1TokenBridgeRetryableSender())) + ); + + vm.deal(deployer, 10 ether); + vm.prank(deployer); + l1Creator.initialize(sender); + } + + /* solhint-disable func-name-mixedcase */ + function test_initialize() public { + L1AtomicTokenBridgeCreator _creator = L1AtomicTokenBridgeCreator( + TestUtil.deployProxy(address(new L1AtomicTokenBridgeCreator())) + ); + L1TokenBridgeRetryableSender _sender = L1TokenBridgeRetryableSender( + TestUtil.deployProxy(address(new L1TokenBridgeRetryableSender())) + ); + + vm.prank(deployer); + _creator.initialize(_sender); + + assertEq(_creator.owner(), deployer, "Wrong owner"); + assertEq(address(_creator.retryableSender()), address(_sender), "Wrong sender"); + assertEq(uint256(vm.load(address(_sender), 0)), 1, "Wrong init state"); + + address exepectedL2Factory = address( + uint160( + uint256( + keccak256( + abi.encodePacked( + bytes1(0xd6), + bytes1(0x94), + AddressAliasHelper.applyL1ToL2Alias(address(_creator)), + bytes1(0x80) + ) + ) + ) + ) + ); + assertEq( + address(_creator.canonicalL2FactoryAddress()), + exepectedL2Factory, + "Wrong canonicalL2FactoryAddress" + ); + } + + function test_initialize_revert_AlreadyInit() public { + L1AtomicTokenBridgeCreator _creator = L1AtomicTokenBridgeCreator( + TestUtil.deployProxy(address(new L1AtomicTokenBridgeCreator())) + ); + L1TokenBridgeRetryableSender _sender = new L1TokenBridgeRetryableSender(); + _creator.initialize(_sender); + + vm.expectRevert("Initializable: contract is already initialized"); + _creator.initialize(_sender); + } + + function test_initialize_revert_CantInitLogic() public { + L1AtomicTokenBridgeCreator _creator = new L1AtomicTokenBridgeCreator(); + + vm.expectRevert("Initializable: contract is already initialized"); + _creator.initialize(L1TokenBridgeRetryableSender(address(100))); + } + + function test_createTokenBridge_checkL1Router() public { + // prepare + _setTemplates(); + (RollupProxy rollup, Inbox inbox, ProxyAdmin pa, UpgradeExecutor upgExecutor) = + _createRollup(); + _createTokenBridge(rollup, inbox, upgExecutor); + + /// check state + (address l1RouterAddress, address standardGatewayAddress,,,) = + l1Creator.inboxToL1Deployment(address(inbox)); + + (L1GatewayRouter routerTemplate,,,,,,,) = l1Creator.l1Templates(); + + address expectedL1RouterAddress = Create2.computeAddress( + keccak256(abi.encodePacked(bytes("L1R"), address(inbox))), + keccak256( + abi.encodePacked( + type(TransparentUpgradeableProxy).creationCode, + abi.encode(address(routerTemplate), pa, bytes("")) + ) + ), + address(l1Creator) + ); + assertEq(l1RouterAddress, expectedL1RouterAddress, "Wrong l1Router address"); + assertTrue(l1RouterAddress.code.length > 0, "Wrong l1Router code"); + + L1GatewayRouter l1Router = L1GatewayRouter(l1RouterAddress); + assertEq(l1Router.owner(), address(upgExecutor), "Wrong l1Router owner"); + assertEq(l1Router.defaultGateway(), standardGatewayAddress, "Wrong l1Router defaultGateway"); + assertEq(l1Router.whitelist(), address(0), "Wrong l1Router whitelist"); + + (address l2Router,,,,,,,,) = l1Creator.inboxToL2Deployment(address(inbox)); + assertEq(l1Router.counterpartGateway(), l2Router, "Wrong l1Router counterpartGateway"); + assertEq(l1Router.inbox(), address(inbox), "Wrong l1Router inbox"); + } + + function test_createTokenBridge_checkL1StandardGateway() public { + // prepare + _setTemplates(); + (RollupProxy rollup, Inbox inbox, ProxyAdmin pa, UpgradeExecutor upgExecutor) = + _createRollup(); + _createTokenBridge(rollup, inbox, upgExecutor); + + /// check state + (address l1RouterAddress, address l1StandardGatewayAddress,,,) = + l1Creator.inboxToL1Deployment(address(inbox)); + + (, L1ERC20Gateway standardGatewayTemplate,,,,,,) = l1Creator.l1Templates(); + + address expectedL1StandardGatewayAddress = Create2.computeAddress( + keccak256(abi.encodePacked(bytes("L1SGW"), address(inbox))), + keccak256( + abi.encodePacked( + type(TransparentUpgradeableProxy).creationCode, + abi.encode(address(standardGatewayTemplate), pa, bytes("")) + ) + ), + address(l1Creator) + ); + assertEq( + l1StandardGatewayAddress, + expectedL1StandardGatewayAddress, + "Wrong l1StandardGateway address" + ); + assertTrue(l1StandardGatewayAddress.code.length > 0, "Wrong l1StandardGateway code"); + + L1ERC20Gateway l1StandardGateway = L1ERC20Gateway(l1StandardGatewayAddress); + (, address l2StandardGateway,,,,,,,) = l1Creator.inboxToL2Deployment(address(inbox)); + assertEq( + l1StandardGateway.counterpartGateway(), + l2StandardGateway, + "Wrong l1StandardGateway counterpartGateway" + ); + assertEq(l1StandardGateway.router(), l1RouterAddress, "Wrong l1StandardGateway router"); + assertEq(l1StandardGateway.inbox(), address(inbox), "Wrong l1StandardGateway inbox"); + assertEq( + l1StandardGateway.cloneableProxyHash(), + keccak256(type(ClonableBeaconProxy).creationCode), + "Wrong l1StandardGateway cloneableProxyHash" + ); + + address expectedL2BeaconProxyFactoryAddress = Create2.computeAddress( + keccak256( + abi.encodePacked( + bytes("L2BPF"), + uint256(2000), + AddressAliasHelper.applyL1ToL2Alias(address(l1Creator.retryableSender())) + ) + ), + keccak256(type(BeaconProxyFactory).creationCode), + l1Creator.canonicalL2FactoryAddress() + ); + assertEq( + l1StandardGateway.l2BeaconProxyFactory(), + expectedL2BeaconProxyFactoryAddress, + "Wrong l1StandardGateway l2BeaconProxyFactory" + ); + } + + function test_createTokenBridge_checkL1CustomGateway() public { + // prepare + _setTemplates(); + (RollupProxy rollup, Inbox inbox, ProxyAdmin pa, UpgradeExecutor upgExecutor) = + _createRollup(); + _createTokenBridge(rollup, inbox, upgExecutor); + + /// check state + (address l1RouterAddress,, address l1CustomGatewayAddress,,) = + l1Creator.inboxToL1Deployment(address(inbox)); + + (,, L1CustomGateway customGatewayTemplate,,,,,) = l1Creator.l1Templates(); + + address expectedL1CustomGatewayAddress = Create2.computeAddress( + keccak256(abi.encodePacked(bytes("L1CGW"), address(inbox))), + keccak256( + abi.encodePacked( + type(TransparentUpgradeableProxy).creationCode, + abi.encode(address(customGatewayTemplate), pa, bytes("")) + ) + ), + address(l1Creator) + ); + assertEq( + l1CustomGatewayAddress, + expectedL1CustomGatewayAddress, + "Wrong l1StandardGateway address" + ); + assertTrue(l1CustomGatewayAddress.code.length > 0, "Wrong l1CustomGatewayAddress code"); + + L1CustomGateway l1CustomGateway = L1CustomGateway(l1CustomGatewayAddress); + (,, address l2CustomGateway,,,,,,) = l1Creator.inboxToL2Deployment(address(inbox)); + assertEq( + l1CustomGateway.counterpartGateway(), + l2CustomGateway, + "Wrong l1CustomGateway counterpartGateway" + ); + assertEq(l1CustomGateway.router(), l1RouterAddress, "Wrong l1CustomGateway router"); + assertEq(l1CustomGateway.inbox(), address(inbox), "Wrong l1CustomGateway inbox"); + assertEq(l1CustomGateway.owner(), address(upgExecutor), "Wrong l1CustomGateway owner"); + } + + function test_createTokenBridge_checkL1WethGateway() public { + // prepare + _setTemplates(); + (RollupProxy rollup, Inbox inbox, ProxyAdmin pa, UpgradeExecutor upgExecutor) = + _createRollup(); + _createTokenBridge(rollup, inbox, upgExecutor); + + /// check state + (address l1RouterAddress,,, address l1WethGatewayAddress,) = + l1Creator.inboxToL1Deployment(address(inbox)); + + (,,, L1WethGateway wethGatewayTemplate,,,,) = l1Creator.l1Templates(); + + address expectedL1WethGatewayAddress = Create2.computeAddress( + keccak256(abi.encodePacked(bytes("L1WGW"), address(inbox))), + keccak256( + abi.encodePacked( + type(TransparentUpgradeableProxy).creationCode, + abi.encode(address(wethGatewayTemplate), pa, bytes("")) + ) + ), + address(l1Creator) + ); + assertEq(l1WethGatewayAddress, expectedL1WethGatewayAddress, "Wrong l1WethGatewayAddresss"); + assertTrue(l1WethGatewayAddress.code.length > 0, "Wrong l1WethGatewayAddress code"); + + L1WethGateway l1WethGateway = L1WethGateway(payable(l1WethGatewayAddress)); + (,,, address l2WethGateway, address l2Weth,,,,) = + l1Creator.inboxToL2Deployment(address(inbox)); + assertEq( + l1WethGateway.counterpartGateway(), + l2WethGateway, + "Wrong l1WethGateway counterpartGateway" + ); + assertEq(l1WethGateway.router(), l1RouterAddress, "Wrong l1WethGateway router"); + assertEq(l1WethGateway.inbox(), address(inbox), "Wrong l1WethGateway inbox"); + assertEq(l1WethGateway.l1Weth(), l1Creator.l1Weth(), "Wrong l1WethGateway l1Weth"); + assertEq(l1WethGateway.l2Weth(), l2Weth, "Wrong l1WethGateway l2Weth"); + } + + function test_createTokenBridge_DeployerIsRefunded() public { + // prepare + _setTemplates(); + (RollupProxy rollup, Inbox inbox, ProxyAdmin pa, UpgradeExecutor upgExecutor) = + _createRollup(); + + uint256 deployerBalanceBefore = deployer.balance; + + _createTokenBridge(rollup, inbox, upgExecutor); + + uint256 deployerBalanceAfter = deployer.balance; + + assertGt(deployerBalanceAfter, deployerBalanceBefore - 1 ether, "Refund not received"); + } + + function test_createTokenBridge_ERC20Chain() public { + // prepare + _setTemplates(); + (RollupProxy rollup, ERC20Inbox inbox,, UpgradeExecutor upgExecutor, ERC20 nativeToken) = + _createERC20Rollup(); + + { + // mock owner() => upgExecutor + vm.mockCall( + address(rollup), + abi.encodeWithSignature("owner()"), + abi.encode(address(upgExecutor)) + ); + + // mock rollupOwner is executor on upgExecutor + vm.mockCall( + address(upgExecutor), + abi.encodeWithSignature( + "hasRole(bytes32,address)", upgExecutor.EXECUTOR_ROLE(), deployer + ), + abi.encode(true) + ); + + // mock chain id + uint256 mockChainId = 2000; + vm.mockCall( + address(rollup), abi.encodeWithSignature("chainId()"), abi.encode(mockChainId) + ); + } + + /// do it + vm.deal(deployer, 1 ether); + vm.startPrank(deployer); + nativeToken.approve(address(l1Creator), 10 ether); + l1Creator.createTokenBridge(address(inbox), deployer, 100, 200); + + /// check state + { + ( + address l1Router, + address l1StandardGateway, + address l1CustomGateway, + address l1WethGateway, + address l1Weth + ) = l1Creator.inboxToL1Deployment(address(inbox)); + assertTrue(l1Router != address(0), "Wrong l1Router"); + assertTrue(l1StandardGateway != address(0), "Wrong l1StandardGateway"); + assertTrue(l1CustomGateway != address(0), "Wrong l1CustomGateway"); + assertTrue(l1WethGateway == address(0), "Wrong l1WethGateway"); + assertTrue(l1Weth == address(0), "Wrong l1Weth"); + } + + { + ( + address l2Router, + address l2StandardGateway, + address l2CustomGateway, + address l2WethGateway, + address l2Weth, + address l2ProxyAdmin, + address l2BeaconProxyFactory, + address l2UpgradeExecutor, + address l2Multicall + ) = l1Creator.inboxToL2Deployment(address(inbox)); + assertTrue(l2Router != address(0), "Wrong l2Router"); + assertTrue(l2StandardGateway != address(0), "Wrong l2StandardGateway"); + assertTrue(l2CustomGateway != address(0), "Wrong l2CustomGateway"); + assertTrue(l2WethGateway == address(0), "Wrong l2WethGateway"); + assertTrue(l2Weth == address(0), "Wrong l2Weth"); + assertTrue(l2ProxyAdmin != address(0), "Wrong l2ProxyAdmin"); + assertTrue(l2BeaconProxyFactory != address(0), "Wrong l2BeaconProxyFactory"); + assertTrue(l2UpgradeExecutor != address(0), "Wrong l2UpgradeExecutor"); + assertTrue(l2Multicall != address(0), "Wrong l2Multicall"); + } + } + + function test_createTokenBridge_revert_TemplatesNotSet() public { + vm.expectRevert( + abi.encodeWithSelector( + L1AtomicTokenBridgeCreator.L1AtomicTokenBridgeCreator_TemplatesNotSet.selector + ) + ); + l1Creator.createTokenBridge(address(100), address(101), 100, 200); + } + + function test_createTokenBridge_revert_RollupOwnershipMisconfig() public { + // prepare + _setTemplates(); + (RollupProxy rollup, Inbox inbox,, UpgradeExecutor upgExecutor) = _createRollup(); + + // mock owner() => upgExecutor + vm.mockCall( + address(rollup), abi.encodeWithSignature("owner()"), abi.encode(address(upgExecutor)) + ); + + // expect revert when creating bridge + vm.expectRevert( + abi.encodeWithSelector( + L1AtomicTokenBridgeCreator + .L1AtomicTokenBridgeCreator_RollupOwnershipMisconfig + .selector + ) + ); + l1Creator.createTokenBridge(address(inbox), deployer, 100, 200); + } + + function test_getRouter_NonExistent() public { + assertEq(l1Creator.getRouter(makeAddr("non-existent")), address(0), "Should be empty"); + } + + function test_getRouter() public { + // prepare + _setTemplates(); + (RollupProxy rollup, Inbox inbox,, UpgradeExecutor upgExecutor) = _createRollup(); + + { + // mock owner() => upgExecutor + vm.mockCall( + address(rollup), + abi.encodeWithSignature("owner()"), + abi.encode(address(upgExecutor)) + ); + + // mock rollupOwner is executor on upgExecutor + vm.mockCall( + address(upgExecutor), + abi.encodeWithSignature( + "hasRole(bytes32,address)", upgExecutor.EXECUTOR_ROLE(), deployer + ), + abi.encode(true) + ); + + // mock chain id + uint256 mockChainId = 2000; + vm.mockCall( + address(rollup), abi.encodeWithSignature("chainId()"), abi.encode(mockChainId) + ); + } + + /// do it + vm.deal(deployer, 10 ether); + vm.prank(deployer); + l1Creator.createTokenBridge{value: 1 ether}(address(inbox), deployer, 100, 200); + + /// state check + (address expectedRouter,,,,) = l1Creator.inboxToL1Deployment(address(inbox)); + assertEq(l1Creator.getRouter(address(inbox)), expectedRouter, "Wrong router"); + } + + function test_setDeployment() public { + (RollupProxy rollup, Inbox inbox,, UpgradeExecutor upgExecutor) = _createRollup(); + + // mock owner() => upgExecutor + vm.mockCall( + address(rollup), abi.encodeWithSignature("owner()"), abi.encode(address(upgExecutor)) + ); + + L1DeploymentAddresses memory l1 = L1DeploymentAddresses( + makeAddr("l1Router"), + makeAddr("l1StandardGateway"), + makeAddr("l1CustomGateway"), + makeAddr("l1WethGateway"), + makeAddr("l1Weth") + ); + + L2DeploymentAddresses memory l2 = L2DeploymentAddresses( + makeAddr("l2Router"), + makeAddr("l2StandardGateway"), + makeAddr("l2CustomGateway"), + makeAddr("l2WethGateway"), + makeAddr("l2Weth"), + makeAddr("l2ProxyAdmin"), + makeAddr("l2BeaconProxyFactory"), + makeAddr("l2UpgradeExecutor"), + makeAddr("l2Multicall") + ); + + /// expect event + vm.expectEmit(true, true, true, true); + emit OrbitTokenBridgeDeploymentSet(address(inbox), l1, l2); + + /// do it + vm.prank(address(upgExecutor)); + l1Creator.setDeployment(address(inbox), l1, l2); + + /// check state + { + ( + address l1Router, + address l1StandardGateway, + address l1CustomGateway, + address l1WethGateway, + address l1Weth + ) = l1Creator.inboxToL1Deployment(address(inbox)); + assertEq(l1Router, l1.router, "Wrong l1Router"); + assertEq(l1StandardGateway, l1.standardGateway, "Wrong l1StandardGateway"); + assertEq(l1CustomGateway, l1.customGateway, "Wrong l1CustomGateway"); + assertEq(l1WethGateway, l1.wethGateway, "Wrong l1WethGateway"); + assertEq(l1Weth, l1.weth, "Wrong l1Weth"); + } + + { + ( + address l2Router, + address l2StandardGateway, + address l2CustomGateway, + address l2WethGateway, + address l2Weth, + address l2ProxyAdmin, + address l2BeaconProxyFactory, + address l2UpgradeExecutor, + address l2Multicall + ) = l1Creator.inboxToL2Deployment(address(inbox)); + assertEq(l2Router, l2.router, "Wrong l2Router"); + assertEq(l2StandardGateway, l2.standardGateway, "Wrong l2StandardGateway"); + assertEq(l2CustomGateway, l2.customGateway, "Wrong l2CustomGateway"); + assertEq(l2WethGateway, l2.wethGateway, "Wrong l2WethGateway"); + assertEq(l2Weth, l2.weth, "Wrong l2Weth"); + assertEq(l2ProxyAdmin, l2.proxyAdmin, "Wrong l2ProxyAdmin"); + assertEq(l2Weth, l2.weth, "Wrong l2Weth"); + assertEq(l2BeaconProxyFactory, l2.beaconProxyFactory, "Wrong l2BeaconProxyFactory"); + assertEq(l2UpgradeExecutor, l2.upgradeExecutor, "Wrong l2UpgradeExecutor"); + assertEq(l2Multicall, l2.multicall, "Wrong l2Multicall"); + } + } + + function test_setDeployment_revert_OnlyRollupOwner() public { + (RollupProxy rollup, Inbox inbox,, UpgradeExecutor upgExecutor) = _createRollup(); + + // mock owner() => upgExecutor + vm.mockCall( + address(rollup), abi.encodeWithSignature("owner()"), abi.encode(address(upgExecutor)) + ); + + L1DeploymentAddresses memory l1 = L1DeploymentAddresses( + makeAddr("l1Router"), + makeAddr("l1StandardGateway"), + makeAddr("l1CustomGateway"), + makeAddr("l1WethGateway"), + makeAddr("l1Weth") + ); + + L2DeploymentAddresses memory l2 = L2DeploymentAddresses( + makeAddr("l2Router"), + makeAddr("l2StandardGateway"), + makeAddr("l2CustomGateway"), + makeAddr("l2WethGateway"), + makeAddr("l2Weth"), + makeAddr("l2ProxyAdmin"), + makeAddr("l2BeaconProxyFactory"), + makeAddr("l2UpgradeExecutor"), + makeAddr("l2Multicall") + ); + + vm.expectRevert( + abi.encodeWithSelector( + L1AtomicTokenBridgeCreator.L1AtomicTokenBridgeCreator_OnlyRollupOwner.selector + ) + ); + l1Creator.setDeployment(address(inbox), l1, l2); + } + + function test_setTemplates() public { + L1AtomicTokenBridgeCreator.L1Templates memory _l1Templates = L1AtomicTokenBridgeCreator + .L1Templates( + new L1GatewayRouter(), + new L1ERC20Gateway(), + new L1CustomGateway(), + new L1WethGateway(), + new L1OrbitGatewayRouter(), + new L1OrbitERC20Gateway(), + new L1OrbitCustomGateway(), + new UpgradeExecutor() + ); + + vm.expectEmit(true, true, true, true); + emit OrbitTokenBridgeTemplatesUpdated(); + + vm.prank(deployer); + l1Creator.setTemplates( + _l1Templates, + makeAddr("_l2TokenBridgeFactoryTemplate"), + makeAddr("_l2RouterTemplate"), + makeAddr("_l2StandardGatewayTemplate"), + makeAddr("_l2CustomGatewayTemplate"), + makeAddr("_l2WethGatewayTemplate"), + makeAddr("_l2WethTemplate"), + makeAddr("_l2MulticallTemplate"), + makeAddr("_l1Weth"), + makeAddr("_l1Multicall"), + 1000 + ); + + ( + L1GatewayRouter router, + L1ERC20Gateway gw, + L1CustomGateway customGw, + L1WethGateway wGw, + L1OrbitGatewayRouter oRouter, + L1OrbitERC20Gateway oGw, + L1OrbitCustomGateway oCustomGw, + IUpgradeExecutor executor + ) = l1Creator.l1Templates(); + assertEq(address(router), address(_l1Templates.routerTemplate), "Wrong templates"); + assertEq(address(gw), address(_l1Templates.standardGatewayTemplate), "Wrong templates"); + assertEq(address(customGw), address(_l1Templates.customGatewayTemplate), "Wrong templates"); + assertEq(address(wGw), address(_l1Templates.wethGatewayTemplate), "Wrong templates"); + assertEq(address(oRouter), address(_l1Templates.feeTokenBasedRouterTemplate), "Wrong temp"); + assertEq( + address(oGw), address(_l1Templates.feeTokenBasedStandardGatewayTemplate), "Wrong gw" + ); + assertEq( + address(oCustomGw), address(_l1Templates.feeTokenBasedCustomGatewayTemplate), "Wrong gw" + ); + assertEq(address(executor), address(_l1Templates.upgradeExecutor), "Wrong executor"); + + assertEq( + l1Creator.l2TokenBridgeFactoryTemplate(), + makeAddr("_l2TokenBridgeFactoryTemplate"), + "Wrong ref" + ); + assertEq(l1Creator.l2RouterTemplate(), makeAddr("_l2RouterTemplate"), "Wrong ref"); + assertEq( + l1Creator.l2StandardGatewayTemplate(), + makeAddr("_l2StandardGatewayTemplate"), + "Wrong ref" + ); + assertEq( + l1Creator.l2CustomGatewayTemplate(), makeAddr("_l2CustomGatewayTemplate"), "Wrong ref" + ); + assertEq(l1Creator.l2WethGatewayTemplate(), makeAddr("_l2WethGatewayTemplate"), "Wrong ref"); + assertEq(l1Creator.l2WethTemplate(), makeAddr("_l2WethTemplate"), "Wrong ref"); + assertEq(l1Creator.l2MulticallTemplate(), makeAddr("_l2MulticallTemplate"), "Wrong ref"); + assertEq(l1Creator.l1Weth(), makeAddr("_l1Weth"), "Wrong ref"); + assertEq(l1Creator.l1Multicall(), makeAddr("_l1Multicall"), "Wrong ref"); + assertEq(l1Creator.gasLimitForL2FactoryDeployment(), 1000, "Wrong ref"); + } + + function test_setTemplates_revert_OnlyOwner() public { + L1AtomicTokenBridgeCreator.L1Templates memory _l1Templates = L1AtomicTokenBridgeCreator + .L1Templates( + new L1GatewayRouter(), + new L1ERC20Gateway(), + new L1CustomGateway(), + new L1WethGateway(), + new L1OrbitGatewayRouter(), + new L1OrbitERC20Gateway(), + new L1OrbitCustomGateway(), + new UpgradeExecutor() + ); + + vm.expectRevert("Ownable: caller is not the owner"); + l1Creator.setTemplates( + _l1Templates, + address(0), + address(0), + address(0), + address(0), + address(0), + address(0), + address(0), + address(0), + address(0), + 1000 + ); + } + + function test_setTemplates_revert_L2FactoryCannotBeChanged() public { + L1AtomicTokenBridgeCreator.L1Templates memory _l1Templates = L1AtomicTokenBridgeCreator + .L1Templates( + new L1GatewayRouter(), + new L1ERC20Gateway(), + new L1CustomGateway(), + new L1WethGateway(), + new L1OrbitGatewayRouter(), + new L1OrbitERC20Gateway(), + new L1OrbitCustomGateway(), + new UpgradeExecutor() + ); + + address originalL2Factory = makeAddr("originalL2Factory"); + + vm.prank(deployer); + l1Creator.setTemplates( + _l1Templates, + originalL2Factory, + address(0), + address(0), + address(0), + address(0), + address(0), + address(0), + address(0), + address(0), + 1000 + ); + + address newL2FactoryTemplate = makeAddr("newL2FactoryTemplate"); + vm.expectRevert( + abi.encodeWithSelector( + L1AtomicTokenBridgeCreator + .L1AtomicTokenBridgeCreator_L2FactoryCannotBeChanged + .selector + ) + ); + vm.prank(deployer); + l1Creator.setTemplates( + _l1Templates, + newL2FactoryTemplate, + address(0), + address(0), + address(0), + address(0), + address(0), + address(0), + address(0), + address(0), + 1000 + ); + } + + function _createRollup() + internal + returns (RollupProxy rollup, Inbox inbox, ProxyAdmin pa, UpgradeExecutor upgExecutor) + { + pa = new ProxyAdmin(); + rollup = new RollupProxy(); + upgExecutor = new UpgradeExecutor(); + + Bridge bridge = + Bridge(address(new TransparentUpgradeableProxy(address(new Bridge()), address(pa), ""))); + inbox = Inbox( + address(new TransparentUpgradeableProxy(address(new Inbox(104_857)), address(pa), "")) + ); + + inbox.initialize(IBridge(address(bridge)), ISequencerInbox(makeAddr("sequencerInbox"))); + bridge.initialize(IOwnable(address(rollup))); + + vm.mockCall(address(rollup), abi.encodeWithSignature("owner()"), abi.encode(address(this))); + bridge.setDelayedInbox(address(inbox), true); + } + + function _createERC20Rollup() + internal + returns ( + RollupProxy rollup, + ERC20Inbox inbox, + ProxyAdmin pa, + UpgradeExecutor upgExecutor, + ERC20 nativeToken + ) + { + pa = new ProxyAdmin(); + rollup = new RollupProxy(); + upgExecutor = new UpgradeExecutor(); + + ERC20Bridge bridge = ERC20Bridge( + address(new TransparentUpgradeableProxy(address(new ERC20Bridge()), address(pa), "")) + ); + inbox = ERC20Inbox( + address( + new TransparentUpgradeableProxy(address(new ERC20Inbox(104_857)), address(pa), "") + ) + ); + + nativeToken = ERC20(address(new ERC20PresetMinterPauser("X", "Y"))); + ERC20PresetMinterPauser(address(nativeToken)).mint(deployer, 10 ether); + + bridge.initialize(IOwnable(address(rollup)), address(nativeToken)); + inbox.initialize(IBridge(address(bridge)), ISequencerInbox(makeAddr("sequencerInbox"))); + + vm.mockCall(address(rollup), abi.encodeWithSignature("owner()"), abi.encode(address(this))); + bridge.setDelayedInbox(address(inbox), true); + } + + function _createTokenBridge(RollupProxy rollup, Inbox inbox, UpgradeExecutor upgExecutor) + internal + { + // mock owner() => upgExecutor + vm.mockCall( + address(rollup), abi.encodeWithSignature("owner()"), abi.encode(address(upgExecutor)) + ); + + // mock rollupOwner is executor on upgExecutor + vm.mockCall( + address(upgExecutor), + abi.encodeWithSignature( + "hasRole(bytes32,address)", upgExecutor.EXECUTOR_ROLE(), deployer + ), + abi.encode(true) + ); + + // mock chain id + uint256 mockChainId = 2000; + vm.mockCall(address(rollup), abi.encodeWithSignature("chainId()"), abi.encode(mockChainId)); + + // create token bridge + vm.prank(deployer); + l1Creator.createTokenBridge{value: 1 ether}(address(inbox), deployer, 100, 200); + } + + function _setTemplates() internal { + L1AtomicTokenBridgeCreator.L1Templates memory _l1Templates = L1AtomicTokenBridgeCreator + .L1Templates( + new L1GatewayRouter(), + new L1ERC20Gateway(), + new L1CustomGateway(), + new L1WethGateway(), + new L1OrbitGatewayRouter(), + new L1OrbitERC20Gateway(), + new L1OrbitCustomGateway(), + new UpgradeExecutor() + ); + + vm.prank(deployer); + l1Creator.setTemplates( + _l1Templates, + makeAddr("_l2TokenBridgeFactoryTemplate"), + makeAddr("_l2RouterTemplate"), + makeAddr("_l2StandardGatewayTemplate"), + makeAddr("_l2CustomGatewayTemplate"), + makeAddr("_l2WethGatewayTemplate"), + makeAddr("_l2WethTemplate"), + makeAddr("_l2MulticallTemplate"), + makeAddr("_l1Weth"), + makeAddr("_l1Multicall"), + 1000 + ); + } + + //// + // Event declarations + //// + event OrbitTokenBridgeCreated( + address indexed inbox, + address indexed owner, + L1DeploymentAddresses l1Deployment, + L2DeploymentAddresses l2Deployment, + address proxyAdmin, + address upgradeExecutor + ); + event OrbitTokenBridgeTemplatesUpdated(); + event OrbitTokenBridgeDeploymentSet( + address indexed inbox, L1DeploymentAddresses l1, L2DeploymentAddresses l2 + ); +} diff --git a/test-foundry/L1OrbitIntegration.t.sol b/test-foundry/L1OrbitIntegration.t.sol index 985856e8c1..c7c1badf48 100644 --- a/test-foundry/L1OrbitIntegration.t.sol +++ b/test-foundry/L1OrbitIntegration.t.sol @@ -50,7 +50,7 @@ contract IntegrationTest is Test { 1_000_000 ether, address(this) ); - inbox = ERC20Inbox(TestUtil.deployProxy(address(new ERC20Inbox()))); + inbox = ERC20Inbox(TestUtil.deployProxy(address(new ERC20Inbox(104857)))); bridge = ERC20Bridge(TestUtil.deployProxy(address(new ERC20Bridge()))); // init bridge and inbox diff --git a/test-foundry/L2AtomicTokenBridgeFactory.t.sol b/test-foundry/L2AtomicTokenBridgeFactory.t.sol new file mode 100644 index 0000000000..b02da64264 --- /dev/null +++ b/test-foundry/L2AtomicTokenBridgeFactory.t.sol @@ -0,0 +1,514 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import { + L2AtomicTokenBridgeFactory, + L2RuntimeCode, + ProxyAdmin, + BeaconProxyFactory, + StandardArbERC20, + UpgradeableBeacon, + aeWETH +} from "contracts/tokenbridge/arbitrum/L2AtomicTokenBridgeFactory.sol"; +import {L2GatewayRouter} from "contracts/tokenbridge/arbitrum/gateway/L2GatewayRouter.sol"; +import {L2ERC20Gateway} from "contracts/tokenbridge/arbitrum/gateway/L2ERC20Gateway.sol"; +import {L2CustomGateway} from "contracts/tokenbridge/arbitrum/gateway/L2CustomGateway.sol"; +import {L2WethGateway} from "contracts/tokenbridge/arbitrum/gateway/L2WethGateway.sol"; +import {CreationCodeHelper} from "contracts/tokenbridge/libraries/CreationCodeHelper.sol"; +import {UpgradeExecutor} from "@offchainlabs/upgrade-executor/src/UpgradeExecutor.sol"; +import {ArbMulticall2} from "contracts/rpc-utils/MulticallV2.sol"; +import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; +import {TransparentUpgradeableProxy} from + "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +import "forge-std/console.sol"; + +contract L2AtomicTokenBridgeFactoryTest is Test { + L2AtomicTokenBridgeFactory public l2Factory; + address public deployer = makeAddr("deployer"); + + address public router; + address public standardGateway; + address public customGateway; + address public wethGateway; + address public weth; + address public upgradeExecutor; + address public multicall; + + /// 'deployL2Contracts' inputs + address public l1Router = makeAddr("l1Router"); + address public l1StandardGateway = makeAddr("l1StandardGateway"); + address public l1CustomGateway = makeAddr("l1CustomGateway"); + address public l1WethGateway = makeAddr("l1WethGateway"); + address public l1Weth = makeAddr("l1Weth"); + address public rollupOwner = makeAddr("rollupOwner"); + address public aliasedL1UpgradeExecutor = makeAddr("aliasedL1UpgradeExecutor"); + + L2RuntimeCode public runtimeCode; + + address private constant ADDRESS_DEAD = address(0x000000000000000000000000000000000000dEaD); + + function setUp() public { + l2Factory = new L2AtomicTokenBridgeFactory(); + + // set templates + router = address(new L2GatewayRouter()); + standardGateway = address(new L2ERC20Gateway()); + customGateway = address(new L2CustomGateway()); + wethGateway = address(new L2WethGateway()); + weth = address(new aeWETH()); + upgradeExecutor = address(new UpgradeExecutor()); + multicall = address(new ArbMulticall2()); + + /// bytecode which is sent via retryable + runtimeCode = L2RuntimeCode( + router.code, + standardGateway.code, + customGateway.code, + wethGateway.code, + weth.code, + upgradeExecutor.code, + multicall.code + ); + } + + /* solhint-disable func-name-mixedcase */ + function test_deployL2Contracts_checkRouter() public { + _deployL2Contracts(); + + address expectedProxyAdminAddress = Create2.computeAddress( + keccak256(abi.encodePacked(bytes("L2PA"), block.chainid, address(this))), + keccak256(type(ProxyAdmin).creationCode), + address(l2Factory) + ); + + address expectedL2ERC20GwAddress = _computeAddress( + keccak256(abi.encodePacked(bytes("L2SGW"), block.chainid, address(this))), + expectedProxyAdminAddress + ); + + address expectedL2RouterAddress = _computeAddress( + keccak256(abi.encodePacked(bytes("L2R"), block.chainid, address(this))), + expectedProxyAdminAddress + ); + assertEq( + L2GatewayRouter(expectedL2RouterAddress).counterpartGateway(), + l1Router, + "Wrong l1Router" + ); + assertEq( + L2GatewayRouter(expectedL2RouterAddress).defaultGateway(), + expectedL2ERC20GwAddress, + "Wrong defaultGateway" + ); + + // logic + address expectedL2RouterLogicAddress = Create2.computeAddress( + keccak256(abi.encodePacked(bytes("L2R"), block.chainid, address(this))), + keccak256(CreationCodeHelper.getCreationCodeFor(runtimeCode.router)), + address(l2Factory) + ); + assertEq( + L2GatewayRouter(expectedL2RouterLogicAddress).counterpartGateway(), + ADDRESS_DEAD, + "Wrong l1Router" + ); + assertEq( + L2GatewayRouter(expectedL2RouterLogicAddress).defaultGateway(), + ADDRESS_DEAD, + "Wrong defaultGateway" + ); + } + + function test_deployL2Contracts_checkStandardGateway() public { + _deployL2Contracts(); + + // standard gateway + address expectedProxyAdminAddress = Create2.computeAddress( + keccak256(abi.encodePacked(bytes("L2PA"), block.chainid, address(this))), + keccak256(type(ProxyAdmin).creationCode), + address(l2Factory) + ); + + address expectedL2StandardGwAddress = _computeAddress( + keccak256(abi.encodePacked(bytes("L2SGW"), block.chainid, address(this))), + expectedProxyAdminAddress + ); + + address expectedL2RouterAddress = _computeAddress( + keccak256(abi.encodePacked(bytes("L2R"), block.chainid, address(this))), + expectedProxyAdminAddress + ); + + assertEq( + L2ERC20Gateway(expectedL2StandardGwAddress).counterpartGateway(), + l1StandardGateway, + "Wrong counterpartGateway" + ); + assertEq( + L2ERC20Gateway(expectedL2StandardGwAddress).router(), + expectedL2RouterAddress, + "Wrong router" + ); + + // beacon proxy stuff + address expectedL2BeaconProxyFactoryAddress = Create2.computeAddress( + keccak256(abi.encodePacked(bytes("L2BPF"), block.chainid, address(this))), + keccak256(type(BeaconProxyFactory).creationCode), + address(l2Factory) + ); + assertEq( + L2ERC20Gateway(expectedL2StandardGwAddress).beaconProxyFactory(), + expectedL2BeaconProxyFactoryAddress, + "Wrong beaconProxyFactory" + ); + address expectedStandardArbERC20Address = Create2.computeAddress( + keccak256(abi.encodePacked(bytes("L2BPF"), block.chainid, address(this))), + keccak256(type(StandardArbERC20).creationCode), + address(l2Factory) + ); + address expectedBeaconAddress = Create2.computeAddress( + keccak256(abi.encodePacked(bytes("L2BPF"), block.chainid, address(this))), + keccak256( + abi.encodePacked( + type(UpgradeableBeacon).creationCode, + abi.encode(expectedStandardArbERC20Address) + ) + ), + address(l2Factory) + ); + + assertEq( + UpgradeableBeacon(BeaconProxyFactory(expectedL2BeaconProxyFactoryAddress).beacon()) + .implementation(), + expectedStandardArbERC20Address, + "Wrong implementation" + ); + assertEq( + BeaconProxyFactory(expectedL2BeaconProxyFactoryAddress).beacon(), + expectedBeaconAddress, + "Wrong beacon" + ); + assertEq( + UpgradeableBeacon(expectedBeaconAddress).implementation(), + expectedStandardArbERC20Address, + "Wrong implementation" + ); + + address expectedL2UpgExecutorAddress = _computeAddress( + keccak256(abi.encodePacked(bytes("L2E"), block.chainid, address(this))), + expectedProxyAdminAddress + ); + assertEq( + UpgradeableBeacon(expectedBeaconAddress).owner(), + expectedL2UpgExecutorAddress, + "Wrong beacon owner" + ); + + // logic + address expectedL2StandardGwLogicAddress = Create2.computeAddress( + keccak256(abi.encodePacked(bytes("L2SGW"), block.chainid, address(this))), + keccak256(CreationCodeHelper.getCreationCodeFor(runtimeCode.standardGateway)), + address(l2Factory) + ); + assertEq( + L2ERC20Gateway(expectedL2StandardGwLogicAddress).counterpartGateway(), + ADDRESS_DEAD, + "Wrong counterpartGateway" + ); + assertEq( + L2ERC20Gateway(expectedL2StandardGwLogicAddress).router(), ADDRESS_DEAD, "Wrong router" + ); + assertEq( + L2ERC20Gateway(expectedL2StandardGwLogicAddress).beaconProxyFactory(), + ADDRESS_DEAD, + "Wrong beaconProxyFactory" + ); + } + + function test_deployL2Contracts_checkCustomGateway() public { + _deployL2Contracts(); + + // custom gateway + address expectedProxyAdminAddress = Create2.computeAddress( + keccak256(abi.encodePacked(bytes("L2PA"), block.chainid, address(this))), + keccak256(type(ProxyAdmin).creationCode), + address(l2Factory) + ); + + address expectedL2CustomGwAddress = _computeAddress( + keccak256(abi.encodePacked(bytes("L2CGW"), block.chainid, address(this))), + expectedProxyAdminAddress + ); + + address expectedL2RouterAddress = _computeAddress( + keccak256(abi.encodePacked(bytes("L2R"), block.chainid, address(this))), + expectedProxyAdminAddress + ); + + assertEq( + L2CustomGateway(expectedL2CustomGwAddress).counterpartGateway(), + l1CustomGateway, + "Wrong counterpartGateway" + ); + assertEq( + L2CustomGateway(expectedL2CustomGwAddress).router(), + expectedL2RouterAddress, + "Wrong router" + ); + + // logic + address expectedL2CustomGwLogicAddress = Create2.computeAddress( + keccak256(abi.encodePacked(bytes("L2CGW"), block.chainid, address(this))), + keccak256(CreationCodeHelper.getCreationCodeFor(runtimeCode.customGateway)), + address(l2Factory) + ); + assertEq( + L2CustomGateway(expectedL2CustomGwLogicAddress).counterpartGateway(), + ADDRESS_DEAD, + "Wrong counterpartGateway" + ); + assertEq( + L2CustomGateway(expectedL2CustomGwLogicAddress).router(), ADDRESS_DEAD, "Wrong router" + ); + } + + function test_deployL2Contracts_checkWethGateway() public { + _deployL2Contracts(); + + // weth gateway + address expectedProxyAdminAddress = Create2.computeAddress( + keccak256(abi.encodePacked(bytes("L2PA"), block.chainid, address(this))), + keccak256(type(ProxyAdmin).creationCode), + address(l2Factory) + ); + + address expectedL2WethGwAddress = _computeAddress( + keccak256(abi.encodePacked(bytes("L2WGW"), block.chainid, address(this))), + expectedProxyAdminAddress + ); + + address expectedL2Weth = _computeAddress( + keccak256(abi.encodePacked(bytes("L2W"), block.chainid, address(this))), + expectedProxyAdminAddress + ); + + address expectedL2RouterAddress = _computeAddress( + keccak256(abi.encodePacked(bytes("L2R"), block.chainid, address(this))), + expectedProxyAdminAddress + ); + + assertEq( + L2WethGateway(payable(expectedL2WethGwAddress)).counterpartGateway(), + l1WethGateway, + "Wrong counterpartGateway" + ); + assertEq( + L2WethGateway(payable(expectedL2WethGwAddress)).router(), + expectedL2RouterAddress, + "Wrong router" + ); + assertEq(L2WethGateway(payable(expectedL2WethGwAddress)).l1Weth(), l1Weth, "Wrong l1Weth"); + assertEq( + L2WethGateway(payable(expectedL2WethGwAddress)).l2Weth(), expectedL2Weth, "Wrong l2Weth" + ); + + // wethgateway logic + address expectedL2WethGwLogicAddress = Create2.computeAddress( + keccak256(abi.encodePacked(bytes("L2WGW"), block.chainid, address(this))), + keccak256(CreationCodeHelper.getCreationCodeFor(runtimeCode.wethGateway)), + address(l2Factory) + ); + assertEq( + L2WethGateway(payable(expectedL2WethGwLogicAddress)).counterpartGateway(), + ADDRESS_DEAD, + "Wrong counterpartGateway" + ); + assertEq( + L2WethGateway(payable(expectedL2WethGwLogicAddress)).router(), + ADDRESS_DEAD, + "Wrong router" + ); + assertEq( + L2WethGateway(payable(expectedL2WethGwLogicAddress)).l1Weth(), + ADDRESS_DEAD, + "Wrong l1Weth" + ); + assertEq( + L2WethGateway(payable(expectedL2WethGwLogicAddress)).l2Weth(), + ADDRESS_DEAD, + "Wrong l2Weth" + ); + + // weth + aeWETH l2Weth = aeWETH(payable(expectedL2Weth)); + assertEq(l2Weth.name(), "WETH", "Wrong name"); + assertEq(l2Weth.symbol(), "WETH", "Wrong symbol"); + assertEq(l2Weth.decimals(), 18, "Wrong decimals"); + assertEq(l2Weth.l2Gateway(), expectedL2WethGwAddress, "Wrong l2Gateway"); + assertEq(l2Weth.l1Address(), l1Weth, "Wrong l1Weth"); + + // weth logic + address expectedL2WethLogicAddress = Create2.computeAddress( + keccak256(abi.encodePacked(bytes("L2W"), block.chainid, address(this))), + keccak256(CreationCodeHelper.getCreationCodeFor(runtimeCode.aeWeth)), + address(l2Factory) + ); + aeWETH l2WethLogic = aeWETH(payable(expectedL2WethLogicAddress)); + assertEq(l2WethLogic.name(), "", "Wrong name"); + assertEq(l2WethLogic.symbol(), "", "Wrong symbol"); + assertEq(l2WethLogic.decimals(), 0, "Wrong decimals"); + assertEq(l2WethLogic.l2Gateway(), ADDRESS_DEAD, "Wrong l2Gateway"); + assertEq(l2WethLogic.l1Address(), ADDRESS_DEAD, "Wrong l1Weth"); + } + + function test_deployL2Contracts_checkUpgradeExecutor() public { + _deployL2Contracts(); + + // upgrade executor + address expectedProxyAdminAddress = Create2.computeAddress( + keccak256(abi.encodePacked(bytes("L2PA"), block.chainid, address(this))), + keccak256(type(ProxyAdmin).creationCode), + address(l2Factory) + ); + + address expectedL2UpgExecutorAddress = _computeAddress( + keccak256(abi.encodePacked(bytes("L2E"), block.chainid, address(this))), + expectedProxyAdminAddress + ); + + bytes32 executorRole = UpgradeExecutor(expectedL2UpgExecutorAddress).EXECUTOR_ROLE(); + bytes32 adminRole = UpgradeExecutor(expectedL2UpgExecutorAddress).ADMIN_ROLE(); + + assertEq( + UpgradeExecutor(expectedL2UpgExecutorAddress).hasRole( + executorRole, aliasedL1UpgradeExecutor + ), + true, + "Wrong executor role" + ); + assertEq( + UpgradeExecutor(expectedL2UpgExecutorAddress).hasRole(executorRole, rollupOwner), + true, + "Wrong executor role" + ); + assertEq( + UpgradeExecutor(expectedL2UpgExecutorAddress).hasRole( + adminRole, expectedL2UpgExecutorAddress + ), + true, + "Wrong admin role" + ); + + // logic + address expectedL2UpgExecutorLogicAddress = Create2.computeAddress( + keccak256(abi.encodePacked(bytes("L2E"), block.chainid, address(this))), + keccak256(CreationCodeHelper.getCreationCodeFor(runtimeCode.upgradeExecutor)), + address(l2Factory) + ); + assertEq( + UpgradeExecutor(expectedL2UpgExecutorLogicAddress).hasRole(adminRole, ADDRESS_DEAD), + true, + "Wrong admin role" + ); + } + + function test_deployL2Contracts_checkMulticall() public { + _deployL2Contracts(); + + address expectedMulticallAddress = Create2.computeAddress( + keccak256(abi.encodePacked(bytes("L2MC"), block.chainid, address(this))), + keccak256(type(ArbMulticall2).creationCode), + address(l2Factory) + ); + + assertGt(expectedMulticallAddress.code.length, uint256(0), "Multicall code is empty"); + } + + function test_deployL2Contracts_checkProxyAdmin() public { + _deployL2Contracts(); + + address expectedProxyAdminAddress = Create2.computeAddress( + keccak256(abi.encodePacked(bytes("L2PA"), block.chainid, address(this))), + keccak256(type(ProxyAdmin).creationCode), + address(l2Factory) + ); + + address expectedL2UpgExecutorAddress = _computeAddress( + keccak256(abi.encodePacked(bytes("L2E"), block.chainid, address(this))), + expectedProxyAdminAddress + ); + + assertGt(expectedProxyAdminAddress.code.length, uint256(0), "ProxyAdmin code is empty"); + assertEq( + ProxyAdmin(expectedProxyAdminAddress).owner(), + expectedL2UpgExecutorAddress, + "Wrong owner" + ); + } + + function test_deployL2Contracts_revert_AlreadyExists() public { + _deployL2Contracts(); + + vm.expectRevert( + abi.encodeWithSelector( + L2AtomicTokenBridgeFactory.L2AtomicTokenBridgeFactory_AlreadyExists.selector + ) + ); + l2Factory.deployL2Contracts( + runtimeCode, + l1Router, + l1StandardGateway, + l1CustomGateway, + l1WethGateway, + l1Weth, + makeAddr("l2StandardGatewayCanonicalAddress"), + rollupOwner, + aliasedL1UpgradeExecutor + ); + } + + function _deployL2Contracts() internal { + address l2StandardGatewayCanonicalAddress; + + /// expected L2 standard gateway address needs to be provided to 'deployL2Contracts' call as well + address expectedProxyAdminAddress = Create2.computeAddress( + keccak256(abi.encodePacked(bytes("L2PA"), block.chainid, address(this))), + keccak256(type(ProxyAdmin).creationCode), + address(l2Factory) + ); + address expectedL2ERC20GwAddress = _computeAddress( + keccak256(abi.encodePacked(bytes("L2SGW"), block.chainid, address(this))), + expectedProxyAdminAddress + ); + l2StandardGatewayCanonicalAddress = expectedL2ERC20GwAddress; + + /// do the call + l2Factory.deployL2Contracts( + runtimeCode, + l1Router, + l1StandardGateway, + l1CustomGateway, + l1WethGateway, + l1Weth, + l2StandardGatewayCanonicalAddress, + rollupOwner, + aliasedL1UpgradeExecutor + ); + } + + function _computeAddress(bytes32 salt, address proxyAdmin) internal view returns (address) { + return Create2.computeAddress( + salt, + keccak256( + abi.encodePacked( + type(TransparentUpgradeableProxy).creationCode, + abi.encode(l2Factory, proxyAdmin, bytes("")) + ) + ), + address(l2Factory) + ); + } +}