From 6a80fe3a653adf2b52930370df4d27ef98f6b192 Mon Sep 17 00:00:00 2001 From: TucksonDev Date: Wed, 17 Dec 2025 16:18:22 +0000 Subject: [PATCH] Deployment of deterministic factories --- scripts/atomicTokenBridgeDeployer.ts | 582 ++++++++++++------ .../deployment/deployTokenBridgeCreator.ts | 1 + .../deployCreatorAndCreateTokenBridge.ts | 2 + .../local-deployment/localDeploymentLib.ts | 32 +- 4 files changed, 438 insertions(+), 179 deletions(-) diff --git a/scripts/atomicTokenBridgeDeployer.ts b/scripts/atomicTokenBridgeDeployer.ts index 47a375ac2..058d149e3 100644 --- a/scripts/atomicTokenBridgeDeployer.ts +++ b/scripts/atomicTokenBridgeDeployer.ts @@ -1,4 +1,12 @@ -import { BigNumber, Signer, Wallet, ethers } from 'ethers' +import { + BigNumber, + Contract, + ContractFactory, + Overrides, + Signer, + Wallet, + ethers, +} from 'ethers' import { L1CustomGateway__factory, L1ERC20Gateway__factory, @@ -51,6 +59,98 @@ import { ParentToChildMessageGasParams } from '@arbitrum/sdk/dist/lib/message/Pa */ const ADDRESS_DEAD = '0x000000000000000000000000000000000000dEaD' +/** + * @notice Deploys a contract using the provided factory class and signer. + * @dev Supports optional contract verification and deployment via CREATE2. + * @param FactoryClass - The contract factory class to use for deployment. + * @param signer - The signer to deploy the contract. + * @param constructorArgs - Arguments for the contract constructor. + * @param verify - Whether to verify the contract after deployment. + * @param useCreate2 - Whether to use CREATE2 for deployment. + * @param overrides - Optional transaction overrides. + * @return The deployed contract instance. + */ +export async function deployContract( + FactoryClass: new (signer: Signer) => ContractFactory, + signer: Signer, + constructorArgs: any[] = [], + verify = true, + useCreate2 = false, + overrides?: Overrides +): Promise { + const factory = new FactoryClass(signer) + + const deploymentArgs = [...constructorArgs] + if (overrides) { + deploymentArgs.push(overrides) + } + + let contract: Contract + if (useCreate2) { + contract = await create2( + factory, + constructorArgs, + ethers.constants.HashZero, + overrides + ) + } else { + contract = await factory.deploy(...deploymentArgs) + await contract.deployTransaction.wait() + } + + const contractName = FactoryClass.name.replace('__factory', '') + + console.log( + `* ${contractName} created at address: ${ + contract.address + } ${constructorArgs.join(' ')}` + ) + + /* + if (verify) { + await verifyContract( + signer, + contractName, + contract.address, + constructorArgs + ) + } + */ + + return contract +} + +/** + * @notice Initializes a contract by calling its `initialize` function with the provided arguments. + * @dev If the contract is already initialized, logs a message and does not throw. + * @param contract The contract instance to initialize. + * @param initializationArgs Arguments to pass to the contract's `initialize` function. + * @throws If initialization fails for reasons other than the contract being already initialized. + */ +export async function initializeContract( + contract: Contract, + initializationArgs: any[] = [] +): Promise { + try { + await (await contract.initialize(...initializationArgs)).wait() + console.log(` => Initialized successfully`) + } catch (error: any) { + // Revert reason will be in `error.error.reason` + if ( + error.error && + error.error.reason && + [ + 'execution reverted: ALREADY_INIT', + 'execution reverted: Initializable: contract is already initialized', + ].includes(error.error.reason) + ) { + console.log(` => Already initialized`) + } else { + throw error + } + } +} + /** * 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 @@ -225,30 +325,45 @@ export const deployL1TokenBridgeCreator = async ( l1Deployer: Signer, l1WethAddress: string, gasLimitForL2FactoryDeployment: BigNumber, - verifyContracts = false + verifyContracts = false, + useCreate2 = false ) => { /// deploy creator behind proxy - const l2MulticallAddressOnL1Fac = await new ArbMulticall2__factory( - l1Deployer - ).deploy() - const l2MulticallAddressOnL1 = await l2MulticallAddressOnL1Fac.deployed() + const l2MulticallAddressOnL1 = await deployContract( + ArbMulticall2__factory, + l1Deployer, + [], + verifyContracts, + useCreate2 + ) - const l1TokenBridgeCreatorProxyAdmin = await new ProxyAdmin__factory( - l1Deployer - ).deploy() - await l1TokenBridgeCreatorProxyAdmin.deployed() + const l1TokenBridgeCreatorProxyAdmin = await deployContract( + ProxyAdmin__factory, + l1Deployer, + [], + verifyContracts, + useCreate2 + ) - const l1TokenBridgeCreatorLogic = - await new L1AtomicTokenBridgeCreator__factory(l1Deployer).deploy() - await l1TokenBridgeCreatorLogic.deployed() + const l1TokenBridgeCreatorLogic = await deployContract( + L1AtomicTokenBridgeCreator__factory, + l1Deployer, + [], + verifyContracts, + useCreate2 + ) - const l1TokenBridgeCreatorProxy = - await new TransparentUpgradeableProxy__factory(l1Deployer).deploy( + const l1TokenBridgeCreatorProxy = await deployContract( + TransparentUpgradeableProxy__factory, + l1Deployer, + [ l1TokenBridgeCreatorLogic.address, l1TokenBridgeCreatorProxyAdmin.address, - '0x' - ) - await l1TokenBridgeCreatorProxy.deployed() + '0x', + ], + verifyContracts, + useCreate2 + ) const l1TokenBridgeCreator = L1AtomicTokenBridgeCreator__factory.connect( l1TokenBridgeCreatorProxy.address, @@ -256,19 +371,25 @@ export const deployL1TokenBridgeCreator = async ( ) /// deploy retryable sender behind proxy - const retryableSenderLogic = await new L1TokenBridgeRetryableSender__factory( - l1Deployer - ).deploy() - await retryableSenderLogic.deployed() + const retryableSenderLogic = await deployContract( + L1TokenBridgeRetryableSender__factory, + l1Deployer, + [], + verifyContracts, + useCreate2 + ) - const retryableSenderProxy = await new TransparentUpgradeableProxy__factory( - l1Deployer - ).deploy( - retryableSenderLogic.address, - l1TokenBridgeCreatorProxyAdmin.address, - '0x' + const retryableSenderProxy = await deployContract( + TransparentUpgradeableProxy__factory, + l1Deployer, + [ + retryableSenderLogic.address, + l1TokenBridgeCreatorProxyAdmin.address, + '0x', + ], + verifyContracts, + useCreate2 ) - await retryableSenderProxy.deployed() const retryableSender = L1TokenBridgeRetryableSender__factory.connect( retryableSenderProxy.address, @@ -276,111 +397,132 @@ export const deployL1TokenBridgeCreator = async ( ) // initialize retryable sender logic contract - await (await retryableSenderLogic.initialize()).wait() + await initializeContract(retryableSenderLogic, []) + + // initialize creator logic + await initializeContract(l1TokenBridgeCreatorLogic, [ + retryableSenderLogic.address, + ]) /// init creator - await (await l1TokenBridgeCreator.initialize(retryableSender.address)).wait() + await initializeContract(l1TokenBridgeCreator, [retryableSender.address]) /// 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, - UpgradeExecutorBytecode, - l1Deployer + const routerTemplate = await deployContract( + L1GatewayRouter__factory, + l1Deployer, + [], + verifyContracts, + useCreate2 + ) + await initializeContract(routerTemplate, [ + ADDRESS_DEAD, + ADDRESS_DEAD, + ADDRESS_DEAD, + ADDRESS_DEAD, + ADDRESS_DEAD, + ]) + + const standardGatewayTemplate = await deployContract( + L1ERC20Gateway__factory, + l1Deployer, + [], + verifyContracts, + useCreate2 + ) + await initializeContract(standardGatewayTemplate, [ + ADDRESS_DEAD, + ADDRESS_DEAD, + ADDRESS_DEAD, + ethers.utils.hexZeroPad('0x01', 32), + ADDRESS_DEAD, + ]) + + const customGatewayTemplate = await deployContract( + L1CustomGateway__factory, + l1Deployer, + [], + verifyContracts, + useCreate2 + ) + await initializeContract(customGatewayTemplate, [ + ADDRESS_DEAD, + ADDRESS_DEAD, + ADDRESS_DEAD, + ADDRESS_DEAD, + ]) + + const wethGatewayTemplate = await deployContract( + L1WethGateway__factory, + l1Deployer, + [], + verifyContracts, + useCreate2 + ) + await initializeContract(wethGatewayTemplate, [ + ADDRESS_DEAD, + ADDRESS_DEAD, + ADDRESS_DEAD, + ADDRESS_DEAD, + ADDRESS_DEAD, + ]) + + const feeTokenBasedRouterTemplate = await deployContract( + L1OrbitGatewayRouter__factory, + l1Deployer, + [], + verifyContracts, + useCreate2 + ) + await initializeContract(feeTokenBasedRouterTemplate, [ + ADDRESS_DEAD, + ADDRESS_DEAD, + ADDRESS_DEAD, + ADDRESS_DEAD, + ADDRESS_DEAD, + ]) + + const feeTokenBasedStandardGatewayTemplate = await deployContract( + L1OrbitERC20Gateway__factory, + l1Deployer, + [], + verifyContracts, + useCreate2 + ) + await initializeContract(feeTokenBasedStandardGatewayTemplate, [ + ADDRESS_DEAD, + ADDRESS_DEAD, + ADDRESS_DEAD, + ethers.utils.hexZeroPad('0x01', 32), + ADDRESS_DEAD, + ]) + + const feeTokenBasedCustomGatewayTemplate = await deployContract( + L1OrbitCustomGateway__factory, + l1Deployer, + [], + verifyContracts, + useCreate2 ) - const upgradeExecutor = await upgradeExecutorFactory.deploy() - await upgradeExecutor.deployed() + await initializeContract(feeTokenBasedCustomGatewayTemplate, [ + ADDRESS_DEAD, + ADDRESS_DEAD, + ADDRESS_DEAD, + ADDRESS_DEAD, + ]) + + const upgradeExecutor = await deployContract( + class UpgradeExecutorFactory extends ContractFactory { + constructor(signer: Signer) { + super(UpgradeExecutorABI, UpgradeExecutorBytecode, signer) + } + }, + l1Deployer, + [], + verifyContracts, + useCreate2 + ) + await initializeContract(upgradeExecutor, [ADDRESS_DEAD, [ADDRESS_DEAD]]) const l1Templates = { routerTemplate: routerTemplate.address, @@ -396,56 +538,84 @@ export const deployL1TokenBridgeCreator = async ( } /// deploy L2 contracts as placeholders on L1. Initialize them with dummy data - const l2TokenBridgeFactoryOnL1 = - await new L2AtomicTokenBridgeFactory__factory(l1Deployer).deploy() - await l2TokenBridgeFactoryOnL1.deployed() - - const l2GatewayRouterOnL1 = await new L2GatewayRouter__factory( - 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() + const l2TokenBridgeFactoryOnL1 = await deployContract( + L2AtomicTokenBridgeFactory__factory, + l1Deployer, + [], + verifyContracts, + useCreate2 + ) - const l1Multicall = await new Multicall2__factory(l1Deployer).deploy() - await l1Multicall.deployed() + const l2GatewayRouterOnL1 = await deployContract( + L2GatewayRouter__factory, + l1Deployer, + [], + verifyContracts, + useCreate2 + ) + await initializeContract(l2GatewayRouterOnL1, [ADDRESS_DEAD, ADDRESS_DEAD]) + + const l2StandardGatewayAddressOnL1 = await deployContract( + L2ERC20Gateway__factory, + l1Deployer, + [], + verifyContracts, + useCreate2 + ) + await initializeContract(l2StandardGatewayAddressOnL1, [ + ADDRESS_DEAD, + ADDRESS_DEAD, + ADDRESS_DEAD, + ]) + + const l2CustomGatewayAddressOnL1 = await deployContract( + L2CustomGateway__factory, + l1Deployer, + [], + verifyContracts, + useCreate2 + ) + await initializeContract(l2CustomGatewayAddressOnL1, [ + ADDRESS_DEAD, + ADDRESS_DEAD, + ]) + + const l2WethGatewayAddressOnL1 = await deployContract( + L2WethGateway__factory, + l1Deployer, + [], + verifyContracts, + useCreate2 + ) + await initializeContract(l2WethGatewayAddressOnL1, [ + ADDRESS_DEAD, + ADDRESS_DEAD, + ADDRESS_DEAD, + ADDRESS_DEAD, + ]) + + const l2WethAddressOnL1 = await deployContract( + AeWETH__factory, + l1Deployer, + [], + verifyContracts, + useCreate2 + ) + await initializeContract(l2WethAddressOnL1, [ + 'WethTemplate', + 'WETHT', + 18, + ADDRESS_DEAD, + ADDRESS_DEAD, + ]) + + const l1Multicall = await deployContract( + Multicall2__factory, + l1Deployer, + [], + verifyContracts, + useCreate2 + ) await ( await l1TokenBridgeCreator.setTemplates( @@ -724,3 +894,59 @@ const _getFeeToken = async ( export function sleep(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)) } + +/** + * @notice Deploys a contract using the CREATE2 opcode for deterministic address generation. + * @dev The Create2 factory address can be overridden by the CREATE2_FACTORY environment variable. + * Default factory: https://github.com/Arachnid/deterministic-deployment-proxy/ + * + * @param fac The contract factory used to generate the deployment bytecode. + * @param deploymentArgs The arguments to pass to the contract constructor. + * @param salt The 32-byte salt used for CREATE2 address calculation. Defaults to HashZero. + * @param overrides Optional transaction overrides. + * @return The deployed contract instance at the deterministic address. + */ +export async function create2( + fac: ContractFactory, + deploymentArgs: Array, + salt = ethers.constants.HashZero, + overrides?: Overrides +): Promise { + if (ethers.utils.hexDataLength(salt) !== 32) { + throw new Error('Salt must be a 32-byte hex string') + } + + const DEFAULT_FACTORY = '0x4e59b44847b379578588920cA78FbF26c0B4956C' + const FACTORY = process.env.CREATE2_FACTORY ?? DEFAULT_FACTORY + if ((await fac.signer.provider!.getCode(FACTORY)).length <= 2) { + throw new Error( + `Factory contract not deployed at address: ${FACTORY}${ + FACTORY.toLowerCase() === DEFAULT_FACTORY.toLowerCase() + ? '\n(For deployment instructions, see https://github.com/Arachnid/deterministic-deployment-proxy/ )' + : '' + }` + ) + } + const data = fac.getDeployTransaction(...deploymentArgs).data + if (!data) { + throw new Error('No deploy data found for contract factory') + } + + const address = ethers.utils.getCreate2Address( + FACTORY, + salt, + ethers.utils.keccak256(data) + ) + if ((await fac.signer.provider!.getCode(address)).length > 2) { + return fac.attach(address) + } + + const tx = await fac.signer.sendTransaction({ + to: FACTORY, + data: ethers.utils.concat([salt, data]), + ...overrides, + }) + await tx.wait() + + return fac.attach(address) +} diff --git a/scripts/deployment/deployTokenBridgeCreator.ts b/scripts/deployment/deployTokenBridgeCreator.ts index 6fa8daaf6..2f4e85f48 100644 --- a/scripts/deployment/deployTokenBridgeCreator.ts +++ b/scripts/deployment/deployTokenBridgeCreator.ts @@ -79,6 +79,7 @@ export const deployTokenBridgeCreator = async () => { l1Deployer, envVars.baseChainWeth, gasLimitForL2FactoryDeployment, + true, true ) diff --git a/scripts/local-deployment/deployCreatorAndCreateTokenBridge.ts b/scripts/local-deployment/deployCreatorAndCreateTokenBridge.ts index 69ffab894..c6af25b86 100644 --- a/scripts/local-deployment/deployCreatorAndCreateTokenBridge.ts +++ b/scripts/local-deployment/deployCreatorAndCreateTokenBridge.ts @@ -1,5 +1,7 @@ import * as fs from 'fs' import { setupTokenBridgeInLocalEnv } from './localDeploymentLib' +import dotenv from 'dotenv' +dotenv.config() async function main() { const { diff --git a/scripts/local-deployment/localDeploymentLib.ts b/scripts/local-deployment/localDeploymentLib.ts index 020565c99..f3642b0e6 100644 --- a/scripts/local-deployment/localDeploymentLib.ts +++ b/scripts/local-deployment/localDeploymentLib.ts @@ -21,6 +21,31 @@ const LOCALHOST_L2_RPC = 'http://127.0.0.1:8547' const LOCALHOST_L3_RPC = 'http://127.0.0.1:3347' const LOCALHOST_L3_OWNER_KEY = '0xecdf21cb41c65afb51f91df408b7656e2c8739a5877f2814add0afd780cc210e' + +export const deployCreate2Factory = async ( + deployerWallet: Wallet +): Promise => { + const create2FactoryAddress = '0x4e59b44847b379578588920ca78fbf26c0b4956c' + const factoryCode = await deployerWallet.provider.getCode( + create2FactoryAddress + ) + if (factoryCode.length <= 2) { + console.log('CREATE2 factory not yet deployed. Deploying...') + const fundingTx = await deployerWallet.sendTransaction({ + to: '0x3fab184622dc19b6109349b94811493bf2a45362', + value: ethers.utils.parseEther('0.01'), + }) + await fundingTx.wait() + const create2SignedTx = + '0xf8a58085174876e800830186a08080b853604580600e600039806000f350fe7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf31ba02222222222222222222222222222222222222222222222222222222222222222a02222222222222222222222222222222222222222222222222222222222222222' + const create2DeployTx = await deployerWallet.provider.sendTransaction( + create2SignedTx + ) + await create2DeployTx.wait() + console.log(`CREATE2 factory deployed at ${create2FactoryAddress}`) + } +} + /** * Steps: * - read network info from local container and register networks @@ -83,6 +108,9 @@ export const setupTokenBridgeInLocalEnv = async () => { ) registerCustomArbitrumNetwork(coreL2Network) + // prerequisite - deploy CREATE2 factory + await deployCreate2Factory(parentDeployer) + // prerequisite - deploy L1 creator and set templates console.log('Deploying L1TokenBridgeCreator') @@ -108,7 +136,9 @@ export const setupTokenBridgeInLocalEnv = async () => { await deployL1TokenBridgeCreator( parentDeployer, l1Weth, - gasLimitForL2FactoryDeployment + gasLimitForL2FactoryDeployment, + false, + true ) console.log('L1TokenBridgeCreator', l1TokenBridgeCreator.address) console.log('L1TokenBridgeRetryableSender', retryableSender.address)