diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index aba7a24a69..86c07c44dd 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -94,3 +94,39 @@ 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 diff --git a/contracts/tokenbridge/arbitrum/L2AtomicTokenBridgeFactory.sol b/contracts/tokenbridge/arbitrum/L2AtomicTokenBridgeFactory.sol index 4b0c2b8703..8d563f5c60 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"; @@ -75,7 +76,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); @@ -93,14 +98,19 @@ contract L2AtomicTokenBridgeFactory { ); // Create UpgradeExecutor logic and upgrade to it. - // Note: UpgradeExecutor logic has initializer disabled so no need to call it. address upExecutorLogic = Create2.deploy( - 0, _getL2Salt(OrbitSalts.L2_EXECUTOR_LOGIC), _creationCodeFor(runtimeCode) + 0, + _getL2Salt(OrbitSalts.L2_EXECUTOR_LOGIC), + 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; @@ -122,8 +132,11 @@ contract L2AtomicTokenBridgeFactory { ); // 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_LOGIC), + CreationCodeHelper.getCreationCodeFor(runtimeCode) + ); ProxyAdmin(proxyAdmin).upgrade(ITransparentUpgradeableProxy(canonicalRouter), routerLogic); // init logic contract with dummy values. @@ -151,7 +164,9 @@ contract L2AtomicTokenBridgeFactory { // 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_LOGIC), + CreationCodeHelper.getCreationCodeFor(runtimeCode) ); ProxyAdmin(proxyAdmin).upgrade( ITransparentUpgradeableProxy(canonicalStdGateway), stdGatewayLogic @@ -196,7 +211,9 @@ contract L2AtomicTokenBridgeFactory { // 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_LOGIC), + CreationCodeHelper.getCreationCodeFor(runtimeCode) ); ProxyAdmin(proxyAdmin).upgrade( ITransparentUpgradeableProxy(canonicalCustomGateway), customGatewayLogicAddress @@ -224,7 +241,9 @@ contract L2AtomicTokenBridgeFactory { // Create L2WETH logic and upgrade address l2WethLogic = Create2.deploy( - 0, _getL2Salt(OrbitSalts.L2_WETH_LOGIC), _creationCodeFor(aeWethRuntimeCode) + 0, + _getL2Salt(OrbitSalts.L2_WETH_LOGIC), + CreationCodeHelper.getCreationCodeFor(aeWethRuntimeCode) ); ProxyAdmin(proxyAdmin).upgrade(ITransparentUpgradeableProxy(canonicalL2Weth), l2WethLogic); @@ -239,7 +258,7 @@ contract L2AtomicTokenBridgeFactory { address l2WethGatewayLogic = Create2.deploy( 0, _getL2Salt(OrbitSalts.L2_WETH_GATEWAY_LOGIC), - _creationCodeFor(wethGatewayRuntimeCode) + CreationCodeHelper.getCreationCodeFor(wethGatewayRuntimeCode) ); ProxyAdmin(proxyAdmin).upgrade( ITransparentUpgradeableProxy(canonicalL2WethGateway), l2WethGatewayLogic @@ -288,27 +307,6 @@ contract L2AtomicTokenBridgeFactory { ) ); } - - /** - * @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); - } } /** @@ -333,7 +331,6 @@ struct L2RuntimeCode { * Collection of salts used in CREATE2 deployment of L2 token bridge contracts. */ 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"); diff --git a/contracts/tokenbridge/ethereum/L1AtomicTokenBridgeCreator.sol b/contracts/tokenbridge/ethereum/L1AtomicTokenBridgeCreator.sol index 9a2fa9ddec..632df002fa 100644 --- a/contracts/tokenbridge/ethereum/L1AtomicTokenBridgeCreator.sol +++ b/contracts/tokenbridge/ethereum/L1AtomicTokenBridgeCreator.sol @@ -24,6 +24,7 @@ import { L2RuntimeCode, ProxyAdmin } from "../arbitrum/L2AtomicTokenBridgeFactory.sol"; +import {CreationCodeHelper} from "../libraries/CreationCodeHelper.sol"; import {BytesLib} from "../libraries/BytesLib.sol"; import { IUpgradeExecutor, @@ -57,6 +58,7 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { error L1AtomicTokenBridgeCreator_TemplatesNotSet(); error L1AtomicTokenBridgeCreator_RollupOwnershipMisconfig(); error L1AtomicTokenBridgeCreator_ProxyAdminNotFound(); + error L1AtomicTokenBridgeCreator_L2FactoryCannotBeChanged(); event OrbitTokenBridgeCreated( address indexed inbox, @@ -122,7 +124,8 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { constructor(address _l2MulticallTemplate) { l2MulticallTemplate = _l2MulticallTemplate; - ARB_MULTICALL_CODE_HASH = keccak256(_creationCodeFor(l2MulticallTemplate.code)); + ARB_MULTICALL_CODE_HASH = + keccak256(CreationCodeHelper.getCreationCodeFor(l2MulticallTemplate.code)); _disableInitializers(); } @@ -156,6 +159,12 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { ) external onlyOwner { l1Templates = _l1Templates; + if ( + l2TokenBridgeFactoryTemplate != address(0) + && l2TokenBridgeFactoryTemplate != _l2TokenBridgeFactoryTemplate + ) { + revert L1AtomicTokenBridgeCreator_L2FactoryCannotBeChanged(); + } l2TokenBridgeFactoryTemplate = _l2TokenBridgeFactoryTemplate; l2RouterTemplate = _l2RouterTemplate; l2StandardGatewayTemplate = _l2StandardGatewayTemplate; @@ -413,7 +422,8 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { returns (uint256) { // 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 @@ -518,11 +528,7 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { } function getCanonicalL1RouterAddress(address inbox) public view returns (address) { - address expectedL1ProxyAdminAddress = Create2.computeAddress( - _getL1Salt(OrbitSalts.L1_PROXY_ADMIN, inbox), - keccak256(type(ProxyAdmin).creationCode), - address(this) - ); + address proxyAdminAddress = IInbox_ProxyAdmin(inbox).getProxyAdmin(); bool isUsingFeeToken = _getFeeToken(inbox) != address(0); address template = isUsingFeeToken @@ -534,7 +540,7 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { keccak256( abi.encodePacked( type(TransparentUpgradeableProxy).creationCode, - abi.encode(template, expectedL1ProxyAdminAddress, bytes("")) + abi.encode(template, proxyAdminAddress, bytes("")) ) ), address(this) @@ -661,27 +667,6 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { 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); - } - /** * @notice L2 contracts are deployed as proxy with dummy seed logic contracts using CREATE2. That enables * us to upfront calculate the expected canonical addresses. 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/package.json b/package.json index 48b5c78da6..dc16eb10c5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@arbitrum/token-bridge-contracts", - "version": "1.1.0", + "version": "1.1.2", "license": "Apache-2.0", "scripts": { "prepublishOnly": "hardhat clean && hardhat compile", @@ -24,6 +24,7 @@ "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/local-deployment/deployCreatorAndCreateTokenBridge.ts b/scripts/local-deployment/deployCreatorAndCreateTokenBridge.ts index 897ea6cc56..f1c7402d83 100644 --- a/scripts/local-deployment/deployCreatorAndCreateTokenBridge.ts +++ b/scripts/local-deployment/deployCreatorAndCreateTokenBridge.ts @@ -110,14 +110,14 @@ export const setupTokenBridgeInLocalEnv = async () => { //// run retryable estimate for deploying L2 factory const deployFactoryGasParams = await getEstimateForDeployingFactory( - l1Deployer, - l2Deployer.provider! + parentDeployer, + childDeployer.provider! ) const gasLimitForL2FactoryDeployment = deployFactoryGasParams.gasLimit const { l1TokenBridgeCreator, retryableSender } = await deployL1TokenBridgeCreator( - l1Deployer, + parentDeployer, l1Weth, gasLimitForL2FactoryDeployment ) 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..01a0fdd473 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, IInbox__factory, IOwnable__factory, IRollupCore__factory, + L1AtomicTokenBridgeCreator, L1AtomicTokenBridgeCreator__factory, L1CustomGateway, L1CustomGateway__factory, @@ -85,10 +89,15 @@ describe('tokenBridge', () => { l1RetryableSender.toLowerCase() ) + const creator = L1AtomicTokenBridgeCreator__factory.connect( + l1TokenBridgeCreator, + l1Provider + ) await checkL1RouterInitialization( L1GatewayRouter__factory.connect(l1.router, l1Provider), l1, - l2 + l2, + creator ) await checkL1StandardGatewayInitialization( @@ -103,7 +112,7 @@ describe('tokenBridge', () => { l2 ) - const usingFeeToken = await isUsingFeeToken(l1.inbox, l1Provider) + const usingFeeToken = await _isUsingFeeToken(l1.inbox, l1Provider) if (!usingFeeToken) await checkL1WethGatewayInitialization( L1WethGateway__factory.connect(l1.wethGateway, l1Provider), @@ -131,6 +140,10 @@ describe('tokenBridge', () => { l2 ) + await checkL2MulticallInitialization( + ArbMulticall2__factory.connect(l2.multicall, l2Provider) + ) + if (!usingFeeToken) { await checkL2WethGatewayInitialization( L2WethGateway__factory.connect(l2.wethGateway, l2Provider), @@ -148,6 +161,7 @@ describe('tokenBridge', () => { await checkL1Ownership(l1) await checkL2Ownership(l2) + await checkLogicContracts(usingFeeToken, l2) }) }) @@ -156,10 +170,15 @@ describe('tokenBridge', () => { async function checkL1RouterInitialization( l1Router: L1GatewayRouter, l1: L1, - l2: L2 + l2: L2, + creator: L1AtomicTokenBridgeCreator ) { console.log('checkL1RouterInitialization') + expect(l1.router.toLowerCase()).to.be.eq( + (await creator.getCanonicalL1RouterAddress(l1.inbox)).toLowerCase() + ) + expect((await l1Router.defaultGateway()).toLowerCase()).to.be.eq( l1.standardGateway.toLowerCase() ) @@ -190,6 +209,9 @@ async function checkL1StandardGatewayInitialization( expect((await l1ERC20Gateway.inbox()).toLowerCase()).to.be.eq( l1.inbox.toLowerCase() ) + expect((await l1ERC20Gateway.l2BeaconProxyFactory()).toLowerCase()).to.be.eq( + l2.beaconProxyFactory + ) expect((await l1ERC20Gateway.l2BeaconProxyFactory()).toLowerCase()).to.be.eq( ( await L2ERC20Gateway__factory.connect( @@ -375,10 +397,17 @@ async function checkL2WethGatewayInitialization( ) } +async function checkL2MulticallInitialization(l2Multicall: ArbMulticall2) { + // check l2Multicall is deployed + const l2MulticallCode = await l2Provider.getCode(l2Multicall.address) + expect(l2MulticallCode.length).to.be.gt(0) +} + async function checkL1Ownership(l1: L1) { 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 @@ -411,6 +440,7 @@ async function checkL2Ownership(l2: L2) { const l2ProxyAdmin = await _getProxyAdmin(l2.router, l2Provider) // check proxyAdmins + expect(l2ProxyAdmin).to.be.eq(l2.proxyAdmin) expect(await _getProxyAdmin(l2.router, l2Provider)).to.be.eq(l2ProxyAdmin) expect(await _getProxyAdmin(l2.standardGateway, l2Provider)).to.be.eq( l2ProxyAdmin @@ -432,8 +462,63 @@ async function checkL2Ownership(l2: L2) { expect(await _getOwner(l2ProxyAdmin, l2Provider)).to.be.eq(l2.upgradeExecutor) } +async function checkLogicContracts(usingFeeToken: boolean, l2: L2) { + console.log('checkLogicContracts') + + const upgExecutorLogic = await _getLogicAddress( + l2.upgradeExecutor, + l2Provider + ) + expect(await _isInitialized(upgExecutorLogic, l2Provider)).to.be.true + + const routerLogic = await _getLogicAddress(l2.router, l2Provider) + expect( + await L2GatewayRouter__factory.connect( + routerLogic, + l2Provider + ).counterpartGateway() + ).to.be.not.eq(ethers.constants.AddressZero) + + const standardGatewayLogic = await _getLogicAddress( + l2.standardGateway, + l2Provider + ) + expect( + await L2ERC20Gateway__factory.connect( + standardGatewayLogic, + l2Provider + ).counterpartGateway() + ).to.be.not.eq(ethers.constants.AddressZero) + + const customGatewayLogic = await _getLogicAddress( + l2.customGateway, + l2Provider + ) + expect( + await L2CustomGateway__factory.connect( + customGatewayLogic, + l2Provider + ).counterpartGateway() + ).to.be.not.eq(ethers.constants.AddressZero) + + if (!usingFeeToken) { + const wethGatewayLogic = await _getLogicAddress(l2.wethGateway, l2Provider) + expect( + await L2WethGateway__factory.connect( + wethGatewayLogic, + l2Provider + ).counterpartGateway() + ).to.be.not.eq(ethers.constants.AddressZero) + + const wethLogic = await _getLogicAddress(l2.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 { @@ -511,7 +596,7 @@ async function _getTokenBridgeAddresses( upgradeExecutor: upgradeExecutor.toLowerCase(), } - const usingFeeToken = await isUsingFeeToken(l1.inbox, l1Provider) + const usingFeeToken = await _isUsingFeeToken(l1.inbox, l1Provider) const chainId = await IRollupCore__factory.connect( rollupAddress, @@ -540,6 +625,17 @@ async function _getTokenBridgeAddresses( upgradeExecutor: ( await l1TokenBridgeCreator.getCanonicalL2UpgradeExecutorAddress(chainId) ).toLowerCase(), + multicall: ( + await l1TokenBridgeCreator.getCanonicalL2Multicall(chainId) + ).toLowerCase(), + proxyAdmin: ( + await l1TokenBridgeCreator.getCanonicalL2ProxyAdminAddress(chainId) + ).toLowerCase(), + beaconProxyFactory: ( + await l1TokenBridgeCreator.getCanonicalL2BeaconProxyFactoryAddress( + chainId + ) + ).toLowerCase(), } return { @@ -561,6 +657,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,6 +701,23 @@ async function _getAddressAtStorageSlot( return ethers.utils.getAddress(formatAddress) } +/** + * 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 L1 { inbox: string rollupOwner: string @@ -610,4 +736,7 @@ interface L2 { wethGateway: string weth: string upgradeExecutor: string + multicall: string + proxyAdmin: string + beaconProxyFactory: string }