diff --git a/.github/workflows/sbom-export.yaml b/.github/workflows/sbom-export.yaml index 0c4824b5..7a3f212f 100644 --- a/.github/workflows/sbom-export.yaml +++ b/.github/workflows/sbom-export.yaml @@ -2,7 +2,7 @@ name: SBOM Export & Centralize on: push: - branches: [ "main" ] + branches: ['main'] schedule: - cron: '36 8 * * 1' diff --git a/audit-ci.jsonc b/audit-ci.jsonc index ed951562..3d70e840 100644 --- a/audit-ci.jsonc +++ b/audit-ci.jsonc @@ -70,7 +70,43 @@ // elliptic has no patched release yet (affected: <=6.6.1) // transitive via ethereumjs-util -> ethereum-cryptography -> secp256k1 -> elliptic // from: @safe-global/protocol-kit>ethereumjs-util>ethereum-cryptography>secp256k1>elliptic - "GHSA-848j-6mx2-7j84" + "GHSA-848j-6mx2-7j84", + // flatted + //////////// + // https://github.com/advisories/GHSA-25h7-pfq9-p65f + // flatted ReDoS - transitive dev dep via eslint>file-entry-cache>flat-cache>flatted + // not a runtime dependency + "GHSA-25h7-pfq9-p65f", + // https://github.com/advisories/GHSA-rf6f-7fwh-wjgh + // flatted DoS - transitive dev dep via eslint>file-entry-cache>flat-cache>flatted + // not a runtime dependency + "GHSA-rf6f-7fwh-wjgh", + + // picomatch + //////////// + // https://github.com/advisories/GHSA-3v7f-55p6-f55p + // picomatch method injection - transitive dev dep via @wagmi/cli>chokidar and typescript-eslint + // not a runtime dependency + "GHSA-3v7f-55p6-f55p", + // https://github.com/advisories/GHSA-c2c7-rcm5-vvqj + // picomatch ReDoS - transitive dev dep via @wagmi/cli>chokidar and typescript-eslint + // not a runtime dependency + "GHSA-c2c7-rcm5-vvqj", + + // brace-expansion + //////////// + // https://github.com/advisories/GHSA-f886-m6hf-6m8v + // brace-expansion infinite loop - transitive dev dep via eslint>minimatch, ts-morph, typescript-eslint + // not a runtime dependency + "GHSA-f886-m6hf-6m8v", + + // yaml + //////////// + // https://github.com/advisories/GHSA-48c2-rrv3-qjmp + // yaml stack overflow via deeply nested collections + // transitive dev dep via patch-package>yaml and @arbitrum/nitro-contracts>patch-package>yaml + // not a runtime dependency + "GHSA-48c2-rrv3-qjmp" ] } diff --git a/eslint.config.js b/eslint.config.js index 12b1a38d..1f716647 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,9 +1,9 @@ -import { defineConfig } from 'eslint/config' -import tseslint from 'typescript-eslint' +import { defineConfig } from 'eslint/config'; +import tseslint from 'typescript-eslint'; export default defineConfig( { ignores: ['node_modules/**', 'src/dist/**', 'coverage/**'], }, ...tseslint.configs.recommended, -) +); diff --git a/src/calculateRetryableSubmissionFee.ts b/src/calculateRetryableSubmissionFee.ts new file mode 100644 index 00000000..fb8d9a97 --- /dev/null +++ b/src/calculateRetryableSubmissionFee.ts @@ -0,0 +1,28 @@ +import { Address, PublicClient, Transport, Chain, parseAbi } from 'viem'; + +// Matches DEFAULT_SUBMISSION_FEE_PERCENT_INCREASE in @arbitrum/sdk's ParentToChildMessageGasEstimator +const SUBMISSION_FEE_PERCENT_INCREASE = 300n; + +const inboxABI = parseAbi([ + 'function calculateRetryableSubmissionFee(uint256 dataLength, uint256 baseFee) view returns (uint256)', +]); + +export async function calculateRetryableSubmissionFee( + parentChainPublicClient: PublicClient, + inbox: Address, + dataLength: bigint, +): Promise { + const block = await parentChainPublicClient.getBlock(); + if (!block.baseFeePerGas) { + throw new Error('Latest block did not contain base fee'); + } + + const submissionFee = await parentChainPublicClient.readContract({ + address: inbox, + abi: inboxABI, + functionName: 'calculateRetryableSubmissionFee', + args: [dataLength, block.baseFeePerGas], + }); + + return submissionFee + (submissionFee * SUBMISSION_FEE_PERCENT_INCREASE) / 100n; +} diff --git a/src/constants.ts b/src/constants.ts index 09bbd19f..dc6554c1 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,4 +1,4 @@ -import { parseEther } from 'viem'; +import { parseEther, parseGwei } from 'viem'; /** * Approximate value necessary to pay for retryables fees for `createRollup`. @@ -9,3 +9,12 @@ export const createRollupDefaultRetryablesFees = parseEther('0.125'); * Approximate value necessary to pay for retryables fees for `createTokenBridge`. */ export const createTokenBridgeDefaultRetryablesFees = parseEther('0.02'); + +/** + * 0.1 gwei is a standard default to start the chain with. here we double that for some margin + */ +export const enqueueDefaultMaxGasPrice = parseGwei('0.2'); + +// ~30% headroom over observed gas usage for token bridge retryables +export const enqueueDefaultMaxGasForContracts = 20_000_000n; +export const enqueueDefaultGasLimitForWethGateway = 100_000n; diff --git a/src/contracts/GatewayRouter.ts b/src/contracts/GatewayRouter.ts new file mode 100644 index 00000000..c5c438c0 --- /dev/null +++ b/src/contracts/GatewayRouter.ts @@ -0,0 +1,60 @@ +export const gatewayRouterABI = [ + { + inputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + name: 'l1TokenToGateway', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address[]', + name: '_token', + type: 'address[]', + }, + { + internalType: 'address[]', + name: '_gateway', + type: 'address[]', + }, + { + internalType: 'uint256', + name: '_maxGas', + type: 'uint256', + }, + { + internalType: 'uint256', + name: '_gasPriceBid', + type: 'uint256', + }, + { + internalType: 'uint256', + name: '_maxSubmissionCost', + type: 'uint256', + }, + ], + name: 'setGateways', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'payable', + type: 'function', + }, +] as const; diff --git a/src/contracts/UpgradeExecutor.ts b/src/contracts/UpgradeExecutor.ts index b58201f2..3f39ca13 100644 --- a/src/contracts/UpgradeExecutor.ts +++ b/src/contracts/UpgradeExecutor.ts @@ -1,8 +1,8 @@ import { parseAbi } from 'viem'; export const upgradeExecutorABI = parseAbi([ - 'function execute(address upgrade, bytes upgradeCallData)', - 'function executeCall(address target, bytes targetCallData)', + 'function execute(address upgrade, bytes upgradeCallData) payable', + 'function executeCall(address target, bytes targetCallData) payable', 'function hasRole(bytes32 role, address account) public view returns (bool)', 'function grantRole(bytes32 role, address account)', 'function revokeRole(bytes32 role, address account)', diff --git a/src/createTokenBridge-ethers.ts b/src/createTokenBridge-ethers.ts index 48a80994..140654b0 100644 --- a/src/createTokenBridge-ethers.ts +++ b/src/createTokenBridge-ethers.ts @@ -35,7 +35,6 @@ export async function createTokenBridgeGetInputs< TParentChain extends Chain | undefined, TOrbitChain extends Chain | undefined, >( - l1DeployerAddress: string, l1PublicClient: PublicClient, l2PublicClient: PublicClient, l1TokenBridgeCreatorAddress: string, @@ -51,22 +50,12 @@ export async function createTokenBridgeGetInputs< const { maxSubmissionCost: maxSubmissionCostForFactoryEstimation, maxGas: maxGasForFactoryEstimation, - } = await getEstimateForDeployingFactory( - l1DeployerAddress, - l1TokenBridgeCreatorAddress, - l1Provider, - l2Provider, - ); + } = await getEstimateForDeployingFactory(l1TokenBridgeCreatorAddress, l1Provider, l2Provider); const { maxSubmissionCost: maxSubmissionCostForContractsEstimation, maxGas: maxGasForContractsEstimation, - } = await getEstimateForDeployingContracts( - l1DeployerAddress, - l1TokenBridgeCreatorAddress, - l1Provider, - l2Provider, - ); + } = await getEstimateForDeployingContracts(l1TokenBridgeCreatorAddress, l1Provider, l2Provider); //// apply gas overrides const maxSubmissionCostForFactory = @@ -138,65 +127,23 @@ export async function createTokenBridgeGetInputs< }; } -const getEstimateForDeployingFactory = async ( - l1DeployerAddress: string, - l1TokenBridgeCreatorAddress: string, - l1Provider: ethers.providers.Provider, - l2Provider: ethers.providers.Provider, -): Promise<{ - maxSubmissionCost: BigNumber; - maxGas: BigNumber; -}> => { - const L1AtomicTokenBridgeCreator__factory = new ethers.Contract( - l1TokenBridgeCreatorAddress, - L1AtomicTokenBridgeCreator.abi, - ); - const l1TokenBridgeCreator = L1AtomicTokenBridgeCreator__factory.connect(l1Provider); - - //// run retryable estimate for deploying L2 factory - const l1ToL2MsgGasEstimate = new ParentToChildMessageGasEstimator(l2Provider); - - const { maxSubmissionCost } = await l1ToL2MsgGasEstimate.estimateAll( - { - from: ethers.Wallet.createRandom().address, - to: ethers.constants.AddressZero, - l2CallValue: BigNumber.from(0), - excessFeeRefundAddress: l1DeployerAddress, - callValueRefundAddress: l1DeployerAddress, - data: L2AtomicTokenBridgeFactory__factory.bytecode, - }, - await getBaseFee(l1Provider), - l1Provider, - ); - - const maxGas = (await l1TokenBridgeCreator.gasLimitForL2FactoryDeployment()) as BigNumber; - - return { - // there's already a 300% increase buffer in the SDK - // https://github.com/OffchainLabs/arbitrum-sdk/blob/main/src/lib/message/ParentToChildMessageGasEstimator.ts#L27 - maxSubmissionCost, - maxGas, - }; -}; +export function getFactoryDeploymentDataSize(): number { + return ethers.utils.hexDataLength(L2AtomicTokenBridgeFactory__factory.bytecode); +} -async function getEstimateForDeployingContracts( - l1DeployerAddress: string, +export async function getContractsDeploymentData( l1TokenBridgeCreatorAddress: string, l1Provider: ethers.providers.Provider, - l2Provider: ethers.providers.Provider, -): Promise<{ - maxSubmissionCost: BigNumber; - maxGas: BigNumber; -}> { - const L1AtomicTokenBridgeCreator__factory = new ethers.Contract( +) { + const l1TokenBridgeCreator = new ethers.Contract( l1TokenBridgeCreatorAddress, L1AtomicTokenBridgeCreator.abi, - ); - const l1TokenBridgeCreator = L1AtomicTokenBridgeCreator__factory.connect(l1Provider); + ).connect(l1Provider); const l2FactoryTemplate = L2AtomicTokenBridgeFactory__factory.attach( await l1TokenBridgeCreator.l2TokenBridgeFactoryTemplate(), ).connect(l1Provider); + const l2Code = { router: await l1Provider.getCode(await l1TokenBridgeCreator.l2RouterTemplate()), standardGateway: await l1Provider.getCode( @@ -213,8 +160,6 @@ async function getEstimateForDeployingContracts( multicall: await l1Provider.getCode(await l1TokenBridgeCreator.l2MulticallTemplate()), }; - const l1ToL2MsgGasEstimate = new ParentToChildMessageGasEstimator(l2Provider); - const calldata = l2FactoryTemplate.interface.encodeFunctionData('deployL2Contracts', [ l2Code, ethers.Wallet.createRandom().address, @@ -227,10 +172,58 @@ async function getEstimateForDeployingContracts( ethers.Wallet.createRandom().address, ]); + return { + dataSize: ethers.utils.hexDataLength(calldata), + l2Code, + l2FactoryTemplate, + }; +} + +const getEstimateForDeployingFactory = async ( + l1TokenBridgeCreatorAddress: string, + l1Provider: ethers.providers.Provider, + l2Provider: ethers.providers.Provider, +): Promise<{ + maxSubmissionCost: BigNumber; + maxGas: BigNumber; +}> => { + const l1TokenBridgeCreator = new ethers.Contract( + l1TokenBridgeCreatorAddress, + L1AtomicTokenBridgeCreator.abi, + ).connect(l1Provider); + + const l1ToL2MsgGasEstimate = new ParentToChildMessageGasEstimator(l2Provider); + // 300% increase buffer is applied by the SDK + const maxSubmissionCost = await l1ToL2MsgGasEstimate.estimateSubmissionFee( + l1Provider, + await getBaseFee(l1Provider), + getFactoryDeploymentDataSize(), + ); + + const maxGas = (await l1TokenBridgeCreator.gasLimitForL2FactoryDeployment()) as BigNumber; + + return { maxSubmissionCost, maxGas }; +}; + +async function getEstimateForDeployingContracts( + l1TokenBridgeCreatorAddress: string, + l1Provider: ethers.providers.Provider, + l2Provider: ethers.providers.Provider, +): Promise<{ + maxSubmissionCost: BigNumber; + maxGas: BigNumber; +}> { + const { dataSize, l2Code, l2FactoryTemplate } = await getContractsDeploymentData( + l1TokenBridgeCreatorAddress, + l1Provider, + ); + + const l1ToL2MsgGasEstimate = new ParentToChildMessageGasEstimator(l2Provider); + // 300% increase buffer is applied by the SDK const maxSubmissionCost = await l1ToL2MsgGasEstimate.estimateSubmissionFee( l1Provider, await l1Provider.getGasPrice(), - ethers.utils.hexDataLength(calldata), + dataSize, ); const maxGas = await l2FactoryTemplate.estimateGas.deployL2Contracts( @@ -246,8 +239,6 @@ async function getEstimateForDeployingContracts( ); return { - // there's already a 300% increase buffer in the SDK - // https://github.com/OffchainLabs/arbitrum-sdk/blob/main/src/lib/message/ParentToChildMessageGasEstimator.ts#L27 maxSubmissionCost, maxGas: maxGas.mul(2), }; diff --git a/src/createTokenBridgePrepareSetWethGatewayTransactionRequest.ts b/src/createTokenBridgePrepareSetWethGatewayTransactionRequest.ts index e201fd56..3d50b913 100644 --- a/src/createTokenBridgePrepareSetWethGatewayTransactionRequest.ts +++ b/src/createTokenBridgePrepareSetWethGatewayTransactionRequest.ts @@ -5,6 +5,7 @@ import { upgradeExecutorEncodeFunctionData } from './upgradeExecutorEncodeFuncti import { createTokenBridgeFetchTokenBridgeContracts } from './createTokenBridgeFetchTokenBridgeContracts'; import { createRollupFetchCoreContracts } from './createRollupFetchCoreContracts'; import { getEstimateForSettingGateway } from './createTokenBridge-ethers'; +import { gatewayRouterABI } from './contracts/GatewayRouter'; import { GasOverrideOptions, applyPercentIncrease } from './utils/gasOverrides'; import { Prettify } from './types/utils'; import { validateParentChain } from './types/ParentChain'; @@ -41,67 +42,6 @@ export type CreateTokenBridgePrepareRegisterWethGatewayTransactionRequestParams< }> >; -const parentChainGatewayRouterAbi = [ - { - inputs: [ - { - internalType: 'address', - name: '', - type: 'address', - }, - ], - name: 'l1TokenToGateway', - outputs: [ - { - internalType: 'address', - name: '', - type: 'address', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address[]', - name: '_token', - type: 'address[]', - }, - { - internalType: 'address[]', - name: '_gateway', - type: 'address[]', - }, - { - internalType: 'uint256', - name: '_maxGas', - type: 'uint256', - }, - { - internalType: 'uint256', - name: '_gasPriceBid', - type: 'uint256', - }, - { - internalType: 'uint256', - name: '_maxSubmissionCost', - type: 'uint256', - }, - ], - name: 'setGateways', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - stateMutability: 'payable', - type: 'function', - }, -]; - export async function createTokenBridgePrepareSetWethGatewayTransactionRequest< TParentChain extends Chain | undefined, TOrbitChain extends Chain | undefined, @@ -150,7 +90,7 @@ export async function createTokenBridgePrepareSetWethGatewayTransactionRequest< // check whether the weth gateway is already registered in the router const registeredWethGateway = await parentChainPublicClient.readContract({ address: tokenBridgeContracts.parentChainContracts.router, - abi: parentChainGatewayRouterAbi, + abi: gatewayRouterABI, functionName: 'l1TokenToGateway', args: [tokenBridgeContracts.parentChainContracts.weth], }); @@ -167,7 +107,7 @@ export async function createTokenBridgePrepareSetWethGatewayTransactionRequest< // encode data for the setGateways call // (we first encode dummy data, to get the retryable message estimates) const setGatewaysDummyCalldata = encodeFunctionData({ - abi: parentChainGatewayRouterAbi, + abi: gatewayRouterABI, functionName: 'setGateways', args: [ [tokenBridgeContracts.parentChainContracts.weth], @@ -217,7 +157,7 @@ export async function createTokenBridgePrepareSetWethGatewayTransactionRequest< // (and then we encode the real data, to send the transaction) const setGatewaysCalldata = encodeFunctionData({ - abi: parentChainGatewayRouterAbi, + abi: gatewayRouterABI, functionName: 'setGateways', args: [ [tokenBridgeContracts.parentChainContracts.weth], diff --git a/src/createTokenBridgePrepareTransactionRequest.ts b/src/createTokenBridgePrepareTransactionRequest.ts index 6f72877b..d811d0de 100644 --- a/src/createTokenBridgePrepareTransactionRequest.ts +++ b/src/createTokenBridgePrepareTransactionRequest.ts @@ -66,7 +66,6 @@ export async function createTokenBridgePrepareTransactionRequest< tokenBridgeCreatorAddressOverride ?? getTokenBridgeCreatorAddress(parentChainPublicClient); const { inbox, maxGasForContracts, maxGasPrice, retryableFee } = await createTokenBridgeGetInputs( - account, parentChainPublicClient, orbitChainPublicClient, tokenBridgeCreatorAddress, diff --git a/src/enqueueTokenBridge.integration.test.ts b/src/enqueueTokenBridge.integration.test.ts new file mode 100644 index 00000000..2f90203c --- /dev/null +++ b/src/enqueueTokenBridge.integration.test.ts @@ -0,0 +1,164 @@ +import { describe, it, expect } from 'vitest'; +import { createPublicClient, http, zeroAddress, parseAbi } from 'viem'; + +import { nitroTestnodeL1, nitroTestnodeL2 } from './chains'; +import { getInformationFromTestnode, getNitroTestnodePrivateKeyAccounts } from './testHelpers'; +import { enqueueTokenBridgePrepareTransactionRequest } from './enqueueTokenBridgePrepareTransactionRequest'; +import { createTokenBridgePrepareTransactionReceipt } from './createTokenBridgePrepareTransactionReceipt'; +import { deployTokenBridgeCreator } from './createTokenBridge-testHelpers'; +import { enqueueTokenBridgePrepareSetWethGatewayTransactionRequest } from './enqueueTokenBridgePrepareSetWethGatewayTransactionRequest'; +import { createTokenBridgePrepareSetWethGatewayTransactionReceipt } from './createTokenBridgePrepareSetWethGatewayTransactionReceipt'; +import { TokenBridgeContracts } from './types/TokenBridgeContracts'; +import { registerNewNetwork } from './utils/registerNewNetwork'; +import { publicClientToProvider } from './ethers-compat/publicClientToProvider'; + +const testnodeAccounts = getNitroTestnodePrivateKeyAccounts(); +const l2RollupOwner = testnodeAccounts.l2RollupOwner; + +const nitroTestnodeL1Client = createPublicClient({ + chain: nitroTestnodeL1, + transport: http(nitroTestnodeL1.rpcUrls.default.http[0]), +}); + +const nitroTestnodeL2Client = createPublicClient({ + chain: nitroTestnodeL2, + transport: http(nitroTestnodeL2.rpcUrls.default.http[0]), +}); + +function checkTokenBridgeContracts(tokenBridgeContracts: TokenBridgeContracts) { + expect(Object.keys(tokenBridgeContracts)).toHaveLength(2); + + // 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); + + // 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); +} + +async function checkWethGateways(tokenBridgeContracts: TokenBridgeContracts) { + // verify weth gateway (parent chain) + const registeredWethGatewayOnParentChain = await nitroTestnodeL1Client.readContract({ + address: tokenBridgeContracts.parentChainContracts.router, + abi: parseAbi(['function l1TokenToGateway(address) view returns (address)']), + functionName: 'l1TokenToGateway', + args: [tokenBridgeContracts.parentChainContracts.weth], + }); + expect(registeredWethGatewayOnParentChain).toEqual( + tokenBridgeContracts.parentChainContracts.wethGateway, + ); + + // verify weth gateway (orbit chain) + // Note: we pass the address of the token on the parent chain when asking for the registered gateway on the orbit chain + const registeredWethGatewayOnOrbitChain = await nitroTestnodeL2Client.readContract({ + address: tokenBridgeContracts.orbitChainContracts.router, + abi: parseAbi(['function l1TokenToGateway(address) view returns (address)']), + functionName: 'l1TokenToGateway', + args: [tokenBridgeContracts.parentChainContracts.weth], + }); + expect(registeredWethGatewayOnOrbitChain).toEqual( + tokenBridgeContracts.orbitChainContracts.wethGateway, + ); + + 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); +} + +describe('enqueueTokenBridge', () => { + 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 enqueueTokenBridgePrepareTransactionRequest({ + params: { + rollup: testnodeInformation.rollup, + rollupOwner: l2RollupOwner.address, + }, + parentChainPublicClient: nitroTestnodeL1Client, + account: l2RollupOwner.address, + gasOverrides: { + gasLimit: { + base: 6_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'); + + // register the orbit chain network with @arbitrum/sdk (needed for waitForRetryables) + await registerNewNetwork( + publicClientToProvider(nitroTestnodeL1Client), + publicClientToProvider(nitroTestnodeL2Client), + testnodeInformation.rollup, + ); + + // 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 enqueueTokenBridgePrepareSetWethGatewayTransactionRequest( + { + rollup: testnodeInformation.rollup, + parentChainPublicClient: nitroTestnodeL1Client, + account: l2RollupOwner.address, + 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'); + + await checkWethGateways(tokenBridgeContracts); + }); +}); diff --git a/src/enqueueTokenBridgePrepareSetWethGatewayTransactionRequest.ts b/src/enqueueTokenBridgePrepareSetWethGatewayTransactionRequest.ts new file mode 100644 index 00000000..d0322200 --- /dev/null +++ b/src/enqueueTokenBridgePrepareSetWethGatewayTransactionRequest.ts @@ -0,0 +1,139 @@ +import { Address, PublicClient, Transport, Chain, encodeFunctionData, parseAbi } from 'viem'; + +import { validateParentChain } from './types/ParentChain'; +import { isCustomFeeTokenChain } from './utils/isCustomFeeTokenChain'; +import { createTokenBridgeFetchTokenBridgeContracts } from './createTokenBridgeFetchTokenBridgeContracts'; +import { createRollupFetchCoreContracts } from './createRollupFetchCoreContracts'; +import { upgradeExecutorEncodeFunctionData } from './upgradeExecutorEncodeFunctionData'; +import { gatewayRouterABI } from './contracts/GatewayRouter'; +import { Prettify } from './types/utils'; +import { WithTokenBridgeCreatorAddressOverride } from './types/createTokenBridgeTypes'; +import { enqueueDefaultMaxGasPrice, enqueueDefaultGasLimitForWethGateway } from './constants'; +import { calculateRetryableSubmissionFee } from './calculateRetryableSubmissionFee'; + +export type EnqueueTokenBridgePrepareSetWethGatewayTransactionRequestParams< + TParentChain extends Chain | undefined, +> = Prettify< + WithTokenBridgeCreatorAddressOverride<{ + /** + * Address of the Rollup contract. + */ + rollup: Address; + account: Address; + /** + * Number of the block in which the Rollup contract was deployed. + * + * This parameter is used to reduce the span of blocks to query, so it doesn't have to be exactly the right block number. + * However, for the query to work properly, it has to be **less than or equal to** the right block number. + */ + rollupDeploymentBlockNumber?: bigint; + parentChainPublicClient: PublicClient; + gasLimit?: bigint; + maxGasPrice?: bigint; + }> +>; + +/** + * Prepares the transaction to register the WETH gateway on the parent chain router via the + * UpgradeExecutor. Must be called after the `enqueueTokenBridgePrepareTransactionRequest` transaction + * has confirmed on the parent chain. Unlike {@link createTokenBridgePrepareSetWethGatewayTransactionRequest}, + * this function does not require an orbit chain connection -- retryable gas parameters are estimated + * from parent chain state. + */ +export async function enqueueTokenBridgePrepareSetWethGatewayTransactionRequest< + TParentChain extends Chain | undefined, +>({ + rollup, + account, + rollupDeploymentBlockNumber, + parentChainPublicClient, + gasLimit = enqueueDefaultGasLimitForWethGateway, + maxGasPrice = enqueueDefaultMaxGasPrice, + tokenBridgeCreatorAddressOverride, +}: EnqueueTokenBridgePrepareSetWethGatewayTransactionRequestParams) { + const { chainId } = validateParentChain(parentChainPublicClient); + + if ( + await isCustomFeeTokenChain({ + rollup, + parentChainPublicClient, + }) + ) { + throw new Error('chain is custom fee token chain, no need to register the weth gateway.'); + } + + const inbox = await parentChainPublicClient.readContract({ + address: rollup, + abi: parseAbi(['function inbox() view returns (address)']), + functionName: 'inbox', + }); + + const tokenBridgeContracts = await createTokenBridgeFetchTokenBridgeContracts({ + inbox, + parentChainPublicClient, + tokenBridgeCreatorAddressOverride, + }); + + const registeredWethGateway = await parentChainPublicClient.readContract({ + address: tokenBridgeContracts.parentChainContracts.router, + abi: gatewayRouterABI, + functionName: 'l1TokenToGateway', + args: [tokenBridgeContracts.parentChainContracts.weth], + }); + if (registeredWethGateway === tokenBridgeContracts.parentChainContracts.wethGateway) { + throw new Error('weth gateway is already registered in the router.'); + } + + const rollupCoreContracts = await createRollupFetchCoreContracts({ + rollup, + rollupDeploymentBlockNumber, + publicClient: parentChainPublicClient, + }); + + // Encode with placeholder values to measure data size (uint256 values are always 32 bytes in ABI encoding) + const dummyCalldata = encodeFunctionData({ + abi: gatewayRouterABI, + functionName: 'setGateways', + args: [ + [tokenBridgeContracts.parentChainContracts.weth], + [tokenBridgeContracts.parentChainContracts.wethGateway], + 0n, + 0n, + 0n, + ], + }); + const calldataSize = BigInt((dummyCalldata.length - 2) / 2); + const maxSubmissionCost = await calculateRetryableSubmissionFee( + parentChainPublicClient, + inbox, + calldataSize, + ); + + const deposit = gasLimit * maxGasPrice + maxSubmissionCost; + + const setGatewaysCalldata = encodeFunctionData({ + abi: gatewayRouterABI, + functionName: 'setGateways', + args: [ + [tokenBridgeContracts.parentChainContracts.weth], + [tokenBridgeContracts.parentChainContracts.wethGateway], + gasLimit, // _maxGas + maxGasPrice, // _gasPriceBid + maxSubmissionCost, // _maxSubmissionCost + ], + }); + + // @ts-expect-error -- todo: fix viem type issue + const request = await parentChainPublicClient.prepareTransactionRequest({ + chain: parentChainPublicClient.chain, + to: rollupCoreContracts.upgradeExecutor, + data: upgradeExecutorEncodeFunctionData({ + functionName: 'executeCall', + args: [tokenBridgeContracts.parentChainContracts.router, setGatewaysCalldata], + }), + value: deposit, + account, + }); + + return { ...request, chainId }; +} diff --git a/src/enqueueTokenBridgePrepareTransactionRequest.ts b/src/enqueueTokenBridgePrepareTransactionRequest.ts new file mode 100644 index 00000000..d47a47bb --- /dev/null +++ b/src/enqueueTokenBridgePrepareTransactionRequest.ts @@ -0,0 +1,135 @@ +import { Address, PublicClient, Transport, Chain, encodeFunctionData, zeroAddress } from 'viem'; + +import { tokenBridgeCreatorABI } from './contracts/TokenBridgeCreator'; +import { rollupABI } from './contracts/Rollup'; +import { validateParentChain } from './types/ParentChain'; +import { isCustomFeeTokenChain } from './utils/isCustomFeeTokenChain'; +import { TransactionRequestGasOverrides, applyPercentIncrease } from './utils/gasOverrides'; +import { Prettify } from './types/utils'; +import { WithTokenBridgeCreatorAddressOverride } from './types/createTokenBridgeTypes'; +import { getTokenBridgeCreatorAddress } from './utils/getTokenBridgeCreatorAddress'; +import { enqueueDefaultMaxGasPrice, enqueueDefaultMaxGasForContracts } from './constants'; +import { + getFactoryDeploymentDataSize, + getContractsDeploymentData, +} from './createTokenBridge-ethers'; +import { publicClientToProvider } from './ethers-compat/publicClientToProvider'; +import { calculateRetryableSubmissionFee } from './calculateRetryableSubmissionFee'; + +export type EnqueueTokenBridgePrepareTransactionRequestParams< + TParentChain extends Chain | undefined, +> = Prettify< + WithTokenBridgeCreatorAddressOverride<{ + params: { rollup: Address; rollupOwner: Address }; + account: Address; + parentChainPublicClient: PublicClient; + maxGasForContracts?: bigint; + maxGasForFactory?: bigint; + maxGasPrice?: bigint; + gasOverrides?: TransactionRequestGasOverrides; + }> +>; + +/** + * Prepares the transaction to deploy token bridge contracts via `TokenBridgeCreator.createTokenBridge`. + * The parent chain transaction creates retryable tickets that execute on the orbit chain when it + * processes its inbox. Unlike {@link createTokenBridgePrepareTransactionRequest}, this function + * does not require an orbit chain connection -- retryable gas parameters are estimated from + * parent chain state. + */ +export async function enqueueTokenBridgePrepareTransactionRequest< + TParentChain extends Chain | undefined, +>({ + params, + account, + parentChainPublicClient, + maxGasForContracts = enqueueDefaultMaxGasForContracts, + maxGasForFactory: maxGasForFactoryOverride, + maxGasPrice = enqueueDefaultMaxGasPrice, + gasOverrides, + tokenBridgeCreatorAddressOverride, +}: EnqueueTokenBridgePrepareTransactionRequestParams) { + const { chainId } = validateParentChain(parentChainPublicClient); + + const tokenBridgeCreatorAddress = + tokenBridgeCreatorAddressOverride ?? getTokenBridgeCreatorAddress(parentChainPublicClient); + + const inbox = await parentChainPublicClient.readContract({ + address: params.rollup, + abi: rollupABI, + functionName: 'inbox', + }); + + // Parent-chain-only idempotency check (no orbit chain client available in this flow). + // If router is non-zero, a prior createTokenBridge tx has executed on the parent chain. + // If the L2 retryables failed, they should be manually redeemed rather than redeployed. + const [router] = await parentChainPublicClient.readContract({ + address: tokenBridgeCreatorAddress, + abi: tokenBridgeCreatorABI, + functionName: 'inboxToL2Deployment', + args: [inbox], + }); + if (router !== zeroAddress) { + throw new Error(`Token bridge contracts for Rollup ${params.rollup} are already deployed`); + } + + const maxGasForFactory = + maxGasForFactoryOverride ?? + (await parentChainPublicClient.readContract({ + address: tokenBridgeCreatorAddress, + abi: tokenBridgeCreatorABI, + functionName: 'gasLimitForL2FactoryDeployment', + })); + + const l1Provider = publicClientToProvider(parentChainPublicClient); + const { dataSize: contractsDataSize } = await getContractsDeploymentData( + tokenBridgeCreatorAddress, + l1Provider, + ); + + const [maxSubmissionCostForFactory, maxSubmissionCostForContracts] = await Promise.all([ + calculateRetryableSubmissionFee( + parentChainPublicClient, + inbox, + BigInt(getFactoryDeploymentDataSize()), + ), + calculateRetryableSubmissionFee(parentChainPublicClient, inbox, BigInt(contractsDataSize)), + ]); + + const chainUsesCustomFee = await isCustomFeeTokenChain({ + rollup: params.rollup, + parentChainPublicClient, + }); + + const retryableFee = + maxSubmissionCostForFactory + + maxSubmissionCostForContracts + + maxGasPrice * (maxGasForContracts + maxGasForFactory); + + // @ts-expect-error -- todo: fix viem type issue + const request = await parentChainPublicClient.prepareTransactionRequest({ + chain: parentChainPublicClient.chain, + to: tokenBridgeCreatorAddress, + data: encodeFunctionData({ + abi: tokenBridgeCreatorABI, + functionName: 'createTokenBridge', + args: [inbox, params.rollupOwner, maxGasForContracts, maxGasPrice], + }), + value: chainUsesCustomFee ? 0n : retryableFee, + account, + // if the base gas limit override was provided, hardcode gas to 0 to skip estimation + // we'll set the actual value in the code below + gas: typeof gasOverrides?.gasLimit?.base !== 'undefined' ? 0n : undefined, + }); + + // potential gas overrides (gas limit) + if (gasOverrides && gasOverrides.gasLimit) { + request.gas = applyPercentIncrease({ + // the ! is here because we should let it error in case we don't have the estimated gas + base: gasOverrides.gasLimit.base ?? request.gas!, + percentIncrease: gasOverrides.gasLimit.percentIncrease, + }); + } + + return { ...request, chainId }; +} diff --git a/src/index.ts b/src/index.ts index e05d0fe4..c9cce085 100644 --- a/src/index.ts +++ b/src/index.ts @@ -178,7 +178,18 @@ import { import { createRollupDefaultRetryablesFees, createTokenBridgeDefaultRetryablesFees, + enqueueDefaultMaxGasPrice, + enqueueDefaultMaxGasForContracts, + enqueueDefaultGasLimitForWethGateway, } from './constants'; +import { + enqueueTokenBridgePrepareTransactionRequest, + EnqueueTokenBridgePrepareTransactionRequestParams, +} from './enqueueTokenBridgePrepareTransactionRequest'; +import { + enqueueTokenBridgePrepareSetWethGatewayTransactionRequest, + EnqueueTokenBridgePrepareSetWethGatewayTransactionRequestParams, +} from './enqueueTokenBridgePrepareSetWethGatewayTransactionRequest'; import { CreateRollupGetRetryablesFeesParams, createRollupGetRetryablesFees, @@ -348,6 +359,14 @@ export { // createTokenBridgeDefaultRetryablesFees, // + enqueueDefaultMaxGasPrice, + enqueueDefaultMaxGasForContracts, + enqueueDefaultGasLimitForWethGateway, + enqueueTokenBridgePrepareTransactionRequest, + EnqueueTokenBridgePrepareTransactionRequestParams, + enqueueTokenBridgePrepareSetWethGatewayTransactionRequest, + EnqueueTokenBridgePrepareSetWethGatewayTransactionRequestParams, + // fetchAllowance, FetchAllowanceProps, fetchDecimals, diff --git a/src/package.json b/src/package.json index 33da709c..27bf3a12 100644 --- a/src/package.json +++ b/src/package.json @@ -47,6 +47,9 @@ "repository": "git+https://github.com/OffchainLabs/arbitrum-chain-sdk.git", "author": "Offchain Labs, Inc.", "license": "Apache-2.0", + "scripts": { + "prepare": "cd .. && npm run build" + }, "engines": { "node": ">=18" },