From c3eca4c862dfb37ac7185c903017ed2961fa9031 Mon Sep 17 00:00:00 2001 From: Christophe Deveaux Date: Fri, 12 Apr 2024 14:37:05 +0200 Subject: [PATCH] feat: add high level createTokenBridge function (#67) --- src/createTokenBridge-ethers.ts | 1 - src/createTokenBridge.integration.test.ts | 544 +++++++++++------- src/createTokenBridge.ts | 315 ++++++++++ ...ateTokenBridgePrepareTransactionReceipt.ts | 18 +- src/index.ts | 8 + 5 files changed, 665 insertions(+), 221 deletions(-) create mode 100644 src/createTokenBridge.ts diff --git a/src/createTokenBridge-ethers.ts b/src/createTokenBridge-ethers.ts index b7c18221..ba387ad2 100644 --- a/src/createTokenBridge-ethers.ts +++ b/src/createTokenBridge-ethers.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-empty */ import { Address, PublicClient } from 'viem'; import { BigNumber, ContractFactory, ethers } from 'ethers'; import { L1ToL2MessageGasEstimator } from '@arbitrum/sdk'; diff --git a/src/createTokenBridge.integration.test.ts b/src/createTokenBridge.integration.test.ts index 3a15415d..9492d2aa 100644 --- a/src/createTokenBridge.integration.test.ts +++ b/src/createTokenBridge.integration.test.ts @@ -1,4 +1,4 @@ -import { it, expect } from 'vitest'; +import { describe, it, expect } from 'vitest'; import { createPublicClient, encodeFunctionData, @@ -18,6 +18,8 @@ import { createTokenBridgePrepareCustomFeeTokenApprovalTransactionRequest } from import { erc20 } from './contracts'; import { createTokenBridgePrepareSetWethGatewayTransactionRequest } from './createTokenBridgePrepareSetWethGatewayTransactionRequest'; import { createTokenBridgePrepareSetWethGatewayTransactionReceipt } from './createTokenBridgePrepareSetWethGatewayTransactionReceipt'; +import { CreateTokenBridgeParams, createTokenBridge } from './createTokenBridge'; +import { TokenBridgeContracts } from './types/TokenBridgeContracts'; const testnodeAccounts = getNitroTestnodePrivateKeyAccounts(); const l2RollupOwner = testnodeAccounts.l2RollupOwner; @@ -39,67 +41,7 @@ const nitroTestnodeL3Client = createPublicClient({ transport: http(nitroTestnodeL3.rpcUrls.default.http[0]), }); -it(`successfully deploys token bridge contracts through token bridge creator`, async () => { - const testnodeInformation = getInformationFromTestnode(); - - // deploy a fresh token bridge creator, because it is only possible to deploy one token bridge per rollup per token bridge creator - const tokenBridgeCreator = await deployTokenBridgeCreator({ - publicClient: nitroTestnodeL1Client, - }); - - const txRequest = await createTokenBridgePrepareTransactionRequest({ - params: { - rollup: testnodeInformation.rollup, - rollupOwner: l2RollupOwner.address, - }, - parentChainPublicClient: nitroTestnodeL1Client, - orbitChainPublicClient: nitroTestnodeL2Client, - account: l2RollupOwner.address, - gasOverrides: { - gasLimit: { - base: 6_000_000n, - }, - }, - retryableGasOverrides: { - maxGasForFactory: { - base: 20_000_000n, - }, - maxGasForContracts: { - base: 20_000_000n, - }, - maxSubmissionCostForFactory: { - base: 4_000_000_000_000n, - }, - maxSubmissionCostForContracts: { - base: 4_000_000_000_000n, - }, - }, - tokenBridgeCreatorAddressOverride: tokenBridgeCreator, - }); - - // sign and send the transaction - const txHash = await nitroTestnodeL1Client.sendRawTransaction({ - serializedTransaction: await l2RollupOwner.signTransaction(txRequest), - }); - - // get the transaction receipt after waiting for the transaction to complete - const txReceipt = createTokenBridgePrepareTransactionReceipt( - await nitroTestnodeL1Client.waitForTransactionReceipt({ hash: txHash }), - ); - expect(txReceipt.status).toEqual('success'); - - // checking retryables execution - const orbitChainRetryableReceipts = await txReceipt.waitForRetryables({ - orbitPublicClient: nitroTestnodeL2Client, - }); - expect(orbitChainRetryableReceipts).toHaveLength(2); - expect(orbitChainRetryableReceipts[0].status).toEqual('success'); - expect(orbitChainRetryableReceipts[1].status).toEqual('success'); - - // get contracts - const tokenBridgeContracts = await txReceipt.getTokenBridgeContracts({ - parentChainPublicClient: nitroTestnodeL1Client, - }); +function checkTokenBridgeContracts(tokenBridgeContracts: TokenBridgeContracts) { expect(Object.keys(tokenBridgeContracts)).toHaveLength(2); // parent chain contracts @@ -107,8 +49,6 @@ it(`successfully deploys token bridge contracts through token bridge creator`, a expect(tokenBridgeContracts.parentChainContracts.router).not.toEqual(zeroAddress); expect(tokenBridgeContracts.parentChainContracts.standardGateway).not.toEqual(zeroAddress); expect(tokenBridgeContracts.parentChainContracts.customGateway).not.toEqual(zeroAddress); - expect(tokenBridgeContracts.parentChainContracts.wethGateway).not.toEqual(zeroAddress); - expect(tokenBridgeContracts.parentChainContracts.weth).not.toEqual(zeroAddress); expect(tokenBridgeContracts.parentChainContracts.multicall).not.toEqual(zeroAddress); // orbit chain contracts @@ -116,43 +56,24 @@ it(`successfully deploys token bridge contracts through token bridge creator`, a expect(tokenBridgeContracts.orbitChainContracts.router).not.toEqual(zeroAddress); expect(tokenBridgeContracts.orbitChainContracts.standardGateway).not.toEqual(zeroAddress); expect(tokenBridgeContracts.orbitChainContracts.customGateway).not.toEqual(zeroAddress); - expect(tokenBridgeContracts.orbitChainContracts.wethGateway).not.toEqual(zeroAddress); - expect(tokenBridgeContracts.orbitChainContracts.weth).not.toEqual(zeroAddress); expect(tokenBridgeContracts.orbitChainContracts.proxyAdmin).not.toEqual(zeroAddress); expect(tokenBridgeContracts.orbitChainContracts.beaconProxyFactory).not.toEqual(zeroAddress); expect(tokenBridgeContracts.orbitChainContracts.upgradeExecutor).not.toEqual(zeroAddress); expect(tokenBridgeContracts.orbitChainContracts.multicall).not.toEqual(zeroAddress); - - // set weth gateway - const setWethGatewayTxRequest = await createTokenBridgePrepareSetWethGatewayTransactionRequest({ - rollup: testnodeInformation.rollup, - parentChainPublicClient: nitroTestnodeL1Client, - orbitChainPublicClient: nitroTestnodeL2Client, - account: l2RollupOwner.address, - retryableGasOverrides: { - gasLimit: { - base: 100_000n, - }, - }, - tokenBridgeCreatorAddressOverride: tokenBridgeCreator, - }); - - // sign and send the transaction - const setWethGatewayTxHash = await nitroTestnodeL1Client.sendRawTransaction({ - serializedTransaction: await l2RollupOwner.signTransaction(setWethGatewayTxRequest), - }); - - // get the transaction receipt after waiting for the transaction to complete - const setWethGatewayTxReceipt = createTokenBridgePrepareSetWethGatewayTransactionReceipt( - await nitroTestnodeL1Client.waitForTransactionReceipt({ hash: setWethGatewayTxHash }), - ); - - // checking retryables execution - const orbitChainSetGatewayRetryableReceipt = await setWethGatewayTxReceipt.waitForRetryables({ - orbitPublicClient: nitroTestnodeL2Client, - }); - expect(orbitChainSetGatewayRetryableReceipt).toHaveLength(1); - expect(orbitChainSetGatewayRetryableReceipt[0].status).toEqual('success'); +} + +async function checkWethGateways( + tokenBridgeContracts: TokenBridgeContracts, + { customFeeToken }: { customFeeToken: boolean }, +) { + if (customFeeToken) { + // wethGateway and weth should be the zeroAddress on custom-fee-token chains + expect(tokenBridgeContracts.orbitChainContracts.wethGateway).toEqual(zeroAddress); + expect(tokenBridgeContracts.orbitChainContracts.weth).toEqual(zeroAddress); + expect(tokenBridgeContracts.parentChainContracts.wethGateway).toEqual(zeroAddress); + expect(tokenBridgeContracts.parentChainContracts.weth).toEqual(zeroAddress); + return; + } // verify weth gateway (parent chain) const registeredWethGatewayOnParentChain = await nitroTestnodeL1Client.readContract({ @@ -176,140 +97,341 @@ it(`successfully deploys token bridge contracts through token bridge creator`, a expect(registeredWethGatewayOnOrbitChain).toEqual( tokenBridgeContracts.orbitChainContracts.wethGateway, ); -}); -it(`successfully deploys token bridge contracts with a custom fee token through token bridge creator`, async () => { - const testnodeInformation = getInformationFromTestnode(); + expect(tokenBridgeContracts.parentChainContracts.weth).not.toEqual(zeroAddress); + expect(tokenBridgeContracts.parentChainContracts.wethGateway).not.toEqual(zeroAddress); + expect(tokenBridgeContracts.orbitChainContracts.weth).not.toEqual(zeroAddress); + expect(tokenBridgeContracts.orbitChainContracts.wethGateway).not.toEqual(zeroAddress); +} - // deploy a fresh token bridge creator, because it is only possible to deploy one token bridge per rollup per token bridge creator - const tokenBridgeCreator = await deployTokenBridgeCreator({ - publicClient: nitroTestnodeL2Client, - }); +describe('createTokenBridge utils function', () => { + it(`successfully deploys token bridge contracts through token bridge creator`, async () => { + const testnodeInformation = getInformationFromTestnode(); - // ----------------------------- - // 1. fund l3deployer account - const fundTxRequestRaw = await nitroTestnodeL2Client.prepareTransactionRequest({ - chain: nitroTestnodeL2Client.chain, - to: testnodeInformation.l3NativeToken, - data: encodeFunctionData({ - abi: erc20.abi, - functionName: 'transfer', - args: [l3RollupOwner.address, parseEther('500')], - }), - value: BigInt(0), - account: l3TokenBridgeDeployer, - }); + // deploy a fresh token bridge creator, because it is only possible to deploy one token bridge per rollup per token bridge creator + const tokenBridgeCreator = await deployTokenBridgeCreator({ + publicClient: nitroTestnodeL1Client, + }); - // sign and send the transaction - const fundTxRequest = { ...fundTxRequestRaw, chainId: nitroTestnodeL2Client.chain.id }; - const fundTxHash = await nitroTestnodeL2Client.sendRawTransaction({ - serializedTransaction: await l3TokenBridgeDeployer.signTransaction(fundTxRequest), - }); + const txRequest = await createTokenBridgePrepareTransactionRequest({ + params: { + rollup: testnodeInformation.rollup, + rollupOwner: l2RollupOwner.address, + }, + parentChainPublicClient: nitroTestnodeL1Client, + orbitChainPublicClient: nitroTestnodeL2Client, + account: l2RollupOwner.address, + gasOverrides: { + gasLimit: { + base: 6_000_000n, + }, + }, + retryableGasOverrides: { + maxGasForFactory: { + base: 20_000_000n, + }, + maxGasForContracts: { + base: 20_000_000n, + }, + maxSubmissionCostForFactory: { + base: 4_000_000_000_000n, + }, + maxSubmissionCostForContracts: { + base: 4_000_000_000_000n, + }, + }, + tokenBridgeCreatorAddressOverride: tokenBridgeCreator, + }); - // get the transaction receipt after waiting for the transaction to complete - const fundTxReceipt = await nitroTestnodeL2Client.waitForTransactionReceipt({ - hash: fundTxHash, - }); - expect(fundTxReceipt.status).toEqual('success'); - - // ----------------------------- - // 2. approve custom fee token to be spent by the TokenBridgeCreator - const allowanceParams: CreateTokenBridgeEnoughCustomFeeTokenAllowanceParams = { - nativeToken: testnodeInformation.l3NativeToken, - owner: l3RollupOwner.address, - publicClient: nitroTestnodeL2Client, - tokenBridgeCreatorAddressOverride: tokenBridgeCreator, - }; - - // sign and send the transaction - const approvalForTokenBridgeCreatorTxHash = await nitroTestnodeL2Client.sendRawTransaction({ - serializedTransaction: await l3RollupOwner.signTransaction( - await createTokenBridgePrepareCustomFeeTokenApprovalTransactionRequest(allowanceParams), - ), + // sign and send the transaction + const txHash = await nitroTestnodeL1Client.sendRawTransaction({ + serializedTransaction: await l2RollupOwner.signTransaction(txRequest), + }); + + // get the transaction receipt after waiting for the transaction to complete + const txReceipt = createTokenBridgePrepareTransactionReceipt( + await nitroTestnodeL1Client.waitForTransactionReceipt({ hash: txHash }), + ); + expect(txReceipt.status).toEqual('success'); + + // checking retryables execution + const orbitChainRetryableReceipts = await txReceipt.waitForRetryables({ + orbitPublicClient: nitroTestnodeL2Client, + }); + expect(orbitChainRetryableReceipts).toHaveLength(2); + expect(orbitChainRetryableReceipts[0].status).toEqual('success'); + expect(orbitChainRetryableReceipts[1].status).toEqual('success'); + + // get contracts + const tokenBridgeContracts = await txReceipt.getTokenBridgeContracts({ + parentChainPublicClient: nitroTestnodeL1Client, + }); + checkTokenBridgeContracts(tokenBridgeContracts); + + // set weth gateway + const setWethGatewayTxRequest = await createTokenBridgePrepareSetWethGatewayTransactionRequest({ + rollup: testnodeInformation.rollup, + parentChainPublicClient: nitroTestnodeL1Client, + orbitChainPublicClient: nitroTestnodeL2Client, + account: l2RollupOwner.address, + retryableGasOverrides: { + gasLimit: { + base: 100_000n, + }, + }, + tokenBridgeCreatorAddressOverride: tokenBridgeCreator, + }); + + // sign and send the transaction + const setWethGatewayTxHash = await nitroTestnodeL1Client.sendRawTransaction({ + serializedTransaction: await l2RollupOwner.signTransaction(setWethGatewayTxRequest), + }); + + // get the transaction receipt after waiting for the transaction to complete + const setWethGatewayTxReceipt = createTokenBridgePrepareSetWethGatewayTransactionReceipt( + await nitroTestnodeL1Client.waitForTransactionReceipt({ hash: setWethGatewayTxHash }), + ); + + // checking retryables execution + const orbitChainSetGatewayRetryableReceipt = await setWethGatewayTxReceipt.waitForRetryables({ + orbitPublicClient: nitroTestnodeL2Client, + }); + expect(orbitChainSetGatewayRetryableReceipt).toHaveLength(1); + expect(orbitChainSetGatewayRetryableReceipt[0].status).toEqual('success'); + + checkWethGateways(tokenBridgeContracts, { customFeeToken: false }); }); - // get the transaction receipt after waiting for the transaction to complete - const approvalForNewTokenBridgeCreatorTxReceipt = - await nitroTestnodeL2Client.waitForTransactionReceipt({ - hash: approvalForTokenBridgeCreatorTxHash, + it(`successfully deploys token bridge contracts with a custom fee token through token bridge creator`, async () => { + const testnodeInformation = getInformationFromTestnode(); + + // deploy a fresh token bridge creator, because it is only possible to deploy one token bridge per rollup per token bridge creator + const tokenBridgeCreator = await deployTokenBridgeCreator({ + publicClient: nitroTestnodeL2Client, }); - expect(approvalForNewTokenBridgeCreatorTxReceipt.status).toEqual('success'); - // ----------------------------- - // 3. create the token bridge - const txRequest = await createTokenBridgePrepareTransactionRequest({ - params: { - rollup: testnodeInformation.l3Rollup, - rollupOwner: l3RollupOwner.address, - }, - parentChainPublicClient: nitroTestnodeL2Client, - orbitChainPublicClient: nitroTestnodeL3Client, - account: l3RollupOwner.address, - gasOverrides: { - gasLimit: { - base: 6_000_000n, + // ----------------------------- + // 1. fund l3deployer account + const fundTxRequestRaw = await nitroTestnodeL2Client.prepareTransactionRequest({ + chain: nitroTestnodeL2Client.chain, + to: testnodeInformation.l3NativeToken, + data: encodeFunctionData({ + abi: erc20.abi, + functionName: 'transfer', + args: [l3RollupOwner.address, parseEther('500')], + }), + value: BigInt(0), + account: l3TokenBridgeDeployer, + }); + + // sign and send the transaction + const fundTxRequest = { ...fundTxRequestRaw, chainId: nitroTestnodeL2Client.chain.id }; + const fundTxHash = await nitroTestnodeL2Client.sendRawTransaction({ + serializedTransaction: await l3TokenBridgeDeployer.signTransaction(fundTxRequest), + }); + + // get the transaction receipt after waiting for the transaction to complete + const fundTxReceipt = await nitroTestnodeL2Client.waitForTransactionReceipt({ + hash: fundTxHash, + }); + expect(fundTxReceipt.status).toEqual('success'); + + // ----------------------------- + // 2. approve custom fee token to be spent by the TokenBridgeCreator + const allowanceParams: CreateTokenBridgeEnoughCustomFeeTokenAllowanceParams = { + nativeToken: testnodeInformation.l3NativeToken, + owner: l3RollupOwner.address, + publicClient: nitroTestnodeL2Client, + tokenBridgeCreatorAddressOverride: tokenBridgeCreator, + }; + + // sign and send the transaction + const approvalForTokenBridgeCreatorTxHash = await nitroTestnodeL2Client.sendRawTransaction({ + serializedTransaction: await l3RollupOwner.signTransaction( + await createTokenBridgePrepareCustomFeeTokenApprovalTransactionRequest(allowanceParams), + ), + }); + + // get the transaction receipt after waiting for the transaction to complete + const approvalForNewTokenBridgeCreatorTxReceipt = + await nitroTestnodeL2Client.waitForTransactionReceipt({ + hash: approvalForTokenBridgeCreatorTxHash, + }); + expect(approvalForNewTokenBridgeCreatorTxReceipt.status).toEqual('success'); + + // ----------------------------- + // 3. create the token bridge + const txRequest = await createTokenBridgePrepareTransactionRequest({ + params: { + rollup: testnodeInformation.l3Rollup, + rollupOwner: l3RollupOwner.address, + }, + parentChainPublicClient: nitroTestnodeL2Client, + orbitChainPublicClient: nitroTestnodeL3Client, + account: l3RollupOwner.address, + gasOverrides: { + gasLimit: { + base: 6_000_000n, + }, }, - }, - retryableGasOverrides: { - maxGasForFactory: { - base: 20_000_000n, + retryableGasOverrides: { + maxGasForFactory: { + base: 20_000_000n, + }, + maxGasForContracts: { + base: 20_000_000n, + }, + maxSubmissionCostForFactory: { + base: 4_000_000_000_000n, + }, + maxSubmissionCostForContracts: { + base: 4_000_000_000_000n, + }, }, - maxGasForContracts: { - base: 20_000_000n, + tokenBridgeCreatorAddressOverride: tokenBridgeCreator, + }); + + // sign and send the transaction + const txHash = await nitroTestnodeL2Client.sendRawTransaction({ + serializedTransaction: await l3RollupOwner.signTransaction(txRequest), + }); + + // get the transaction receipt after waiting for the transaction to complete + const txReceipt = createTokenBridgePrepareTransactionReceipt( + await nitroTestnodeL2Client.waitForTransactionReceipt({ hash: txHash }), + ); + expect(txReceipt.status).toEqual('success'); + + // checking retryables execution + const orbitChainRetryableReceipts = await txReceipt.waitForRetryables({ + orbitPublicClient: nitroTestnodeL3Client, + }); + expect(orbitChainRetryableReceipts).toHaveLength(2); + expect(orbitChainRetryableReceipts[0].status).toEqual('success'); + expect(orbitChainRetryableReceipts[1].status).toEqual('success'); + + // get contracts + const tokenBridgeContracts = await txReceipt.getTokenBridgeContracts({ + parentChainPublicClient: nitroTestnodeL2Client, + }); + + checkTokenBridgeContracts(tokenBridgeContracts); + checkWethGateways(tokenBridgeContracts, { customFeeToken: true }); + }); +}); + +describe('createTokenBridge', () => { + it('successfully deploys token bridge contracts', async () => { + const testnodeInformation = getInformationFromTestnode(); + + // deploy a fresh token bridge creator, because it is only possible to deploy one token bridge per rollup per token bridge creator + const tokenBridgeCreator = await deployTokenBridgeCreator({ + publicClient: nitroTestnodeL1Client, + }); + + const { tokenBridgeContracts } = await createTokenBridge({ + rollupOwner: l2RollupOwner.address, + rollupAddress: testnodeInformation.rollup, + account: l2RollupOwner, + parentChainPublicClient: nitroTestnodeL1Client, + orbitChainPublicClient: nitroTestnodeL2Client, + tokenBridgeCreatorAddressOverride: tokenBridgeCreator, + gasOverrides: { + gasLimit: { + base: 6_000_000n, + }, }, - maxSubmissionCostForFactory: { - base: 4_000_000_000_000n, + retryableGasOverrides: { + maxGasForFactory: { + base: 20_000_000n, + }, + maxGasForContracts: { + base: 20_000_000n, + }, + maxSubmissionCostForFactory: { + base: 4_000_000_000_000n, + }, + maxSubmissionCostForContracts: { + base: 4_000_000_000_000n, + }, }, - maxSubmissionCostForContracts: { - base: 4_000_000_000_000n, + setWethGatewayGasOverrides: { + gasLimit: { + base: 100_000n, + }, }, - }, - tokenBridgeCreatorAddressOverride: tokenBridgeCreator, - }); + }); - // sign and send the transaction - const txHash = await nitroTestnodeL2Client.sendRawTransaction({ - serializedTransaction: await l3RollupOwner.signTransaction(txRequest), + checkTokenBridgeContracts(tokenBridgeContracts); + checkWethGateways(tokenBridgeContracts, { customFeeToken: false }); }); - // get the transaction receipt after waiting for the transaction to complete - const txReceipt = createTokenBridgePrepareTransactionReceipt( - await nitroTestnodeL2Client.waitForTransactionReceipt({ hash: txHash }), - ); - expect(txReceipt.status).toEqual('success'); + it('successfully deploys token bridge contracts with a custom fee token', async () => { + const testnodeInformation = getInformationFromTestnode(); - // checking retryables execution - const orbitChainRetryableReceipts = await txReceipt.waitForRetryables({ - orbitPublicClient: nitroTestnodeL3Client, - }); - expect(orbitChainRetryableReceipts).toHaveLength(2); - expect(orbitChainRetryableReceipts[0].status).toEqual('success'); - expect(orbitChainRetryableReceipts[1].status).toEqual('success'); + // deploy a fresh token bridge creator, because it is only possible to deploy one token bridge per rollup per token bridge creator + const tokenBridgeCreator = await deployTokenBridgeCreator({ + publicClient: nitroTestnodeL2Client, + }); - // get contracts - const tokenBridgeContracts = await txReceipt.getTokenBridgeContracts({ - parentChainPublicClient: nitroTestnodeL2Client, - }); - expect(Object.keys(tokenBridgeContracts)).toHaveLength(2); + // ----------------------------- + // 1. fund l3deployer account + const fundTxRequestRaw = await nitroTestnodeL2Client.prepareTransactionRequest({ + chain: nitroTestnodeL2Client.chain, + to: testnodeInformation.l3NativeToken, + data: encodeFunctionData({ + abi: erc20.abi, + functionName: 'transfer', + args: [l3RollupOwner.address, parseEther('500')], + }), + value: BigInt(0), + account: l3TokenBridgeDeployer, + }); - // parent chain contracts - expect(Object.keys(tokenBridgeContracts.parentChainContracts)).toHaveLength(6); - expect(tokenBridgeContracts.parentChainContracts.router).not.toEqual(zeroAddress); - expect(tokenBridgeContracts.parentChainContracts.standardGateway).not.toEqual(zeroAddress); - expect(tokenBridgeContracts.parentChainContracts.customGateway).not.toEqual(zeroAddress); - expect(tokenBridgeContracts.parentChainContracts.multicall).not.toEqual(zeroAddress); + // sign and send the transaction + const fundTxRequest = { ...fundTxRequestRaw, chainId: nitroTestnodeL2Client.chain.id }; + const fundTxHash = await nitroTestnodeL2Client.sendRawTransaction({ + serializedTransaction: await l3TokenBridgeDeployer.signTransaction(fundTxRequest), + }); - // orbit chain contracts - expect(Object.keys(tokenBridgeContracts.orbitChainContracts)).toHaveLength(9); - expect(tokenBridgeContracts.orbitChainContracts.router).not.toEqual(zeroAddress); - expect(tokenBridgeContracts.orbitChainContracts.standardGateway).not.toEqual(zeroAddress); - expect(tokenBridgeContracts.orbitChainContracts.customGateway).not.toEqual(zeroAddress); - expect(tokenBridgeContracts.orbitChainContracts.proxyAdmin).not.toEqual(zeroAddress); - expect(tokenBridgeContracts.orbitChainContracts.beaconProxyFactory).not.toEqual(zeroAddress); - expect(tokenBridgeContracts.orbitChainContracts.upgradeExecutor).not.toEqual(zeroAddress); - expect(tokenBridgeContracts.orbitChainContracts.multicall).not.toEqual(zeroAddress); + // get the transaction receipt after waiting for the transaction to complete + const fundTxReceipt = await nitroTestnodeL2Client.waitForTransactionReceipt({ + hash: fundTxHash, + }); + expect(fundTxReceipt.status).toEqual('success'); - // wethGateway and weth should be the zeroAddress on custom-fee-token chains - expect(tokenBridgeContracts.orbitChainContracts.wethGateway).toEqual(zeroAddress); - expect(tokenBridgeContracts.orbitChainContracts.weth).toEqual(zeroAddress); + // ----------------------------- + // 2. Deploy token bridge contracts + const { tokenBridgeContracts } = await createTokenBridge({ + rollupOwner: l3RollupOwner.address, + rollupAddress: testnodeInformation.l3Rollup, + account: l3RollupOwner, + parentChainPublicClient: nitroTestnodeL2Client, + orbitChainPublicClient: nitroTestnodeL3Client, + nativeTokenAddress: testnodeInformation.l3NativeToken, + tokenBridgeCreatorAddressOverride: tokenBridgeCreator, + gasOverrides: { + gasLimit: { + base: 6_000_000n, + }, + }, + retryableGasOverrides: { + maxGasForFactory: { + base: 20_000_000n, + }, + maxGasForContracts: { + base: 20_000_000n, + }, + maxSubmissionCostForFactory: { + base: 4_000_000_000_000n, + }, + maxSubmissionCostForContracts: { + base: 4_000_000_000_000n, + }, + }, + }); + + checkTokenBridgeContracts(tokenBridgeContracts); + checkWethGateways(tokenBridgeContracts, { customFeeToken: true }); + }); }); diff --git a/src/createTokenBridge.ts b/src/createTokenBridge.ts new file mode 100644 index 00000000..0631c95d --- /dev/null +++ b/src/createTokenBridge.ts @@ -0,0 +1,315 @@ +import { + Address, + Chain, + PrivateKeyAccount, + PublicClient, + Transaction, + TransactionReceipt, +} from 'viem'; +import { + CreateTokenBridgeEnoughCustomFeeTokenAllowanceParams, + createTokenBridgeEnoughCustomFeeTokenAllowance, +} from './createTokenBridgeEnoughCustomFeeTokenAllowance'; +import { createTokenBridgePrepareCustomFeeTokenApprovalTransactionRequest } from './createTokenBridgePrepareCustomFeeTokenApprovalTransactionRequest'; +import { TokenBridgeContracts } from './types/TokenBridgeContracts'; +import { + TransactionRequestRetryableGasOverrides as TokenBridgeRetryableGasOverrides, + createTokenBridgePrepareTransactionRequest, +} from './createTokenBridgePrepareTransactionRequest'; +import { + CreateTokenBridgeTransactionReceipt, + WaitForRetryablesResult, + createTokenBridgePrepareTransactionReceipt, +} from './createTokenBridgePrepareTransactionReceipt'; +import { + createTokenBridgePrepareSetWethGatewayTransactionRequest, + TransactionRequestRetryableGasOverrides as SetWethGatewayRetryableGasOverrides, +} from './createTokenBridgePrepareSetWethGatewayTransactionRequest'; +import { + CreateTokenBridgeSetWethGatewayTransactionReceipt, + createTokenBridgePrepareSetWethGatewayTransactionReceipt, +} from './createTokenBridgePrepareSetWethGatewayTransactionReceipt'; +import { isCustomFeeTokenAddress } from './utils/isCustomFeeTokenAddress'; +import { WithTokenBridgeCreatorAddressOverride } from './types/createTokenBridgeTypes'; +import { TransactionRequestGasOverrides } from './utils/gasOverrides'; + +function getBlockExplorerUrl(chain: Chain | undefined) { + return chain?.blockExplorers?.default.url; +} + +export type CreateTokenBridgeParams = WithTokenBridgeCreatorAddressOverride<{ + rollupOwner: Address; + rollupAddress: Address; + account: PrivateKeyAccount; + nativeTokenAddress?: Address; + parentChainPublicClient: PublicClient; + orbitChainPublicClient: PublicClient; + gasOverrides?: TransactionRequestGasOverrides; + retryableGasOverrides?: TokenBridgeRetryableGasOverrides; + setWethGatewayGasOverrides?: SetWethGatewayRetryableGasOverrides; +}>; +export type CreateTokenBridgeResults = { + /** + * Transaction of createTokenBridgePrepareTransactionRequest + */ + transaction: Transaction; + /** + * Transaction receipt of createTokenBridgePrepareTransactionReceipt ({@link CreateTokenBridgeTransactionReceipt}) + */ + transactionReceipt: CreateTokenBridgeTransactionReceipt; + /** + * Retryable transaction receipts of createTokenBridgePrepareTransactionReceipt ({@link WaitForRetryablesResult}) + */ + retryables: WaitForRetryablesResult; + /** + * Core token bridge contracts ({@link TokenBridgeContracts}) + */ + tokenBridgeContracts: TokenBridgeContracts; + /** + * Optional: createTokenBridgePrepareSetWethGatewayTransaction's result + */ + setWethGateway?: { + /** + * Transaction of createTokenBridgePrepareSetWethGatewayTransactionReceipt ({@link Transaction}) + */ + transaction: Transaction; + /** + * Transaction receipt of createTokenBridgePrepareSetWethGatewayTransactionReceipt ({@link createTokenBridgePrepareSetWethGatewayTransactionReceipt}) + */ + transactionReceipt: CreateTokenBridgeSetWethGatewayTransactionReceipt; + /** + * Retryable transaction receipt of createTokenBridgePrepareSetWethGatewayTransactionReceipt ({@link WaitForRetryablesResult}) + */ + retryables: [TransactionReceipt]; + }; +}; + +/** + * Performs the transactions to deploy the token bridge core contracts + * + * For chain with custom gas token, it checks the custom gas token allowance + * token allowance and approve if necessary. + * + * Returns the token bridge core contracts. + * + * @param {CreateTokenBridgeParams} createTokenBridgeParams + * @param {String} createRollupParams.rollupOwner - The address of the rollup owner + * @param {Object} createRollupParams.rollupAddress - The address of the Rollup contract + * @param {Object} [createRollupParams.account] - The private key account to sign transactions + * @param {String} [createRollupParams.nativeTokenAddress=] - Optional + * If nativeTokenAddress is passed and different than zero address, deploy a token bridge with custom fee token. + * @param {Object} createRollupParams.parentChainPublicClient - The parent chain Viem Public Client + * @param {Object} createRollupParams.orbitChainPublicClient - The orbit chain Viem Public Client + * @param {String} [createRollupParams.tokenBridgeCreatorAddressOverride=] - Optional + * If tokenBridgeCreatorAddressOverride is passed, the address is overridden in the different transactions + * @param {Object} [createRollupParams.gasOverrides=] - {@link TransactionRequestGasOverrides} Optional + * Gas overrides for createTokenBridgePrepareTransactionRequest + * @param {Object} createRollupParams.retryableGasOverrides - {@link TokenBridgeRetryableGasOverrides} Optional + * Retryable gas overrides for createTokenBridgePrepareTransactionRequest + * @param {Object} createRollupParams.setWethGatewayGasOverrides - {@link SetWethGatewayRetryableGasOverrides} Optional + * Retryable gas overrides for createTokenBridgePrepareSetWethGatewayTransactionRequest + * + * @returns Promise<{@link CreateTokenBridgeResults}> + * + * @example + * const tokenBridgeCreator = await deployTokenBridgeCreator({ + * publicClient: l2Client, + * }); + * + * const tokenBridgeContracts = await createTokenBridge({ + * rollupOwner: rollupOwner.address, + * rollupAddress: rollupAddress, + * account: deployer, + * parentChainPublicClient: l1Client, + * orbitChainPublicClient: l2Client, + * tokenBridgeCreatorAddressOverride: tokenBridgeCreator, + * gasOverrides: { + * gasLimit: { + * base: 6_000_000n, + * }, + * }, + * retryableGasOverrides: { + * maxGasForFactory: { + * base: 20_000_000n, + * }, + * maxGasForContracts: { + * base: 20_000_000n, + * }, + * maxSubmissionCostForFactory: { + * base: 4_000_000_000_000n, + * }, + * maxSubmissionCostForContracts: { + * base: 4_000_000_000_000n, + * }, + * }, + * setWethGatewayGasOverrides: { + * gasLimit: { + * base: 100_000n, + * }, + * }, + * }); + */ +export async function createTokenBridge({ + rollupOwner, + rollupAddress, + account, + nativeTokenAddress, + parentChainPublicClient, + orbitChainPublicClient, + tokenBridgeCreatorAddressOverride, + gasOverrides, + retryableGasOverrides, + setWethGatewayGasOverrides, +}: CreateTokenBridgeParams): Promise { + const isCustomFeeTokenBridge = isCustomFeeTokenAddress(nativeTokenAddress); + if (isCustomFeeTokenBridge) { + // set the custom fee token + // prepare transaction to approve custom fee token spend + const allowanceParams: CreateTokenBridgeEnoughCustomFeeTokenAllowanceParams = { + nativeToken: nativeTokenAddress, + owner: account.address, + publicClient: parentChainPublicClient, + tokenBridgeCreatorAddressOverride, + }; + + // Check allowance and approve if necessary + if (!(await createTokenBridgeEnoughCustomFeeTokenAllowance(allowanceParams))) { + const approvalTxRequest = + await createTokenBridgePrepareCustomFeeTokenApprovalTransactionRequest(allowanceParams); + + // sign and send the transaction + const approvalTxHash = await parentChainPublicClient.sendRawTransaction({ + serializedTransaction: await account.signTransaction(approvalTxRequest), + }); + + // get the transaction receipt after waiting for the transaction to complete + const approvalTxReceipt = await parentChainPublicClient.waitForTransactionReceipt({ + hash: approvalTxHash, + }); + + console.log( + `Tokens approved in ${getBlockExplorerUrl(parentChainPublicClient.chain)}/tx/${ + approvalTxReceipt.transactionHash + }`, + ); + } + } + + // prepare the transaction for deploying the core contracts + const txRequest = await createTokenBridgePrepareTransactionRequest({ + params: { + rollup: rollupAddress, + rollupOwner, + }, + parentChainPublicClient, + orbitChainPublicClient, + account: account.address, + tokenBridgeCreatorAddressOverride, + gasOverrides, + retryableGasOverrides, + }); + + // sign and send the transaction + console.log(`Deploying the TokenBridge...`); + const txHash = await parentChainPublicClient.sendRawTransaction({ + serializedTransaction: await account.signTransaction(txRequest), + }); + + const transaction = await parentChainPublicClient.getTransaction({ hash: txHash }); + + // get the transaction receipt after waiting for the transaction to complete + const txReceipt = createTokenBridgePrepareTransactionReceipt( + await parentChainPublicClient.waitForTransactionReceipt({ hash: txHash }), + ); + console.log( + `Deployed in ${getBlockExplorerUrl(parentChainPublicClient.chain)}/tx/${ + txReceipt.transactionHash + }`, + ); + + // wait for retryables to execute + console.log(`Waiting for retryable tickets to execute on the Orbit chain...`); + const orbitChainRetryableReceipts = await txReceipt.waitForRetryables({ + orbitPublicClient: orbitChainPublicClient, + }); + console.log(`Retryables executed`); + console.log( + `Transaction hash for first retryable is ${orbitChainRetryableReceipts[0].transactionHash}`, + ); + console.log( + `Transaction hash for second retryable is ${orbitChainRetryableReceipts[1].transactionHash}`, + ); + + // fetching the TokenBridge contracts + const tokenBridgeContracts = await txReceipt.getTokenBridgeContracts({ + parentChainPublicClient, + }); + + // Non custom fee token + if (!isCustomFeeTokenBridge) { + // set weth gateway + const setWethGatewayTxRequest = await createTokenBridgePrepareSetWethGatewayTransactionRequest({ + rollup: rollupAddress, + parentChainPublicClient, + orbitChainPublicClient, + account: account.address, + tokenBridgeCreatorAddressOverride, + retryableGasOverrides: setWethGatewayGasOverrides, + }); + + // sign and send the transaction + const setWethGatewayTxHash = await parentChainPublicClient.sendRawTransaction({ + serializedTransaction: await account.signTransaction(setWethGatewayTxRequest), + }); + + const setWethGatewayTransaction = await parentChainPublicClient.getTransaction({ + hash: setWethGatewayTxHash, + }); + + // get the transaction receipt after waiting for the transaction to complete + const setWethGatewayTxReceipt = createTokenBridgePrepareSetWethGatewayTransactionReceipt( + await parentChainPublicClient.waitForTransactionReceipt({ hash: setWethGatewayTxHash }), + ); + + console.log( + `Weth gateway set in ${getBlockExplorerUrl(parentChainPublicClient.chain)}/tx/${ + setWethGatewayTxReceipt.transactionHash + }`, + ); + + // Wait for retryables to execute + const orbitChainSetWethGatewayRetryableReceipt = + await setWethGatewayTxReceipt.waitForRetryables({ + orbitPublicClient: orbitChainPublicClient, + }); + console.log(`Retryables executed`); + console.log( + `Transaction hash for retryable is ${orbitChainSetWethGatewayRetryableReceipt[0].transactionHash}`, + ); + + if (orbitChainSetWethGatewayRetryableReceipt[0].status !== 'success') { + throw new Error( + `Retryable status is not success: ${orbitChainSetWethGatewayRetryableReceipt[0].status}. Aborting...`, + ); + } + + return { + transaction, + transactionReceipt: txReceipt, + retryables: orbitChainRetryableReceipts, + tokenBridgeContracts, + setWethGateway: { + transaction: setWethGatewayTransaction, + transactionReceipt: setWethGatewayTxReceipt, + retryables: [orbitChainSetWethGatewayRetryableReceipt[0]], + }, + }; + } + + return { + transaction, + transactionReceipt: txReceipt, + retryables: orbitChainRetryableReceipts, + tokenBridgeContracts, + }; +} diff --git a/src/createTokenBridgePrepareTransactionReceipt.ts b/src/createTokenBridgePrepareTransactionReceipt.ts index bfb120ea..faca61a9 100644 --- a/src/createTokenBridgePrepareTransactionReceipt.ts +++ b/src/createTokenBridgePrepareTransactionReceipt.ts @@ -58,16 +58,18 @@ type GetTokenBridgeContractsParameters = { }; export type CreateTokenBridgeTransactionReceipt = TransactionReceipt & { - waitForRetryables(params: WaitForRetryablesParameters): Promise; - getTokenBridgeContracts(): TokenBridgeContracts; + waitForRetryables(params: WaitForRetryablesParameters): Promise; + getTokenBridgeContracts( + parentChainPublicClient: GetTokenBridgeContractsParameters, + ): Promise; }; -export function createTokenBridgePrepareTransactionReceipt(txReceipt: TransactionReceipt) { +export function createTokenBridgePrepareTransactionReceipt( + txReceipt: TransactionReceipt, +): CreateTokenBridgeTransactionReceipt { return { ...txReceipt, - waitForRetryables: async function ({ - orbitPublicClient, - }: WaitForRetryablesParameters): Promise { + waitForRetryables: async function ({ orbitPublicClient }) { const ethersTxReceipt = viemTransactionReceiptToEthersTransactionReceipt(txReceipt); const l1TxReceipt = new L1TransactionReceipt(ethersTxReceipt); const orbitProvider = publicClientToProvider(orbitPublicClient); @@ -95,9 +97,7 @@ export function createTokenBridgePrepareTransactionReceipt(txReceipt: Transactio ) as WaitForRetryablesResult ); }, - getTokenBridgeContracts: async function ({ - parentChainPublicClient, - }: GetTokenBridgeContractsParameters): Promise { + getTokenBridgeContracts: async function ({ parentChainPublicClient }) { const eventLog = findOrbitTokenBridgeCreatedEventLog(txReceipt); const decodedEventLog = decodeOrbitTokenBridgeCreatedEventLog(eventLog); const { inbox } = decodedEventLog.args; diff --git a/src/index.ts b/src/index.ts index 494d6aab..538c64a8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -51,6 +51,11 @@ import { ParentChain, ParentChainId } from './types/ParentChain'; import { NodeConfig } from './types/NodeConfig.generated'; import { NodeConfigChainInfoJson } from './types/NodeConfig'; import { prepareNodeConfig } from './prepareNodeConfig'; +import { + CreateTokenBridgeParams, + CreateTokenBridgeResults, + createTokenBridge, +} from './createTokenBridge'; import { createTokenBridgeEnoughCustomFeeTokenAllowance, CreateTokenBridgeEnoughCustomFeeTokenAllowanceParams, @@ -117,6 +122,9 @@ export { prepareKeyset, utils, // + CreateTokenBridgeParams, + CreateTokenBridgeResults, + createTokenBridge, createTokenBridgeEnoughCustomFeeTokenAllowance, CreateTokenBridgeEnoughCustomFeeTokenAllowanceParams, createTokenBridgePrepareCustomFeeTokenApprovalTransactionRequest,