From aa54664e9d6036270661cfb51e20dbddb584a826 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Thu, 19 Mar 2026 13:20:24 +0000 Subject: [PATCH 01/20] feat: add enqueueTokenBridgeDeployment and enqueueSetWethGateway Prepare token bridge deployment transactions without requiring an orbit chain RPC. enqueueTokenBridgeDeployment encodes the createTokenBridge call with caller-provided gas params. enqueueSetWethGateway encodes the setGateways call through UpgradeExecutor with explicit retryable costs. --- src/constants.ts | 2 + src/enqueueSetWethGateway.ts | 164 ++++++++++++++++ src/enqueueSetWethGateway.unit.test.ts | 183 ++++++++++++++++++ src/enqueueTokenBridgeDeployment.ts | 90 +++++++++ src/enqueueTokenBridgeDeployment.unit.test.ts | 133 +++++++++++++ src/index.ts | 15 ++ 6 files changed, 587 insertions(+) create mode 100644 src/enqueueSetWethGateway.ts create mode 100644 src/enqueueSetWethGateway.unit.test.ts create mode 100644 src/enqueueTokenBridgeDeployment.ts create mode 100644 src/enqueueTokenBridgeDeployment.unit.test.ts diff --git a/src/constants.ts b/src/constants.ts index 09bbd19f..3e34e11d 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -9,3 +9,5 @@ export const createRollupDefaultRetryablesFees = parseEther('0.125'); * Approximate value necessary to pay for retryables fees for `createTokenBridge`. */ export const createTokenBridgeDefaultRetryablesFees = parseEther('0.02'); + +export const enqueueDefaultMaxGasPrice = 200_000_000n; // 0.2 gwei diff --git a/src/enqueueSetWethGateway.ts b/src/enqueueSetWethGateway.ts new file mode 100644 index 00000000..9b1f1bb9 --- /dev/null +++ b/src/enqueueSetWethGateway.ts @@ -0,0 +1,164 @@ +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 { Prettify } from './types/utils'; +import { WithTokenBridgeCreatorAddressOverride } from './types/createTokenBridgeTypes'; +import { enqueueDefaultMaxGasPrice } from './constants'; + +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', + }, +] as const; + +export type EnqueueSetWethGatewayParams = Prettify< + WithTokenBridgeCreatorAddressOverride<{ + rollup: Address; + account: Address; + rollupDeploymentBlockNumber?: bigint; + parentChainPublicClient: PublicClient; + gasLimit: bigint; + maxFeePerGas?: bigint; + maxSubmissionCost: bigint; + }> +>; + +export async function enqueueSetWethGateway({ + rollup, + account, + rollupDeploymentBlockNumber, + parentChainPublicClient, + gasLimit, + maxFeePerGas = enqueueDefaultMaxGasPrice, + maxSubmissionCost, + tokenBridgeCreatorAddressOverride, +}: EnqueueSetWethGatewayParams) { + 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: parentChainGatewayRouterAbi, + 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, + }); + + const deposit = gasLimit * maxFeePerGas + maxSubmissionCost; + + const setGatewaysCalldata = encodeFunctionData({ + abi: parentChainGatewayRouterAbi, + functionName: 'setGateways', + args: [ + [tokenBridgeContracts.parentChainContracts.weth], + [tokenBridgeContracts.parentChainContracts.wethGateway], + gasLimit, + maxFeePerGas, + 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/enqueueSetWethGateway.unit.test.ts b/src/enqueueSetWethGateway.unit.test.ts new file mode 100644 index 00000000..f043f508 --- /dev/null +++ b/src/enqueueSetWethGateway.unit.test.ts @@ -0,0 +1,183 @@ +import { it, expect, vi, describe } from 'vitest'; +import { Address, PublicClient, zeroAddress } from 'viem'; +import { arbitrumSepolia } from 'viem/chains'; + +import { enqueueSetWethGateway } from './enqueueSetWethGateway'; + +// vi.mock factories are hoisted, so addresses must be defined via vi.hoisted +const { + rollupAddress, + account, + tokenBridgeCreatorAddr, + mockInboxAddress, + mockBridgeAddress, + mockRouterAddress, + mockWethAddress, + mockWethGatewayAddress, + mockUpgradeExecutorAddress, +} = vi.hoisted(() => ({ + rollupAddress: '0x1111111111111111111111111111111111111111' as Address, + account: '0x3333333333333333333333333333333333333333' as Address, + tokenBridgeCreatorAddr: '0x4444444444444444444444444444444444444444' as Address, + mockInboxAddress: '0x5555555555555555555555555555555555555555' as Address, + mockBridgeAddress: '0x6666666666666666666666666666666666666666' as Address, + mockRouterAddress: '0x7777777777777777777777777777777777777777' as Address, + mockWethAddress: '0x8888888888888888888888888888888888888888' as Address, + mockWethGatewayAddress: '0x9999999999999999999999999999999999999999' as Address, + mockUpgradeExecutorAddress: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' as Address, +})); + +vi.mock('./utils/isCustomFeeTokenChain', () => ({ + isCustomFeeTokenChain: vi.fn().mockResolvedValue(false), +})); + +vi.mock('./createTokenBridgeFetchTokenBridgeContracts', () => ({ + createTokenBridgeFetchTokenBridgeContracts: vi.fn().mockResolvedValue({ + parentChainContracts: { + router: mockRouterAddress, + standardGateway: '0x0000000000000000000000000000000000000000', + customGateway: '0x0000000000000000000000000000000000000000', + wethGateway: mockWethGatewayAddress, + weth: mockWethAddress, + multicall: '0x0000000000000000000000000000000000000000', + }, + orbitChainContracts: { + router: '0x0000000000000000000000000000000000000000', + standardGateway: '0x0000000000000000000000000000000000000000', + customGateway: '0x0000000000000000000000000000000000000000', + wethGateway: '0x0000000000000000000000000000000000000000', + weth: '0x0000000000000000000000000000000000000000', + proxyAdmin: '0x0000000000000000000000000000000000000000', + beaconProxyFactory: '0x0000000000000000000000000000000000000000', + upgradeExecutor: '0x0000000000000000000000000000000000000000', + multicall: '0x0000000000000000000000000000000000000000', + }, + }), +})); + +vi.mock('./createRollupFetchCoreContracts', () => ({ + createRollupFetchCoreContracts: vi.fn().mockResolvedValue({ + upgradeExecutor: mockUpgradeExecutorAddress, + rollup: rollupAddress, + inbox: mockInboxAddress, + outbox: '0x0000000000000000000000000000000000000000', + adminProxy: '0x0000000000000000000000000000000000000000', + sequencerInbox: '0x0000000000000000000000000000000000000000', + bridge: mockBridgeAddress, + validatorUtils: '0x0000000000000000000000000000000000000000', + validatorWalletCreator: '0x0000000000000000000000000000000000000000', + deployedAtBlockNumber: 0n, + }), +})); + +function createMockClient({ + registeredWethGateway = zeroAddress, +}: { + registeredWethGateway?: Address; +} = {}) { + const readContract = vi.fn().mockImplementation(({ functionName }: { functionName: string }) => { + if (functionName === 'inbox') return mockInboxAddress; + if (functionName === 'l1TokenToGateway') return registeredWethGateway; + throw new Error(`Unexpected readContract call: ${functionName}`); + }); + + const prepareTransactionRequest = vi.fn().mockResolvedValue({ + to: mockUpgradeExecutorAddress, + data: '0x', + value: 0n, + gas: 500_000n, + account, + from: account, + nonce: 0, + maxFeePerGas: 1_000_000_000n, + maxPriorityFeePerGas: 0n, + }); + + return { + chain: arbitrumSepolia, + readContract, + prepareTransactionRequest, + } as unknown as PublicClient; +} + +describe('enqueueSetWethGateway', () => { + it('returns tx routed through UpgradeExecutor with correct deposit', async () => { + const client = createMockClient(); + const gasLimit = 100_000n; + const maxFeePerGas = 200_000_000n; + const maxSubmissionCost = 50_000n; + + const result = await enqueueSetWethGateway({ + rollup: rollupAddress, + account, + parentChainPublicClient: client, + gasLimit, + maxFeePerGas, + maxSubmissionCost, + tokenBridgeCreatorAddressOverride: tokenBridgeCreatorAddr, + }); + + expect(result.chainId).toEqual(arbitrumSepolia.id); + + const mockClient = client as unknown as { prepareTransactionRequest: ReturnType }; + const prepareCall = mockClient.prepareTransactionRequest.mock.calls[0][0]; + expect(prepareCall.to).toEqual(mockUpgradeExecutorAddress); + + const expectedDeposit = gasLimit * maxFeePerGas + maxSubmissionCost; + expect(prepareCall.value).toEqual(expectedDeposit); + }); + + it('deposit equals gasLimit * maxFeePerGas + maxSubmissionCost', async () => { + const client = createMockClient(); + const gasLimit = 500_000n; + const maxFeePerGas = 300_000_000n; + const maxSubmissionCost = 100_000n; + + await enqueueSetWethGateway({ + rollup: rollupAddress, + account, + parentChainPublicClient: client, + gasLimit, + maxFeePerGas, + maxSubmissionCost, + tokenBridgeCreatorAddressOverride: tokenBridgeCreatorAddr, + }); + + const mockClient = client as unknown as { prepareTransactionRequest: ReturnType }; + const prepareCall = mockClient.prepareTransactionRequest.mock.calls[0][0]; + expect(prepareCall.value).toEqual(gasLimit * maxFeePerGas + maxSubmissionCost); + }); + + it('throws if custom fee token chain', async () => { + const { isCustomFeeTokenChain } = await import('./utils/isCustomFeeTokenChain'); + vi.mocked(isCustomFeeTokenChain).mockResolvedValueOnce(true); + + const client = createMockClient(); + + await expect( + enqueueSetWethGateway({ + rollup: rollupAddress, + account, + parentChainPublicClient: client, + gasLimit: 100_000n, + maxSubmissionCost: 50_000n, + tokenBridgeCreatorAddressOverride: tokenBridgeCreatorAddr, + }), + ).rejects.toThrowError('chain is custom fee token chain, no need to register the weth gateway.'); + }); + + it('throws if WETH gateway already registered', async () => { + const client = createMockClient({ registeredWethGateway: mockWethGatewayAddress }); + + await expect( + enqueueSetWethGateway({ + rollup: rollupAddress, + account, + parentChainPublicClient: client, + gasLimit: 100_000n, + maxSubmissionCost: 50_000n, + tokenBridgeCreatorAddressOverride: tokenBridgeCreatorAddr, + }), + ).rejects.toThrowError('weth gateway is already registered in the router.'); + }); +}); diff --git a/src/enqueueTokenBridgeDeployment.ts b/src/enqueueTokenBridgeDeployment.ts new file mode 100644 index 00000000..5f15dcfb --- /dev/null +++ b/src/enqueueTokenBridgeDeployment.ts @@ -0,0 +1,90 @@ +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 } from './constants'; + +export type EnqueueTokenBridgeDeploymentParams = Prettify< + WithTokenBridgeCreatorAddressOverride<{ + rollup: Address; + rollupOwner: Address; + account: Address; + parentChainPublicClient: PublicClient; + maxGasForContracts: bigint; + maxGasPrice?: bigint; + retryableFee: bigint; + gasOverrides?: TransactionRequestGasOverrides; + }> +>; + +export async function enqueueTokenBridgeDeployment({ + rollup, + rollupOwner, + account, + parentChainPublicClient, + maxGasForContracts, + maxGasPrice = enqueueDefaultMaxGasPrice, + retryableFee, + gasOverrides, + tokenBridgeCreatorAddressOverride, +}: EnqueueTokenBridgeDeploymentParams) { + const { chainId } = validateParentChain(parentChainPublicClient); + + const tokenBridgeCreatorAddress = + tokenBridgeCreatorAddressOverride ?? getTokenBridgeCreatorAddress(parentChainPublicClient); + + const inbox = await parentChainPublicClient.readContract({ + address: rollup, + abi: rollupABI, + functionName: 'inbox', + }); + + // Parent-chain-only idempotency check: if router is non-zero, bridge is already deployed + const [router] = await parentChainPublicClient.readContract({ + address: tokenBridgeCreatorAddress, + abi: tokenBridgeCreatorABI, + functionName: 'inboxToL2Deployment', + args: [inbox], + }); + if (router !== zeroAddress) { + throw new Error(`Token bridge contracts for Rollup ${rollup} are already deployed`); + } + + const chainUsesCustomFee = await isCustomFeeTokenChain({ + rollup, + parentChainPublicClient, + }); + + // @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, rollupOwner, maxGasForContracts, maxGasPrice], + }), + value: chainUsesCustomFee ? 0n : retryableFee, + account, + // if the base gas limit override was provided, hardcode gas to 0 to skip estimation + gas: typeof gasOverrides?.gasLimit?.base !== 'undefined' ? 0n : undefined, + }); + + if (gasOverrides && gasOverrides.gasLimit) { + request.gas = applyPercentIncrease({ + base: gasOverrides.gasLimit.base ?? request.gas!, + percentIncrease: gasOverrides.gasLimit.percentIncrease, + }); + } + + return { ...request, chainId }; +} diff --git a/src/enqueueTokenBridgeDeployment.unit.test.ts b/src/enqueueTokenBridgeDeployment.unit.test.ts new file mode 100644 index 00000000..d4e3ee92 --- /dev/null +++ b/src/enqueueTokenBridgeDeployment.unit.test.ts @@ -0,0 +1,133 @@ +import { it, expect, vi, describe } from 'vitest'; +import { + Address, + PublicClient, + zeroAddress, + encodeFunctionData, +} from 'viem'; +import { arbitrumSepolia } from 'viem/chains'; + +import { tokenBridgeCreatorABI } from './contracts/TokenBridgeCreator'; +import { enqueueTokenBridgeDeployment } from './enqueueTokenBridgeDeployment'; +import { enqueueDefaultMaxGasPrice } from './constants'; + +const rollupAddress = '0x1111111111111111111111111111111111111111' as Address; +const rollupOwner = '0x2222222222222222222222222222222222222222' as Address; +const account = '0x3333333333333333333333333333333333333333' as Address; +const tokenBridgeCreatorAddr = '0x4444444444444444444444444444444444444444' as Address; +const mockInboxAddress = '0x5555555555555555555555555555555555555555' as Address; +const mockBridgeAddress = '0x6666666666666666666666666666666666666666' as Address; +const nineZeroAddresses = Array(9).fill(zeroAddress) as Address[]; + +function createMockClient({ + inboxToL2DeploymentRouter = zeroAddress, + isCustomFeeToken = false, +}: { + inboxToL2DeploymentRouter?: Address; + isCustomFeeToken?: boolean; +} = {}) { + const readContract = vi.fn().mockImplementation(({ functionName }: { functionName: string }) => { + if (functionName === 'inbox') return mockInboxAddress; + if (functionName === 'inboxToL2Deployment') { + return [inboxToL2DeploymentRouter, ...nineZeroAddresses.slice(1)]; + } + if (functionName === 'bridge') return mockBridgeAddress; + if (functionName === 'nativeToken') { + if (!isCustomFeeToken) throw new Error('revert'); + return '0x7777777777777777777777777777777777777777'; + } + throw new Error(`Unexpected readContract call: ${functionName}`); + }); + + const prepareTransactionRequest = vi.fn().mockResolvedValue({ + to: tokenBridgeCreatorAddr, + data: '0x', + value: 0n, + gas: 500_000n, + account, + from: account, + nonce: 0, + maxFeePerGas: 1_000_000_000n, + maxPriorityFeePerGas: 0n, + }); + + return { + chain: arbitrumSepolia, + readContract, + prepareTransactionRequest, + } as unknown as PublicClient; +} + +describe('enqueueTokenBridgeDeployment', () => { + it('returns correctly encoded tx with expected value and calldata', async () => { + const client = createMockClient(); + const retryableFee = 1_000_000n; + + const result = await enqueueTokenBridgeDeployment({ + rollup: rollupAddress, + rollupOwner, + account, + parentChainPublicClient: client, + maxGasForContracts: 6_000_000n, + retryableFee, + gasOverrides: { gasLimit: { base: 1_000n } }, + tokenBridgeCreatorAddressOverride: tokenBridgeCreatorAddr, + }); + + expect(result.chainId).toEqual(arbitrumSepolia.id); + expect(result.gas).toEqual(1_000n); + + // Verify prepareTransactionRequest was called with correct args + const mockClient = client as unknown as { prepareTransactionRequest: ReturnType }; + const prepareCall = mockClient.prepareTransactionRequest.mock.calls[0][0]; + expect(prepareCall.to).toEqual(tokenBridgeCreatorAddr); + expect(prepareCall.value).toEqual(retryableFee); + expect(prepareCall.account).toEqual(account); + + // Verify calldata encodes createTokenBridge correctly + const expectedData = encodeFunctionData({ + abi: tokenBridgeCreatorABI, + functionName: 'createTokenBridge', + args: [mockInboxAddress, rollupOwner, 6_000_000n, enqueueDefaultMaxGasPrice], + }); + expect(prepareCall.data).toEqual(expectedData); + }); + + it('throws if bridge already deployed (non-zero router from inboxToL2Deployment)', async () => { + const nonZeroRouter = '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' as Address; + const client = createMockClient({ inboxToL2DeploymentRouter: nonZeroRouter }); + + await expect( + enqueueTokenBridgeDeployment({ + rollup: rollupAddress, + rollupOwner, + account, + parentChainPublicClient: client, + maxGasForContracts: 6_000_000n, + retryableFee: 1_000_000n, + tokenBridgeCreatorAddressOverride: tokenBridgeCreatorAddr, + }), + ).rejects.toThrowError( + `Token bridge contracts for Rollup ${rollupAddress} are already deployed`, + ); + }); + + it('sets value to 0n for custom fee token chains', async () => { + const client = createMockClient({ isCustomFeeToken: true }); + + await enqueueTokenBridgeDeployment({ + rollup: rollupAddress, + rollupOwner, + account, + parentChainPublicClient: client, + maxGasForContracts: 6_000_000n, + retryableFee: 1_000_000n, + gasOverrides: { gasLimit: { base: 1_000n } }, + tokenBridgeCreatorAddressOverride: tokenBridgeCreatorAddr, + }); + + const mockClient = client as unknown as { prepareTransactionRequest: ReturnType }; + const prepareCall = mockClient.prepareTransactionRequest.mock.calls[0][0]; + expect(prepareCall.value).toEqual(0n); + }); +}); diff --git a/src/index.ts b/src/index.ts index e05d0fe4..9ccc0acf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -178,7 +178,16 @@ import { import { createRollupDefaultRetryablesFees, createTokenBridgeDefaultRetryablesFees, + enqueueDefaultMaxGasPrice, } from './constants'; +import { + enqueueTokenBridgeDeployment, + EnqueueTokenBridgeDeploymentParams, +} from './enqueueTokenBridgeDeployment'; +import { + enqueueSetWethGateway, + EnqueueSetWethGatewayParams, +} from './enqueueSetWethGateway'; import { CreateRollupGetRetryablesFeesParams, createRollupGetRetryablesFees, @@ -348,6 +357,12 @@ export { // createTokenBridgeDefaultRetryablesFees, // + enqueueDefaultMaxGasPrice, + enqueueTokenBridgeDeployment, + EnqueueTokenBridgeDeploymentParams, + enqueueSetWethGateway, + EnqueueSetWethGatewayParams, + // fetchAllowance, FetchAllowanceProps, fetchDecimals, From 7ffcb0bd0512cf2b2d050e0beabb4ef7a0f470ab Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Thu, 19 Mar 2026 14:15:07 +0000 Subject: [PATCH 02/20] refactor: address PR review feedback - Rename maxFeePerGas to maxGasPrice in enqueueSetWethGateway for consistency with enqueueTokenBridgeDeployment and the constant name - Nest rollup/rollupOwner in params object to match existing createTokenBridgePrepareTransactionRequest pattern - Update idempotency comment to explain parent-chain-only check rationale and that failed retryables should be manually redeemed - Restore gas override comments from reference implementation - Replace duplicate deposit test with calldata verification test for enqueueSetWethGateway --- src/enqueueSetWethGateway.ts | 8 ++-- src/enqueueSetWethGateway.unit.test.ts | 48 ++++++++++++++----- src/enqueueTokenBridgeDeployment.ts | 21 ++++---- src/enqueueTokenBridgeDeployment.unit.test.ts | 9 ++-- 4 files changed, 56 insertions(+), 30 deletions(-) diff --git a/src/enqueueSetWethGateway.ts b/src/enqueueSetWethGateway.ts index 9b1f1bb9..7bed276b 100644 --- a/src/enqueueSetWethGateway.ts +++ b/src/enqueueSetWethGateway.ts @@ -77,7 +77,7 @@ export type EnqueueSetWethGatewayParams rollupDeploymentBlockNumber?: bigint; parentChainPublicClient: PublicClient; gasLimit: bigint; - maxFeePerGas?: bigint; + maxGasPrice?: bigint; maxSubmissionCost: bigint; }> >; @@ -88,7 +88,7 @@ export async function enqueueSetWethGateway) { @@ -131,7 +131,7 @@ export async function enqueueSetWethGateway { it('returns tx routed through UpgradeExecutor with correct deposit', async () => { const client = createMockClient(); const gasLimit = 100_000n; - const maxFeePerGas = 200_000_000n; + const maxGasPrice = 200_000_000n; const maxSubmissionCost = 50_000n; const result = await enqueueSetWethGateway({ @@ -112,7 +113,7 @@ describe('enqueueSetWethGateway', () => { account, parentChainPublicClient: client, gasLimit, - maxFeePerGas, + maxGasPrice, maxSubmissionCost, tokenBridgeCreatorAddressOverride: tokenBridgeCreatorAddr, }); @@ -123,29 +124,54 @@ describe('enqueueSetWethGateway', () => { const prepareCall = mockClient.prepareTransactionRequest.mock.calls[0][0]; expect(prepareCall.to).toEqual(mockUpgradeExecutorAddress); - const expectedDeposit = gasLimit * maxFeePerGas + maxSubmissionCost; + const expectedDeposit = gasLimit * maxGasPrice + maxSubmissionCost; expect(prepareCall.value).toEqual(expectedDeposit); }); - it('deposit equals gasLimit * maxFeePerGas + maxSubmissionCost', async () => { + it('encodes setGateways calldata wrapped in UpgradeExecutor executeCall', async () => { const client = createMockClient(); - const gasLimit = 500_000n; - const maxFeePerGas = 300_000_000n; - const maxSubmissionCost = 100_000n; + const gasLimit = 100_000n; + const maxGasPrice = 200_000_000n; + const maxSubmissionCost = 50_000n; await enqueueSetWethGateway({ rollup: rollupAddress, account, parentChainPublicClient: client, gasLimit, - maxFeePerGas, + maxGasPrice, maxSubmissionCost, tokenBridgeCreatorAddressOverride: tokenBridgeCreatorAddr, }); + const setGatewaysCalldata = encodeFunctionData({ + abi: [ + { + 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, + functionName: 'setGateways', + args: [[mockWethAddress], [mockWethGatewayAddress], gasLimit, maxGasPrice, maxSubmissionCost], + }); + + const expectedData = upgradeExecutorEncodeFunctionData({ + functionName: 'executeCall', + args: [mockRouterAddress, setGatewaysCalldata], + }); + const mockClient = client as unknown as { prepareTransactionRequest: ReturnType }; const prepareCall = mockClient.prepareTransactionRequest.mock.calls[0][0]; - expect(prepareCall.value).toEqual(gasLimit * maxFeePerGas + maxSubmissionCost); + expect(prepareCall.data).toEqual(expectedData); }); it('throws if custom fee token chain', async () => { diff --git a/src/enqueueTokenBridgeDeployment.ts b/src/enqueueTokenBridgeDeployment.ts index 5f15dcfb..17c59749 100644 --- a/src/enqueueTokenBridgeDeployment.ts +++ b/src/enqueueTokenBridgeDeployment.ts @@ -15,8 +15,7 @@ import { enqueueDefaultMaxGasPrice } from './constants'; export type EnqueueTokenBridgeDeploymentParams = Prettify< WithTokenBridgeCreatorAddressOverride<{ - rollup: Address; - rollupOwner: Address; + params: { rollup: Address; rollupOwner: Address }; account: Address; parentChainPublicClient: PublicClient; maxGasForContracts: bigint; @@ -27,8 +26,7 @@ export type EnqueueTokenBridgeDeploymentParams; export async function enqueueTokenBridgeDeployment({ - rollup, - rollupOwner, + params, account, parentChainPublicClient, maxGasForContracts, @@ -43,12 +41,14 @@ export async function enqueueTokenBridgeDeployment { const retryableFee = 1_000_000n; const result = await enqueueTokenBridgeDeployment({ - rollup: rollupAddress, - rollupOwner, + params: { rollup: rollupAddress, rollupOwner }, account, parentChainPublicClient: client, maxGasForContracts: 6_000_000n, @@ -99,8 +98,7 @@ describe('enqueueTokenBridgeDeployment', () => { await expect( enqueueTokenBridgeDeployment({ - rollup: rollupAddress, - rollupOwner, + params: { rollup: rollupAddress, rollupOwner }, account, parentChainPublicClient: client, maxGasForContracts: 6_000_000n, @@ -116,8 +114,7 @@ describe('enqueueTokenBridgeDeployment', () => { const client = createMockClient({ isCustomFeeToken: true }); await enqueueTokenBridgeDeployment({ - rollup: rollupAddress, - rollupOwner, + params: { rollup: rollupAddress, rollupOwner }, account, parentChainPublicClient: client, maxGasForContracts: 6_000_000n, From fb3506d9582965543e7dd442881a00ceb69be822 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Thu, 19 Mar 2026 15:04:38 +0000 Subject: [PATCH 03/20] refactor: rename to PrepareTransactionRequest convention, extract shared ABI - Rename enqueueTokenBridgeDeployment to enqueueTokenBridgePrepareTransactionRequest - Rename enqueueSetWethGateway to enqueueSetWethGatewayPrepareTransactionRequest - Extract parentChainGatewayRouterAbi to src/contracts/GatewayRouter.ts, replacing inline copies in both enqueue and existing set weth gateway files --- src/contracts/GatewayRouter.ts | 60 ++++++++++++++++ ...PrepareSetWethGatewayTransactionRequest.ts | 68 ++---------------- ...etWethGatewayPrepareTransactionRequest.ts} | 72 ++----------------- ...wayPrepareTransactionRequest.unit.test.ts} | 12 ++-- ...ueTokenBridgePrepareTransactionRequest.ts} | 6 +- ...dgePrepareTransactionRequest.unit.test.ts} | 10 +-- src/index.ts | 20 +++--- 7 files changed, 94 insertions(+), 154 deletions(-) create mode 100644 src/contracts/GatewayRouter.ts rename src/{enqueueSetWethGateway.ts => enqueueSetWethGatewayPrepareTransactionRequest.ts} (69%) rename src/{enqueueSetWethGateway.unit.test.ts => enqueueSetWethGatewayPrepareTransactionRequest.unit.test.ts} (94%) rename src/{enqueueTokenBridgeDeployment.ts => enqueueTokenBridgePrepareTransactionRequest.ts} (92%) rename src/{enqueueTokenBridgeDeployment.unit.test.ts => enqueueTokenBridgePrepareTransactionRequest.unit.test.ts} (92%) 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/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/enqueueSetWethGateway.ts b/src/enqueueSetWethGatewayPrepareTransactionRequest.ts similarity index 69% rename from src/enqueueSetWethGateway.ts rename to src/enqueueSetWethGatewayPrepareTransactionRequest.ts index 7bed276b..0492cff9 100644 --- a/src/enqueueSetWethGateway.ts +++ b/src/enqueueSetWethGatewayPrepareTransactionRequest.ts @@ -5,72 +5,12 @@ 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 } from './constants'; -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', - }, -] as const; - -export type EnqueueSetWethGatewayParams = Prettify< +export type EnqueueSetWethGatewayPrepareTransactionRequestParams = Prettify< WithTokenBridgeCreatorAddressOverride<{ rollup: Address; account: Address; @@ -82,7 +22,7 @@ export type EnqueueSetWethGatewayParams }> >; -export async function enqueueSetWethGateway({ +export async function enqueueSetWethGatewayPrepareTransactionRequest({ rollup, account, rollupDeploymentBlockNumber, @@ -91,7 +31,7 @@ export async function enqueueSetWethGateway) { +}: EnqueueSetWethGatewayPrepareTransactionRequestParams) { const { chainId } = validateParentChain(parentChainPublicClient); if ( @@ -117,7 +57,7 @@ export async function enqueueSetWethGateway { +describe('enqueueSetWethGatewayPrepareTransactionRequest', () => { it('returns tx routed through UpgradeExecutor with correct deposit', async () => { const client = createMockClient(); const gasLimit = 100_000n; const maxGasPrice = 200_000_000n; const maxSubmissionCost = 50_000n; - const result = await enqueueSetWethGateway({ + const result = await enqueueSetWethGatewayPrepareTransactionRequest({ rollup: rollupAddress, account, parentChainPublicClient: client, @@ -134,7 +134,7 @@ describe('enqueueSetWethGateway', () => { const maxGasPrice = 200_000_000n; const maxSubmissionCost = 50_000n; - await enqueueSetWethGateway({ + await enqueueSetWethGatewayPrepareTransactionRequest({ rollup: rollupAddress, account, parentChainPublicClient: client, @@ -181,7 +181,7 @@ describe('enqueueSetWethGateway', () => { const client = createMockClient(); await expect( - enqueueSetWethGateway({ + enqueueSetWethGatewayPrepareTransactionRequest({ rollup: rollupAddress, account, parentChainPublicClient: client, @@ -196,7 +196,7 @@ describe('enqueueSetWethGateway', () => { const client = createMockClient({ registeredWethGateway: mockWethGatewayAddress }); await expect( - enqueueSetWethGateway({ + enqueueSetWethGatewayPrepareTransactionRequest({ rollup: rollupAddress, account, parentChainPublicClient: client, diff --git a/src/enqueueTokenBridgeDeployment.ts b/src/enqueueTokenBridgePrepareTransactionRequest.ts similarity index 92% rename from src/enqueueTokenBridgeDeployment.ts rename to src/enqueueTokenBridgePrepareTransactionRequest.ts index 17c59749..1c77fa1e 100644 --- a/src/enqueueTokenBridgeDeployment.ts +++ b/src/enqueueTokenBridgePrepareTransactionRequest.ts @@ -13,7 +13,7 @@ import { WithTokenBridgeCreatorAddressOverride } from './types/createTokenBridge import { getTokenBridgeCreatorAddress } from './utils/getTokenBridgeCreatorAddress'; import { enqueueDefaultMaxGasPrice } from './constants'; -export type EnqueueTokenBridgeDeploymentParams = Prettify< +export type EnqueueTokenBridgePrepareTransactionRequestParams = Prettify< WithTokenBridgeCreatorAddressOverride<{ params: { rollup: Address; rollupOwner: Address }; account: Address; @@ -25,7 +25,7 @@ export type EnqueueTokenBridgeDeploymentParams >; -export async function enqueueTokenBridgeDeployment({ +export async function enqueueTokenBridgePrepareTransactionRequest({ params, account, parentChainPublicClient, @@ -34,7 +34,7 @@ export async function enqueueTokenBridgeDeployment) { +}: EnqueueTokenBridgePrepareTransactionRequestParams) { const { chainId } = validateParentChain(parentChainPublicClient); const tokenBridgeCreatorAddress = diff --git a/src/enqueueTokenBridgeDeployment.unit.test.ts b/src/enqueueTokenBridgePrepareTransactionRequest.unit.test.ts similarity index 92% rename from src/enqueueTokenBridgeDeployment.unit.test.ts rename to src/enqueueTokenBridgePrepareTransactionRequest.unit.test.ts index 40c08598..555b9b2f 100644 --- a/src/enqueueTokenBridgeDeployment.unit.test.ts +++ b/src/enqueueTokenBridgePrepareTransactionRequest.unit.test.ts @@ -8,7 +8,7 @@ import { import { arbitrumSepolia } from 'viem/chains'; import { tokenBridgeCreatorABI } from './contracts/TokenBridgeCreator'; -import { enqueueTokenBridgeDeployment } from './enqueueTokenBridgeDeployment'; +import { enqueueTokenBridgePrepareTransactionRequest } from './enqueueTokenBridgePrepareTransactionRequest'; import { enqueueDefaultMaxGasPrice } from './constants'; const rollupAddress = '0x1111111111111111111111111111111111111111' as Address; @@ -58,12 +58,12 @@ function createMockClient({ } as unknown as PublicClient; } -describe('enqueueTokenBridgeDeployment', () => { +describe('enqueueTokenBridgePrepareTransactionRequest', () => { it('returns correctly encoded tx with expected value and calldata', async () => { const client = createMockClient(); const retryableFee = 1_000_000n; - const result = await enqueueTokenBridgeDeployment({ + const result = await enqueueTokenBridgePrepareTransactionRequest({ params: { rollup: rollupAddress, rollupOwner }, account, parentChainPublicClient: client, @@ -97,7 +97,7 @@ describe('enqueueTokenBridgeDeployment', () => { const client = createMockClient({ inboxToL2DeploymentRouter: nonZeroRouter }); await expect( - enqueueTokenBridgeDeployment({ + enqueueTokenBridgePrepareTransactionRequest({ params: { rollup: rollupAddress, rollupOwner }, account, parentChainPublicClient: client, @@ -113,7 +113,7 @@ describe('enqueueTokenBridgeDeployment', () => { it('sets value to 0n for custom fee token chains', async () => { const client = createMockClient({ isCustomFeeToken: true }); - await enqueueTokenBridgeDeployment({ + await enqueueTokenBridgePrepareTransactionRequest({ params: { rollup: rollupAddress, rollupOwner }, account, parentChainPublicClient: client, diff --git a/src/index.ts b/src/index.ts index 9ccc0acf..35215f48 100644 --- a/src/index.ts +++ b/src/index.ts @@ -181,13 +181,13 @@ import { enqueueDefaultMaxGasPrice, } from './constants'; import { - enqueueTokenBridgeDeployment, - EnqueueTokenBridgeDeploymentParams, -} from './enqueueTokenBridgeDeployment'; + enqueueTokenBridgePrepareTransactionRequest, + EnqueueTokenBridgePrepareTransactionRequestParams, +} from './enqueueTokenBridgePrepareTransactionRequest'; import { - enqueueSetWethGateway, - EnqueueSetWethGatewayParams, -} from './enqueueSetWethGateway'; + enqueueSetWethGatewayPrepareTransactionRequest, + EnqueueSetWethGatewayPrepareTransactionRequestParams, +} from './enqueueSetWethGatewayPrepareTransactionRequest'; import { CreateRollupGetRetryablesFeesParams, createRollupGetRetryablesFees, @@ -358,10 +358,10 @@ export { createTokenBridgeDefaultRetryablesFees, // enqueueDefaultMaxGasPrice, - enqueueTokenBridgeDeployment, - EnqueueTokenBridgeDeploymentParams, - enqueueSetWethGateway, - EnqueueSetWethGatewayParams, + enqueueTokenBridgePrepareTransactionRequest, + EnqueueTokenBridgePrepareTransactionRequestParams, + enqueueSetWethGatewayPrepareTransactionRequest, + EnqueueSetWethGatewayPrepareTransactionRequestParams, // fetchAllowance, FetchAllowanceProps, From e3f85a71e0122d3619353d369e110706b06368c1 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Thu, 19 Mar 2026 15:07:40 +0000 Subject: [PATCH 04/20] refactor: use parseGwei for enqueueDefaultMaxGasPrice --- src/constants.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 3e34e11d..fcdb69c6 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`. @@ -10,4 +10,7 @@ export const createRollupDefaultRetryablesFees = parseEther('0.125'); */ export const createTokenBridgeDefaultRetryablesFees = parseEther('0.02'); -export const enqueueDefaultMaxGasPrice = 200_000_000n; // 0.2 gwei +/** + * 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'); From 9b54a85b20a6f8e7b408ab9b3f8db603b78bfd0d Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Thu, 19 Mar 2026 16:16:26 +0000 Subject: [PATCH 05/20] refactor: split submission costs, rename weth gateway to match convention - Split maxSubmissionFee into maxSubmissionCostForFactory and maxSubmissionCostForContracts to match existing retryable fee formula - Update retryable fee calculation to sum both submission costs - Rename enqueueSetWethGateway* to enqueueTokenBridgePrepareSetWethGateway* to match createTokenBridgePrepareSetWethGateway* naming convention - Remove CR comments --- ...repareSetWethGatewayTransactionRequest.ts} | 21 ++++++++++---- ...ethGatewayTransactionRequest.unit.test.ts} | 12 ++++---- ...eueTokenBridgePrepareTransactionRequest.ts | 13 +++++++-- ...idgePrepareTransactionRequest.unit.test.ts | 28 ++++++++++++++----- src/index.ts | 10 +++---- 5 files changed, 58 insertions(+), 26 deletions(-) rename src/{enqueueSetWethGatewayPrepareTransactionRequest.ts => enqueueTokenBridgePrepareSetWethGatewayTransactionRequest.ts} (79%) rename src/{enqueueSetWethGatewayPrepareTransactionRequest.unit.test.ts => enqueueTokenBridgePrepareSetWethGatewayTransactionRequest.unit.test.ts} (93%) diff --git a/src/enqueueSetWethGatewayPrepareTransactionRequest.ts b/src/enqueueTokenBridgePrepareSetWethGatewayTransactionRequest.ts similarity index 79% rename from src/enqueueSetWethGatewayPrepareTransactionRequest.ts rename to src/enqueueTokenBridgePrepareSetWethGatewayTransactionRequest.ts index 0492cff9..40194174 100644 --- a/src/enqueueSetWethGatewayPrepareTransactionRequest.ts +++ b/src/enqueueTokenBridgePrepareSetWethGatewayTransactionRequest.ts @@ -10,10 +10,19 @@ import { Prettify } from './types/utils'; import { WithTokenBridgeCreatorAddressOverride } from './types/createTokenBridgeTypes'; import { enqueueDefaultMaxGasPrice } from './constants'; -export type EnqueueSetWethGatewayPrepareTransactionRequestParams = Prettify< +export type EnqueueTokenBridgePrepareSetWethGatewayTransactionRequestParams = 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; @@ -22,7 +31,7 @@ export type EnqueueSetWethGatewayPrepareTransactionRequestParams >; -export async function enqueueSetWethGatewayPrepareTransactionRequest({ +export async function enqueueTokenBridgePrepareSetWethGatewayTransactionRequest({ rollup, account, rollupDeploymentBlockNumber, @@ -31,7 +40,7 @@ export async function enqueueSetWethGatewayPrepareTransactionRequest) { +}: EnqueueTokenBridgePrepareSetWethGatewayTransactionRequestParams) { const { chainId } = validateParentChain(parentChainPublicClient); if ( @@ -79,9 +88,9 @@ export async function enqueueSetWethGatewayPrepareTransactionRequest { +describe('enqueueTokenBridgePrepareSetWethGatewayTransactionRequest', () => { it('returns tx routed through UpgradeExecutor with correct deposit', async () => { const client = createMockClient(); const gasLimit = 100_000n; const maxGasPrice = 200_000_000n; const maxSubmissionCost = 50_000n; - const result = await enqueueSetWethGatewayPrepareTransactionRequest({ + const result = await enqueueTokenBridgePrepareSetWethGatewayTransactionRequest({ rollup: rollupAddress, account, parentChainPublicClient: client, @@ -134,7 +134,7 @@ describe('enqueueSetWethGatewayPrepareTransactionRequest', () => { const maxGasPrice = 200_000_000n; const maxSubmissionCost = 50_000n; - await enqueueSetWethGatewayPrepareTransactionRequest({ + await enqueueTokenBridgePrepareSetWethGatewayTransactionRequest({ rollup: rollupAddress, account, parentChainPublicClient: client, @@ -181,7 +181,7 @@ describe('enqueueSetWethGatewayPrepareTransactionRequest', () => { const client = createMockClient(); await expect( - enqueueSetWethGatewayPrepareTransactionRequest({ + enqueueTokenBridgePrepareSetWethGatewayTransactionRequest({ rollup: rollupAddress, account, parentChainPublicClient: client, @@ -196,7 +196,7 @@ describe('enqueueSetWethGatewayPrepareTransactionRequest', () => { const client = createMockClient({ registeredWethGateway: mockWethGatewayAddress }); await expect( - enqueueSetWethGatewayPrepareTransactionRequest({ + enqueueTokenBridgePrepareSetWethGatewayTransactionRequest({ rollup: rollupAddress, account, parentChainPublicClient: client, diff --git a/src/enqueueTokenBridgePrepareTransactionRequest.ts b/src/enqueueTokenBridgePrepareTransactionRequest.ts index 1c77fa1e..dbbf7aaa 100644 --- a/src/enqueueTokenBridgePrepareTransactionRequest.ts +++ b/src/enqueueTokenBridgePrepareTransactionRequest.ts @@ -19,8 +19,10 @@ export type EnqueueTokenBridgePrepareTransactionRequestParams; maxGasForContracts: bigint; + maxGasForFactory: bigint; maxGasPrice?: bigint; - retryableFee: bigint; + maxSubmissionCostForFactory: bigint; + maxSubmissionCostForContracts: bigint; gasOverrides?: TransactionRequestGasOverrides; }> >; @@ -30,8 +32,10 @@ export async function enqueueTokenBridgePrepareTransactionRequest) { @@ -64,6 +68,11 @@ export async function enqueueTokenBridgePrepareTransactionRequest { it('returns correctly encoded tx with expected value and calldata', async () => { const client = createMockClient(); - const retryableFee = 1_000_000n; + + const maxGasForContracts = 6_000_000n; + const maxGasForFactory = 3_000_000n; + const maxSubmissionCostForFactory = 100_000n; + const maxSubmissionCostForContracts = 200_000n; const result = await enqueueTokenBridgePrepareTransactionRequest({ params: { rollup: rollupAddress, rollupOwner }, account, parentChainPublicClient: client, - maxGasForContracts: 6_000_000n, - retryableFee, + maxGasForContracts, + maxGasForFactory, + maxSubmissionCostForFactory, + maxSubmissionCostForContracts, gasOverrides: { gasLimit: { base: 1_000n } }, tokenBridgeCreatorAddressOverride: tokenBridgeCreatorAddr, }); + const expectedRetryableFee = + maxSubmissionCostForFactory + + maxSubmissionCostForContracts + + enqueueDefaultMaxGasPrice * (maxGasForContracts + maxGasForFactory); + expect(result.chainId).toEqual(arbitrumSepolia.id); expect(result.gas).toEqual(1_000n); - // Verify prepareTransactionRequest was called with correct args const mockClient = client as unknown as { prepareTransactionRequest: ReturnType }; const prepareCall = mockClient.prepareTransactionRequest.mock.calls[0][0]; expect(prepareCall.to).toEqual(tokenBridgeCreatorAddr); - expect(prepareCall.value).toEqual(retryableFee); + expect(prepareCall.value).toEqual(expectedRetryableFee); expect(prepareCall.account).toEqual(account); // Verify calldata encodes createTokenBridge correctly @@ -102,7 +112,9 @@ describe('enqueueTokenBridgePrepareTransactionRequest', () => { account, parentChainPublicClient: client, maxGasForContracts: 6_000_000n, - retryableFee: 1_000_000n, + maxGasForFactory: 3_000_000n, + maxSubmissionCostForFactory: 100_000n, + maxSubmissionCostForContracts: 200_000n, tokenBridgeCreatorAddressOverride: tokenBridgeCreatorAddr, }), ).rejects.toThrowError( @@ -118,7 +130,9 @@ describe('enqueueTokenBridgePrepareTransactionRequest', () => { account, parentChainPublicClient: client, maxGasForContracts: 6_000_000n, - retryableFee: 1_000_000n, + maxGasForFactory: 3_000_000n, + maxSubmissionCostForFactory: 100_000n, + maxSubmissionCostForContracts: 200_000n, gasOverrides: { gasLimit: { base: 1_000n } }, tokenBridgeCreatorAddressOverride: tokenBridgeCreatorAddr, }); diff --git a/src/index.ts b/src/index.ts index 35215f48..99d8d7a6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -185,9 +185,9 @@ import { EnqueueTokenBridgePrepareTransactionRequestParams, } from './enqueueTokenBridgePrepareTransactionRequest'; import { - enqueueSetWethGatewayPrepareTransactionRequest, - EnqueueSetWethGatewayPrepareTransactionRequestParams, -} from './enqueueSetWethGatewayPrepareTransactionRequest'; + enqueueTokenBridgePrepareSetWethGatewayTransactionRequest, + EnqueueTokenBridgePrepareSetWethGatewayTransactionRequestParams, +} from './enqueueTokenBridgePrepareSetWethGatewayTransactionRequest'; import { CreateRollupGetRetryablesFeesParams, createRollupGetRetryablesFees, @@ -360,8 +360,8 @@ export { enqueueDefaultMaxGasPrice, enqueueTokenBridgePrepareTransactionRequest, EnqueueTokenBridgePrepareTransactionRequestParams, - enqueueSetWethGatewayPrepareTransactionRequest, - EnqueueSetWethGatewayPrepareTransactionRequestParams, + enqueueTokenBridgePrepareSetWethGatewayTransactionRequest, + EnqueueTokenBridgePrepareSetWethGatewayTransactionRequestParams, // fetchAllowance, FetchAllowanceProps, From efa5df727658d527013f0ff5678d7c2d35746f71 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Thu, 19 Mar 2026 16:45:28 +0000 Subject: [PATCH 06/20] chore: fix prettier formatting --- ...BridgePrepareSetWethGatewayTransactionRequest.ts | 13 +++++++------ ...areSetWethGatewayTransactionRequest.unit.test.ts | 4 +++- src/enqueueTokenBridgePrepareTransactionRequest.ts | 13 +++++++------ ...okenBridgePrepareTransactionRequest.unit.test.ts | 7 +------ 4 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/enqueueTokenBridgePrepareSetWethGatewayTransactionRequest.ts b/src/enqueueTokenBridgePrepareSetWethGatewayTransactionRequest.ts index 40194174..a98cf9e4 100644 --- a/src/enqueueTokenBridgePrepareSetWethGatewayTransactionRequest.ts +++ b/src/enqueueTokenBridgePrepareSetWethGatewayTransactionRequest.ts @@ -10,7 +10,9 @@ import { Prettify } from './types/utils'; import { WithTokenBridgeCreatorAddressOverride } from './types/createTokenBridgeTypes'; import { enqueueDefaultMaxGasPrice } from './constants'; -export type EnqueueTokenBridgePrepareSetWethGatewayTransactionRequestParams = Prettify< +export type EnqueueTokenBridgePrepareSetWethGatewayTransactionRequestParams< + TParentChain extends Chain | undefined, +> = Prettify< WithTokenBridgeCreatorAddressOverride<{ /** * Address of the Rollup contract. @@ -31,7 +33,9 @@ export type EnqueueTokenBridgePrepareSetWethGatewayTransactionRequestParams >; -export async function enqueueTokenBridgePrepareSetWethGatewayTransactionRequest({ +export async function enqueueTokenBridgePrepareSetWethGatewayTransactionRequest< + TParentChain extends Chain | undefined, +>({ rollup, account, rollupDeploymentBlockNumber, @@ -100,10 +104,7 @@ export async function enqueueTokenBridgePrepareSetWethGatewayTransactionRequest< to: rollupCoreContracts.upgradeExecutor, data: upgradeExecutorEncodeFunctionData({ functionName: 'executeCall', - args: [ - tokenBridgeContracts.parentChainContracts.router, - setGatewaysCalldata, - ], + args: [tokenBridgeContracts.parentChainContracts.router, setGatewaysCalldata], }), value: deposit, account, diff --git a/src/enqueueTokenBridgePrepareSetWethGatewayTransactionRequest.unit.test.ts b/src/enqueueTokenBridgePrepareSetWethGatewayTransactionRequest.unit.test.ts index 0db9d8bb..b44af665 100644 --- a/src/enqueueTokenBridgePrepareSetWethGatewayTransactionRequest.unit.test.ts +++ b/src/enqueueTokenBridgePrepareSetWethGatewayTransactionRequest.unit.test.ts @@ -189,7 +189,9 @@ describe('enqueueTokenBridgePrepareSetWethGatewayTransactionRequest', () => { maxSubmissionCost: 50_000n, tokenBridgeCreatorAddressOverride: tokenBridgeCreatorAddr, }), - ).rejects.toThrowError('chain is custom fee token chain, no need to register the weth gateway.'); + ).rejects.toThrowError( + 'chain is custom fee token chain, no need to register the weth gateway.', + ); }); it('throws if WETH gateway already registered', async () => { diff --git a/src/enqueueTokenBridgePrepareTransactionRequest.ts b/src/enqueueTokenBridgePrepareTransactionRequest.ts index dbbf7aaa..51127a20 100644 --- a/src/enqueueTokenBridgePrepareTransactionRequest.ts +++ b/src/enqueueTokenBridgePrepareTransactionRequest.ts @@ -4,16 +4,15 @@ 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 { TransactionRequestGasOverrides, applyPercentIncrease } from './utils/gasOverrides'; import { Prettify } from './types/utils'; import { WithTokenBridgeCreatorAddressOverride } from './types/createTokenBridgeTypes'; import { getTokenBridgeCreatorAddress } from './utils/getTokenBridgeCreatorAddress'; import { enqueueDefaultMaxGasPrice } from './constants'; -export type EnqueueTokenBridgePrepareTransactionRequestParams = Prettify< +export type EnqueueTokenBridgePrepareTransactionRequestParams< + TParentChain extends Chain | undefined, +> = Prettify< WithTokenBridgeCreatorAddressOverride<{ params: { rollup: Address; rollupOwner: Address }; account: Address; @@ -27,7 +26,9 @@ export type EnqueueTokenBridgePrepareTransactionRequestParams >; -export async function enqueueTokenBridgePrepareTransactionRequest({ +export async function enqueueTokenBridgePrepareTransactionRequest< + TParentChain extends Chain | undefined, +>({ params, account, parentChainPublicClient, diff --git a/src/enqueueTokenBridgePrepareTransactionRequest.unit.test.ts b/src/enqueueTokenBridgePrepareTransactionRequest.unit.test.ts index 5a5747ab..14421940 100644 --- a/src/enqueueTokenBridgePrepareTransactionRequest.unit.test.ts +++ b/src/enqueueTokenBridgePrepareTransactionRequest.unit.test.ts @@ -1,10 +1,5 @@ import { it, expect, vi, describe } from 'vitest'; -import { - Address, - PublicClient, - zeroAddress, - encodeFunctionData, -} from 'viem'; +import { Address, PublicClient, zeroAddress, encodeFunctionData } from 'viem'; import { arbitrumSepolia } from 'viem/chains'; import { tokenBridgeCreatorABI } from './contracts/TokenBridgeCreator'; From 54ee7d0e895fd5f75b6d356d10a49581bf73366e Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Thu, 19 Mar 2026 16:57:44 +0000 Subject: [PATCH 07/20] fix: replace dynamic import with static import in weth gateway test Fixes vitest type checker failure caused by dynamic import syntax. --- ...enBridgePrepareSetWethGatewayTransactionRequest.unit.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/enqueueTokenBridgePrepareSetWethGatewayTransactionRequest.unit.test.ts b/src/enqueueTokenBridgePrepareSetWethGatewayTransactionRequest.unit.test.ts index b44af665..6a983158 100644 --- a/src/enqueueTokenBridgePrepareSetWethGatewayTransactionRequest.unit.test.ts +++ b/src/enqueueTokenBridgePrepareSetWethGatewayTransactionRequest.unit.test.ts @@ -4,6 +4,7 @@ import { arbitrumSepolia } from 'viem/chains'; import { enqueueTokenBridgePrepareSetWethGatewayTransactionRequest } from './enqueueTokenBridgePrepareSetWethGatewayTransactionRequest'; import { upgradeExecutorEncodeFunctionData } from './upgradeExecutorEncodeFunctionData'; +import { isCustomFeeTokenChain } from './utils/isCustomFeeTokenChain'; // vi.mock factories are hoisted, so addresses must be defined via vi.hoisted const { @@ -175,7 +176,6 @@ describe('enqueueTokenBridgePrepareSetWethGatewayTransactionRequest', () => { }); it('throws if custom fee token chain', async () => { - const { isCustomFeeTokenChain } = await import('./utils/isCustomFeeTokenChain'); vi.mocked(isCustomFeeTokenChain).mockResolvedValueOnce(true); const client = createMockClient(); From 91615137bf06cc096d1ea84d3e0ab213b5d80fdf Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Thu, 19 Mar 2026 17:22:06 +0000 Subject: [PATCH 08/20] test: add integration test for enqueue token bridge --- src/enqueueTokenBridge.integration.test.ts | 162 +++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 src/enqueueTokenBridge.integration.test.ts diff --git a/src/enqueueTokenBridge.integration.test.ts b/src/enqueueTokenBridge.integration.test.ts new file mode 100644 index 00000000..ebbf21bf --- /dev/null +++ b/src/enqueueTokenBridge.integration.test.ts @@ -0,0 +1,162 @@ +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'; + +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, + maxGasForContracts: 20_000_000n, + maxGasForFactory: 20_000_000n, + maxSubmissionCostForFactory: 4_000_000_000_000n, + maxSubmissionCostForContracts: 4_000_000_000_000n, + 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'); + + // 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, + gasLimit: 100_000n, + maxGasPrice: 200_000_000n, + maxSubmissionCost: 4_000_000_000_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'); + + await checkWethGateways(tokenBridgeContracts); + }); +}); From 8e120c276cd78f408884aa8d54fb6a237e3c5386 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Thu, 19 Mar 2026 17:25:22 +0000 Subject: [PATCH 09/20] test: remove brittle unit tests in favor of integration test --- ...WethGatewayTransactionRequest.unit.test.ts | 211 ------------------ ...idgePrepareTransactionRequest.unit.test.ts | 139 ------------ 2 files changed, 350 deletions(-) delete mode 100644 src/enqueueTokenBridgePrepareSetWethGatewayTransactionRequest.unit.test.ts delete mode 100644 src/enqueueTokenBridgePrepareTransactionRequest.unit.test.ts diff --git a/src/enqueueTokenBridgePrepareSetWethGatewayTransactionRequest.unit.test.ts b/src/enqueueTokenBridgePrepareSetWethGatewayTransactionRequest.unit.test.ts deleted file mode 100644 index 6a983158..00000000 --- a/src/enqueueTokenBridgePrepareSetWethGatewayTransactionRequest.unit.test.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { it, expect, vi, describe } from 'vitest'; -import { Address, PublicClient, zeroAddress, encodeFunctionData } from 'viem'; -import { arbitrumSepolia } from 'viem/chains'; - -import { enqueueTokenBridgePrepareSetWethGatewayTransactionRequest } from './enqueueTokenBridgePrepareSetWethGatewayTransactionRequest'; -import { upgradeExecutorEncodeFunctionData } from './upgradeExecutorEncodeFunctionData'; -import { isCustomFeeTokenChain } from './utils/isCustomFeeTokenChain'; - -// vi.mock factories are hoisted, so addresses must be defined via vi.hoisted -const { - rollupAddress, - account, - tokenBridgeCreatorAddr, - mockInboxAddress, - mockBridgeAddress, - mockRouterAddress, - mockWethAddress, - mockWethGatewayAddress, - mockUpgradeExecutorAddress, -} = vi.hoisted(() => ({ - rollupAddress: '0x1111111111111111111111111111111111111111' as Address, - account: '0x3333333333333333333333333333333333333333' as Address, - tokenBridgeCreatorAddr: '0x4444444444444444444444444444444444444444' as Address, - mockInboxAddress: '0x5555555555555555555555555555555555555555' as Address, - mockBridgeAddress: '0x6666666666666666666666666666666666666666' as Address, - mockRouterAddress: '0x7777777777777777777777777777777777777777' as Address, - mockWethAddress: '0x8888888888888888888888888888888888888888' as Address, - mockWethGatewayAddress: '0x9999999999999999999999999999999999999999' as Address, - mockUpgradeExecutorAddress: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' as Address, -})); - -vi.mock('./utils/isCustomFeeTokenChain', () => ({ - isCustomFeeTokenChain: vi.fn().mockResolvedValue(false), -})); - -vi.mock('./createTokenBridgeFetchTokenBridgeContracts', () => ({ - createTokenBridgeFetchTokenBridgeContracts: vi.fn().mockResolvedValue({ - parentChainContracts: { - router: mockRouterAddress, - standardGateway: '0x0000000000000000000000000000000000000000', - customGateway: '0x0000000000000000000000000000000000000000', - wethGateway: mockWethGatewayAddress, - weth: mockWethAddress, - multicall: '0x0000000000000000000000000000000000000000', - }, - orbitChainContracts: { - router: '0x0000000000000000000000000000000000000000', - standardGateway: '0x0000000000000000000000000000000000000000', - customGateway: '0x0000000000000000000000000000000000000000', - wethGateway: '0x0000000000000000000000000000000000000000', - weth: '0x0000000000000000000000000000000000000000', - proxyAdmin: '0x0000000000000000000000000000000000000000', - beaconProxyFactory: '0x0000000000000000000000000000000000000000', - upgradeExecutor: '0x0000000000000000000000000000000000000000', - multicall: '0x0000000000000000000000000000000000000000', - }, - }), -})); - -vi.mock('./createRollupFetchCoreContracts', () => ({ - createRollupFetchCoreContracts: vi.fn().mockResolvedValue({ - upgradeExecutor: mockUpgradeExecutorAddress, - rollup: rollupAddress, - inbox: mockInboxAddress, - outbox: '0x0000000000000000000000000000000000000000', - adminProxy: '0x0000000000000000000000000000000000000000', - sequencerInbox: '0x0000000000000000000000000000000000000000', - bridge: mockBridgeAddress, - validatorUtils: '0x0000000000000000000000000000000000000000', - validatorWalletCreator: '0x0000000000000000000000000000000000000000', - deployedAtBlockNumber: 0n, - }), -})); - -function createMockClient({ - registeredWethGateway = zeroAddress, -}: { - registeredWethGateway?: Address; -} = {}) { - const readContract = vi.fn().mockImplementation(({ functionName }: { functionName: string }) => { - if (functionName === 'inbox') return mockInboxAddress; - if (functionName === 'l1TokenToGateway') return registeredWethGateway; - throw new Error(`Unexpected readContract call: ${functionName}`); - }); - - const prepareTransactionRequest = vi.fn().mockResolvedValue({ - to: mockUpgradeExecutorAddress, - data: '0x', - value: 0n, - gas: 500_000n, - account, - from: account, - nonce: 0, - maxGasPrice: 1_000_000_000n, - maxPriorityFeePerGas: 0n, - }); - - return { - chain: arbitrumSepolia, - readContract, - prepareTransactionRequest, - } as unknown as PublicClient; -} - -describe('enqueueTokenBridgePrepareSetWethGatewayTransactionRequest', () => { - it('returns tx routed through UpgradeExecutor with correct deposit', async () => { - const client = createMockClient(); - const gasLimit = 100_000n; - const maxGasPrice = 200_000_000n; - const maxSubmissionCost = 50_000n; - - const result = await enqueueTokenBridgePrepareSetWethGatewayTransactionRequest({ - rollup: rollupAddress, - account, - parentChainPublicClient: client, - gasLimit, - maxGasPrice, - maxSubmissionCost, - tokenBridgeCreatorAddressOverride: tokenBridgeCreatorAddr, - }); - - expect(result.chainId).toEqual(arbitrumSepolia.id); - - const mockClient = client as unknown as { prepareTransactionRequest: ReturnType }; - const prepareCall = mockClient.prepareTransactionRequest.mock.calls[0][0]; - expect(prepareCall.to).toEqual(mockUpgradeExecutorAddress); - - const expectedDeposit = gasLimit * maxGasPrice + maxSubmissionCost; - expect(prepareCall.value).toEqual(expectedDeposit); - }); - - it('encodes setGateways calldata wrapped in UpgradeExecutor executeCall', async () => { - const client = createMockClient(); - const gasLimit = 100_000n; - const maxGasPrice = 200_000_000n; - const maxSubmissionCost = 50_000n; - - await enqueueTokenBridgePrepareSetWethGatewayTransactionRequest({ - rollup: rollupAddress, - account, - parentChainPublicClient: client, - gasLimit, - maxGasPrice, - maxSubmissionCost, - tokenBridgeCreatorAddressOverride: tokenBridgeCreatorAddr, - }); - - const setGatewaysCalldata = encodeFunctionData({ - abi: [ - { - 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, - functionName: 'setGateways', - args: [[mockWethAddress], [mockWethGatewayAddress], gasLimit, maxGasPrice, maxSubmissionCost], - }); - - const expectedData = upgradeExecutorEncodeFunctionData({ - functionName: 'executeCall', - args: [mockRouterAddress, setGatewaysCalldata], - }); - - const mockClient = client as unknown as { prepareTransactionRequest: ReturnType }; - const prepareCall = mockClient.prepareTransactionRequest.mock.calls[0][0]; - expect(prepareCall.data).toEqual(expectedData); - }); - - it('throws if custom fee token chain', async () => { - vi.mocked(isCustomFeeTokenChain).mockResolvedValueOnce(true); - - const client = createMockClient(); - - await expect( - enqueueTokenBridgePrepareSetWethGatewayTransactionRequest({ - rollup: rollupAddress, - account, - parentChainPublicClient: client, - gasLimit: 100_000n, - maxSubmissionCost: 50_000n, - tokenBridgeCreatorAddressOverride: tokenBridgeCreatorAddr, - }), - ).rejects.toThrowError( - 'chain is custom fee token chain, no need to register the weth gateway.', - ); - }); - - it('throws if WETH gateway already registered', async () => { - const client = createMockClient({ registeredWethGateway: mockWethGatewayAddress }); - - await expect( - enqueueTokenBridgePrepareSetWethGatewayTransactionRequest({ - rollup: rollupAddress, - account, - parentChainPublicClient: client, - gasLimit: 100_000n, - maxSubmissionCost: 50_000n, - tokenBridgeCreatorAddressOverride: tokenBridgeCreatorAddr, - }), - ).rejects.toThrowError('weth gateway is already registered in the router.'); - }); -}); diff --git a/src/enqueueTokenBridgePrepareTransactionRequest.unit.test.ts b/src/enqueueTokenBridgePrepareTransactionRequest.unit.test.ts deleted file mode 100644 index 14421940..00000000 --- a/src/enqueueTokenBridgePrepareTransactionRequest.unit.test.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { it, expect, vi, describe } from 'vitest'; -import { Address, PublicClient, zeroAddress, encodeFunctionData } from 'viem'; -import { arbitrumSepolia } from 'viem/chains'; - -import { tokenBridgeCreatorABI } from './contracts/TokenBridgeCreator'; -import { enqueueTokenBridgePrepareTransactionRequest } from './enqueueTokenBridgePrepareTransactionRequest'; -import { enqueueDefaultMaxGasPrice } from './constants'; - -const rollupAddress = '0x1111111111111111111111111111111111111111' as Address; -const rollupOwner = '0x2222222222222222222222222222222222222222' as Address; -const account = '0x3333333333333333333333333333333333333333' as Address; -const tokenBridgeCreatorAddr = '0x4444444444444444444444444444444444444444' as Address; -const mockInboxAddress = '0x5555555555555555555555555555555555555555' as Address; -const mockBridgeAddress = '0x6666666666666666666666666666666666666666' as Address; -const nineZeroAddresses = Array(9).fill(zeroAddress) as Address[]; - -function createMockClient({ - inboxToL2DeploymentRouter = zeroAddress, - isCustomFeeToken = false, -}: { - inboxToL2DeploymentRouter?: Address; - isCustomFeeToken?: boolean; -} = {}) { - const readContract = vi.fn().mockImplementation(({ functionName }: { functionName: string }) => { - if (functionName === 'inbox') return mockInboxAddress; - if (functionName === 'inboxToL2Deployment') { - return [inboxToL2DeploymentRouter, ...nineZeroAddresses.slice(1)]; - } - if (functionName === 'bridge') return mockBridgeAddress; - if (functionName === 'nativeToken') { - if (!isCustomFeeToken) throw new Error('revert'); - return '0x7777777777777777777777777777777777777777'; - } - throw new Error(`Unexpected readContract call: ${functionName}`); - }); - - const prepareTransactionRequest = vi.fn().mockResolvedValue({ - to: tokenBridgeCreatorAddr, - data: '0x', - value: 0n, - gas: 500_000n, - account, - from: account, - nonce: 0, - maxFeePerGas: 1_000_000_000n, - maxPriorityFeePerGas: 0n, - }); - - return { - chain: arbitrumSepolia, - readContract, - prepareTransactionRequest, - } as unknown as PublicClient; -} - -describe('enqueueTokenBridgePrepareTransactionRequest', () => { - it('returns correctly encoded tx with expected value and calldata', async () => { - const client = createMockClient(); - - const maxGasForContracts = 6_000_000n; - const maxGasForFactory = 3_000_000n; - const maxSubmissionCostForFactory = 100_000n; - const maxSubmissionCostForContracts = 200_000n; - - const result = await enqueueTokenBridgePrepareTransactionRequest({ - params: { rollup: rollupAddress, rollupOwner }, - account, - parentChainPublicClient: client, - maxGasForContracts, - maxGasForFactory, - maxSubmissionCostForFactory, - maxSubmissionCostForContracts, - gasOverrides: { gasLimit: { base: 1_000n } }, - tokenBridgeCreatorAddressOverride: tokenBridgeCreatorAddr, - }); - - const expectedRetryableFee = - maxSubmissionCostForFactory + - maxSubmissionCostForContracts + - enqueueDefaultMaxGasPrice * (maxGasForContracts + maxGasForFactory); - - expect(result.chainId).toEqual(arbitrumSepolia.id); - expect(result.gas).toEqual(1_000n); - - const mockClient = client as unknown as { prepareTransactionRequest: ReturnType }; - const prepareCall = mockClient.prepareTransactionRequest.mock.calls[0][0]; - expect(prepareCall.to).toEqual(tokenBridgeCreatorAddr); - expect(prepareCall.value).toEqual(expectedRetryableFee); - expect(prepareCall.account).toEqual(account); - - // Verify calldata encodes createTokenBridge correctly - const expectedData = encodeFunctionData({ - abi: tokenBridgeCreatorABI, - functionName: 'createTokenBridge', - args: [mockInboxAddress, rollupOwner, 6_000_000n, enqueueDefaultMaxGasPrice], - }); - expect(prepareCall.data).toEqual(expectedData); - }); - - it('throws if bridge already deployed (non-zero router from inboxToL2Deployment)', async () => { - const nonZeroRouter = '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' as Address; - const client = createMockClient({ inboxToL2DeploymentRouter: nonZeroRouter }); - - await expect( - enqueueTokenBridgePrepareTransactionRequest({ - params: { rollup: rollupAddress, rollupOwner }, - account, - parentChainPublicClient: client, - maxGasForContracts: 6_000_000n, - maxGasForFactory: 3_000_000n, - maxSubmissionCostForFactory: 100_000n, - maxSubmissionCostForContracts: 200_000n, - tokenBridgeCreatorAddressOverride: tokenBridgeCreatorAddr, - }), - ).rejects.toThrowError( - `Token bridge contracts for Rollup ${rollupAddress} are already deployed`, - ); - }); - - it('sets value to 0n for custom fee token chains', async () => { - const client = createMockClient({ isCustomFeeToken: true }); - - await enqueueTokenBridgePrepareTransactionRequest({ - params: { rollup: rollupAddress, rollupOwner }, - account, - parentChainPublicClient: client, - maxGasForContracts: 6_000_000n, - maxGasForFactory: 3_000_000n, - maxSubmissionCostForFactory: 100_000n, - maxSubmissionCostForContracts: 200_000n, - gasOverrides: { gasLimit: { base: 1_000n } }, - tokenBridgeCreatorAddressOverride: tokenBridgeCreatorAddr, - }); - - const mockClient = client as unknown as { prepareTransactionRequest: ReturnType }; - const prepareCall = mockClient.prepareTransactionRequest.mock.calls[0][0]; - expect(prepareCall.value).toEqual(0n); - }); -}); From 93336773c4283d5b4d569315d47a673440caef77 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Fri, 20 Mar 2026 12:59:40 +0000 Subject: [PATCH 10/20] fix: register network before waitForRetryables in integration test --- src/enqueueTokenBridge.integration.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/enqueueTokenBridge.integration.test.ts b/src/enqueueTokenBridge.integration.test.ts index ebbf21bf..d71bf770 100644 --- a/src/enqueueTokenBridge.integration.test.ts +++ b/src/enqueueTokenBridge.integration.test.ts @@ -9,6 +9,8 @@ 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; @@ -113,6 +115,13 @@ describe('enqueueTokenBridge', () => { ); 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, From b3f16f1dda610a9cca631344d91185cd66487b01 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Mon, 23 Mar 2026 09:53:54 +0000 Subject: [PATCH 11/20] docs: add JSDoc to enqueue prepare functions --- ...ueTokenBridgePrepareSetWethGatewayTransactionRequest.ts | 7 +++++++ src/enqueueTokenBridgePrepareTransactionRequest.ts | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/src/enqueueTokenBridgePrepareSetWethGatewayTransactionRequest.ts b/src/enqueueTokenBridgePrepareSetWethGatewayTransactionRequest.ts index a98cf9e4..08cb199a 100644 --- a/src/enqueueTokenBridgePrepareSetWethGatewayTransactionRequest.ts +++ b/src/enqueueTokenBridgePrepareSetWethGatewayTransactionRequest.ts @@ -33,6 +33,13 @@ export type EnqueueTokenBridgePrepareSetWethGatewayTransactionRequestParams< }> >; +/** + * 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 provided + * by the caller. + */ export async function enqueueTokenBridgePrepareSetWethGatewayTransactionRequest< TParentChain extends Chain | undefined, >({ diff --git a/src/enqueueTokenBridgePrepareTransactionRequest.ts b/src/enqueueTokenBridgePrepareTransactionRequest.ts index 51127a20..21373a1b 100644 --- a/src/enqueueTokenBridgePrepareTransactionRequest.ts +++ b/src/enqueueTokenBridgePrepareTransactionRequest.ts @@ -26,6 +26,12 @@ export type EnqueueTokenBridgePrepareTransactionRequestParams< }> >; +/** + * 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 provided by the caller. + */ export async function enqueueTokenBridgePrepareTransactionRequest< TParentChain extends Chain | undefined, >({ From ba3e4e80f484b0cc3d0dc53daca727497314f6f4 Mon Sep 17 00:00:00 2001 From: douglance <4741454+douglance@users.noreply.github.com> Date: Mon, 23 Mar 2026 13:53:46 -0400 Subject: [PATCH 12/20] Add prepare script to src/ for git-based installation When installing chain-sdk from a git URL with &path:src, package managers extract only the subdirectory. This prepare script goes up to the repo root and runs the TypeScript build so dist/ exists. --- src/package.json | 3 +++ 1 file changed, 3 insertions(+) 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" }, From 543b8e3fab4d2c47da2a5734259affc88fef730c Mon Sep 17 00:00:00 2001 From: TucksonDev Date: Tue, 24 Mar 2026 16:16:35 +0000 Subject: [PATCH 13/20] Add payable mutators to upgrade executor functions --- src/contracts/UpgradeExecutor.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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)', From 014e79fd5662ce569e599e1c3d2fe37e37dead4c Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Wed, 25 Mar 2026 10:20:05 +0000 Subject: [PATCH 14/20] feat: added default gas limits for enqueue token bridge variant --- src/constants.ts | 5 +++++ src/enqueueTokenBridge.integration.test.ts | 4 ---- ...ridgePrepareSetWethGatewayTransactionRequest.ts | 6 +++--- src/enqueueTokenBridgePrepareTransactionRequest.ts | 14 +++++++++----- src/index.ts | 6 ++++++ 5 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index fcdb69c6..2cc1db4a 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -14,3 +14,8 @@ 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 = 7_000_000n; +export const enqueueDefaultMaxGasForFactory = 20_000_000n; +export const enqueueDefaultGasLimitForWethGateway = 100_000n; diff --git a/src/enqueueTokenBridge.integration.test.ts b/src/enqueueTokenBridge.integration.test.ts index d71bf770..a2e680da 100644 --- a/src/enqueueTokenBridge.integration.test.ts +++ b/src/enqueueTokenBridge.integration.test.ts @@ -92,8 +92,6 @@ describe('enqueueTokenBridge', () => { }, parentChainPublicClient: nitroTestnodeL1Client, account: l2RollupOwner.address, - maxGasForContracts: 20_000_000n, - maxGasForFactory: 20_000_000n, maxSubmissionCostForFactory: 4_000_000_000_000n, maxSubmissionCostForContracts: 4_000_000_000_000n, gasOverrides: { @@ -142,8 +140,6 @@ describe('enqueueTokenBridge', () => { rollup: testnodeInformation.rollup, parentChainPublicClient: nitroTestnodeL1Client, account: l2RollupOwner.address, - gasLimit: 100_000n, - maxGasPrice: 200_000_000n, maxSubmissionCost: 4_000_000_000_000n, tokenBridgeCreatorAddressOverride: tokenBridgeCreator, }, diff --git a/src/enqueueTokenBridgePrepareSetWethGatewayTransactionRequest.ts b/src/enqueueTokenBridgePrepareSetWethGatewayTransactionRequest.ts index 08cb199a..7fc00d86 100644 --- a/src/enqueueTokenBridgePrepareSetWethGatewayTransactionRequest.ts +++ b/src/enqueueTokenBridgePrepareSetWethGatewayTransactionRequest.ts @@ -8,7 +8,7 @@ import { upgradeExecutorEncodeFunctionData } from './upgradeExecutorEncodeFuncti import { gatewayRouterABI } from './contracts/GatewayRouter'; import { Prettify } from './types/utils'; import { WithTokenBridgeCreatorAddressOverride } from './types/createTokenBridgeTypes'; -import { enqueueDefaultMaxGasPrice } from './constants'; +import { enqueueDefaultMaxGasPrice, enqueueDefaultGasLimitForWethGateway } from './constants'; export type EnqueueTokenBridgePrepareSetWethGatewayTransactionRequestParams< TParentChain extends Chain | undefined, @@ -27,7 +27,7 @@ export type EnqueueTokenBridgePrepareSetWethGatewayTransactionRequestParams< */ rollupDeploymentBlockNumber?: bigint; parentChainPublicClient: PublicClient; - gasLimit: bigint; + gasLimit?: bigint; maxGasPrice?: bigint; maxSubmissionCost: bigint; }> @@ -47,7 +47,7 @@ export async function enqueueTokenBridgePrepareSetWethGatewayTransactionRequest< account, rollupDeploymentBlockNumber, parentChainPublicClient, - gasLimit, + gasLimit = enqueueDefaultGasLimitForWethGateway, maxGasPrice = enqueueDefaultMaxGasPrice, maxSubmissionCost, tokenBridgeCreatorAddressOverride, diff --git a/src/enqueueTokenBridgePrepareTransactionRequest.ts b/src/enqueueTokenBridgePrepareTransactionRequest.ts index 21373a1b..b04853e3 100644 --- a/src/enqueueTokenBridgePrepareTransactionRequest.ts +++ b/src/enqueueTokenBridgePrepareTransactionRequest.ts @@ -8,7 +8,11 @@ import { TransactionRequestGasOverrides, applyPercentIncrease } from './utils/ga import { Prettify } from './types/utils'; import { WithTokenBridgeCreatorAddressOverride } from './types/createTokenBridgeTypes'; import { getTokenBridgeCreatorAddress } from './utils/getTokenBridgeCreatorAddress'; -import { enqueueDefaultMaxGasPrice } from './constants'; +import { + enqueueDefaultMaxGasPrice, + enqueueDefaultMaxGasForContracts, + enqueueDefaultMaxGasForFactory, +} from './constants'; export type EnqueueTokenBridgePrepareTransactionRequestParams< TParentChain extends Chain | undefined, @@ -17,8 +21,8 @@ export type EnqueueTokenBridgePrepareTransactionRequestParams< params: { rollup: Address; rollupOwner: Address }; account: Address; parentChainPublicClient: PublicClient; - maxGasForContracts: bigint; - maxGasForFactory: bigint; + maxGasForContracts?: bigint; + maxGasForFactory?: bigint; maxGasPrice?: bigint; maxSubmissionCostForFactory: bigint; maxSubmissionCostForContracts: bigint; @@ -38,8 +42,8 @@ export async function enqueueTokenBridgePrepareTransactionRequest< params, account, parentChainPublicClient, - maxGasForContracts, - maxGasForFactory, + maxGasForContracts = enqueueDefaultMaxGasForContracts, + maxGasForFactory = enqueueDefaultMaxGasForFactory, maxGasPrice = enqueueDefaultMaxGasPrice, maxSubmissionCostForFactory, maxSubmissionCostForContracts, diff --git a/src/index.ts b/src/index.ts index 99d8d7a6..127a40f7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -179,6 +179,9 @@ import { createRollupDefaultRetryablesFees, createTokenBridgeDefaultRetryablesFees, enqueueDefaultMaxGasPrice, + enqueueDefaultMaxGasForContracts, + enqueueDefaultMaxGasForFactory, + enqueueDefaultGasLimitForWethGateway, } from './constants'; import { enqueueTokenBridgePrepareTransactionRequest, @@ -358,6 +361,9 @@ export { createTokenBridgeDefaultRetryablesFees, // enqueueDefaultMaxGasPrice, + enqueueDefaultMaxGasForContracts, + enqueueDefaultMaxGasForFactory, + enqueueDefaultGasLimitForWethGateway, enqueueTokenBridgePrepareTransactionRequest, EnqueueTokenBridgePrepareTransactionRequestParams, enqueueTokenBridgePrepareSetWethGatewayTransactionRequest, From b89500617ec0fdc14015f0aa82d378cef050f365 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Wed, 25 Mar 2026 11:45:50 +0000 Subject: [PATCH 15/20] refactor: extract shared data-size helpers for retryable submission cost Extract getFactoryDeploymentDataSize and getContractsDeploymentData from the existing estimation functions so that the enqueue variants can reuse the same data-size logic for calculating retryable submission fees. --- src/createTokenBridge-ethers.ts | 119 +++++++++--------- ...ateTokenBridgePrepareTransactionRequest.ts | 1 - 2 files changed, 58 insertions(+), 62 deletions(-) diff --git a/src/createTokenBridge-ethers.ts b/src/createTokenBridge-ethers.ts index 48a80994..698987ad 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, @@ -52,7 +51,6 @@ export async function createTokenBridgeGetInputs< maxSubmissionCost: maxSubmissionCostForFactoryEstimation, maxGas: maxGasForFactoryEstimation, } = await getEstimateForDeployingFactory( - l1DeployerAddress, l1TokenBridgeCreatorAddress, l1Provider, l2Provider, @@ -62,7 +60,6 @@ export async function createTokenBridgeGetInputs< maxSubmissionCost: maxSubmissionCostForContractsEstimation, maxGas: maxGasForContractsEstimation, } = await getEstimateForDeployingContracts( - l1DeployerAddress, l1TokenBridgeCreatorAddress, l1Provider, l2Provider, @@ -138,65 +135,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( @@ -206,15 +161,11 @@ async function getEstimateForDeployingContracts( wethGateway: await l1Provider.getCode(await l1TokenBridgeCreator.l2WethGatewayTemplate()), aeWeth: await l1Provider.getCode(await l1TokenBridgeCreator.l2WethTemplate()), upgradeExecutor: await l1Provider.getCode( - ( - await l1TokenBridgeCreator.l1Templates() - ).upgradeExecutor, + (await l1TokenBridgeCreator.l1Templates()).upgradeExecutor, ), 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 +178,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 +245,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/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, From 2adcbc351761150842bc067be7a9e70dbad4f179 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Wed, 25 Mar 2026 11:46:17 +0000 Subject: [PATCH 16/20] feat: add calculateRetryableSubmissionFee for enqueue variants Replicates the SDK's estimateSubmissionFee logic: calls the Inbox's calculateRetryableSubmissionFee on the parent chain with the current base fee, then applies a 300% safety buffer matching the SDK default. --- src/calculateRetryableSubmissionFee.ts | 28 ++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/calculateRetryableSubmissionFee.ts 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; +} From 867a3f3baa645324a66b7654d009ccedc1904b3f Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Wed, 25 Mar 2026 11:49:22 +0000 Subject: [PATCH 17/20] feat: calculate retryable submission costs internally in enqueue functions The enqueue functions now estimate maxSubmissionCost from parent chain state using the shared data-size helpers and the Inbox's calculateRetryableSubmissionFee, matching the approach used by the create* variants via the SDK. Callers no longer need to provide maxSubmissionCostForFactory, maxSubmissionCostForContracts, or maxSubmissionCost. --- src/enqueueTokenBridge.integration.test.ts | 3 --- ...PrepareSetWethGatewayTransactionRequest.ts | 26 ++++++++++++++++--- ...eueTokenBridgePrepareTransactionRequest.ts | 24 +++++++++++++---- 3 files changed, 41 insertions(+), 12 deletions(-) diff --git a/src/enqueueTokenBridge.integration.test.ts b/src/enqueueTokenBridge.integration.test.ts index a2e680da..2f90203c 100644 --- a/src/enqueueTokenBridge.integration.test.ts +++ b/src/enqueueTokenBridge.integration.test.ts @@ -92,8 +92,6 @@ describe('enqueueTokenBridge', () => { }, parentChainPublicClient: nitroTestnodeL1Client, account: l2RollupOwner.address, - maxSubmissionCostForFactory: 4_000_000_000_000n, - maxSubmissionCostForContracts: 4_000_000_000_000n, gasOverrides: { gasLimit: { base: 6_000_000n, @@ -140,7 +138,6 @@ describe('enqueueTokenBridge', () => { rollup: testnodeInformation.rollup, parentChainPublicClient: nitroTestnodeL1Client, account: l2RollupOwner.address, - maxSubmissionCost: 4_000_000_000_000n, tokenBridgeCreatorAddressOverride: tokenBridgeCreator, }, ); diff --git a/src/enqueueTokenBridgePrepareSetWethGatewayTransactionRequest.ts b/src/enqueueTokenBridgePrepareSetWethGatewayTransactionRequest.ts index 7fc00d86..d0322200 100644 --- a/src/enqueueTokenBridgePrepareSetWethGatewayTransactionRequest.ts +++ b/src/enqueueTokenBridgePrepareSetWethGatewayTransactionRequest.ts @@ -9,6 +9,7 @@ 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, @@ -29,7 +30,6 @@ export type EnqueueTokenBridgePrepareSetWethGatewayTransactionRequestParams< parentChainPublicClient: PublicClient; gasLimit?: bigint; maxGasPrice?: bigint; - maxSubmissionCost: bigint; }> >; @@ -37,8 +37,8 @@ export type EnqueueTokenBridgePrepareSetWethGatewayTransactionRequestParams< * 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 provided - * by the caller. + * 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, @@ -49,7 +49,6 @@ export async function enqueueTokenBridgePrepareSetWethGatewayTransactionRequest< parentChainPublicClient, gasLimit = enqueueDefaultGasLimitForWethGateway, maxGasPrice = enqueueDefaultMaxGasPrice, - maxSubmissionCost, tokenBridgeCreatorAddressOverride, }: EnqueueTokenBridgePrepareSetWethGatewayTransactionRequestParams) { const { chainId } = validateParentChain(parentChainPublicClient); @@ -91,6 +90,25 @@ export async function enqueueTokenBridgePrepareSetWethGatewayTransactionRequest< 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({ diff --git a/src/enqueueTokenBridgePrepareTransactionRequest.ts b/src/enqueueTokenBridgePrepareTransactionRequest.ts index b04853e3..ee0de364 100644 --- a/src/enqueueTokenBridgePrepareTransactionRequest.ts +++ b/src/enqueueTokenBridgePrepareTransactionRequest.ts @@ -13,6 +13,12 @@ import { enqueueDefaultMaxGasForContracts, enqueueDefaultMaxGasForFactory, } 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, @@ -24,8 +30,6 @@ export type EnqueueTokenBridgePrepareTransactionRequestParams< maxGasForContracts?: bigint; maxGasForFactory?: bigint; maxGasPrice?: bigint; - maxSubmissionCostForFactory: bigint; - maxSubmissionCostForContracts: bigint; gasOverrides?: TransactionRequestGasOverrides; }> >; @@ -34,7 +38,8 @@ export type EnqueueTokenBridgePrepareTransactionRequestParams< * 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 provided by the caller. + * does not require an orbit chain connection -- retryable gas parameters are estimated from + * parent chain state. */ export async function enqueueTokenBridgePrepareTransactionRequest< TParentChain extends Chain | undefined, @@ -45,8 +50,6 @@ export async function enqueueTokenBridgePrepareTransactionRequest< maxGasForContracts = enqueueDefaultMaxGasForContracts, maxGasForFactory = enqueueDefaultMaxGasForFactory, maxGasPrice = enqueueDefaultMaxGasPrice, - maxSubmissionCostForFactory, - maxSubmissionCostForContracts, gasOverrides, tokenBridgeCreatorAddressOverride, }: EnqueueTokenBridgePrepareTransactionRequestParams) { @@ -74,6 +77,17 @@ export async function enqueueTokenBridgePrepareTransactionRequest< throw new Error(`Token bridge contracts for Rollup ${params.rollup} are already deployed`); } + 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, From 8479125cf054adb0369378c0d28bdd985380cc56 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Wed, 25 Mar 2026 12:19:45 +0000 Subject: [PATCH 18/20] fix: swap default gas limits for factory and contracts retryables The contracts deployment (7+ contracts + initialization) needs more gas than the factory deployment (single contract creation). --- src/constants.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 2cc1db4a..9eeee700 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -16,6 +16,6 @@ export const createTokenBridgeDefaultRetryablesFees = parseEther('0.02'); export const enqueueDefaultMaxGasPrice = parseGwei('0.2'); // ~30% headroom over observed gas usage for token bridge retryables -export const enqueueDefaultMaxGasForContracts = 7_000_000n; -export const enqueueDefaultMaxGasForFactory = 20_000_000n; +export const enqueueDefaultMaxGasForContracts = 20_000_000n; +export const enqueueDefaultMaxGasForFactory = 7_000_000n; export const enqueueDefaultGasLimitForWethGateway = 100_000n; From 192a9cf91489a826fdf875ba4d4de9526145710f Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Wed, 25 Mar 2026 12:38:58 +0000 Subject: [PATCH 19/20] refactor: read factory gas limit from TokenBridgeCreator contract Instead of using a hardcoded default, read gasLimitForL2FactoryDeployment from the TokenBridgeCreator contract. The caller can still override via the maxGasForFactory parameter. --- src/constants.ts | 1 - src/enqueueTokenBridgePrepareTransactionRequest.ts | 11 +++++++++-- src/index.ts | 4 ++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 9eeee700..dc6554c1 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -17,5 +17,4 @@ export const enqueueDefaultMaxGasPrice = parseGwei('0.2'); // ~30% headroom over observed gas usage for token bridge retryables export const enqueueDefaultMaxGasForContracts = 20_000_000n; -export const enqueueDefaultMaxGasForFactory = 7_000_000n; export const enqueueDefaultGasLimitForWethGateway = 100_000n; diff --git a/src/enqueueTokenBridgePrepareTransactionRequest.ts b/src/enqueueTokenBridgePrepareTransactionRequest.ts index ee0de364..8c5e48c5 100644 --- a/src/enqueueTokenBridgePrepareTransactionRequest.ts +++ b/src/enqueueTokenBridgePrepareTransactionRequest.ts @@ -11,7 +11,6 @@ import { getTokenBridgeCreatorAddress } from './utils/getTokenBridgeCreatorAddre import { enqueueDefaultMaxGasPrice, enqueueDefaultMaxGasForContracts, - enqueueDefaultMaxGasForFactory, } from './constants'; import { getFactoryDeploymentDataSize, @@ -48,7 +47,7 @@ export async function enqueueTokenBridgePrepareTransactionRequest< account, parentChainPublicClient, maxGasForContracts = enqueueDefaultMaxGasForContracts, - maxGasForFactory = enqueueDefaultMaxGasForFactory, + maxGasForFactory: maxGasForFactoryOverride, maxGasPrice = enqueueDefaultMaxGasPrice, gasOverrides, tokenBridgeCreatorAddressOverride, @@ -77,6 +76,14 @@ export async function enqueueTokenBridgePrepareTransactionRequest< 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, diff --git a/src/index.ts b/src/index.ts index 127a40f7..6e7facc0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -180,7 +180,7 @@ import { createTokenBridgeDefaultRetryablesFees, enqueueDefaultMaxGasPrice, enqueueDefaultMaxGasForContracts, - enqueueDefaultMaxGasForFactory, + enqueueDefaultGasLimitForWethGateway, } from './constants'; import { @@ -362,7 +362,7 @@ export { // enqueueDefaultMaxGasPrice, enqueueDefaultMaxGasForContracts, - enqueueDefaultMaxGasForFactory, + enqueueDefaultGasLimitForWethGateway, enqueueTokenBridgePrepareTransactionRequest, EnqueueTokenBridgePrepareTransactionRequestParams, From 33b7b6bcce4463320ff219ffdee716ba0cd7c4ff Mon Sep 17 00:00:00 2001 From: Doug Lance <4741454+douglance@users.noreply.github.com> Date: Tue, 31 Mar 2026 21:08:19 -0400 Subject: [PATCH 20/20] chore: fix Prettier formatting and update audit-ci allowlist Apply Prettier formatting and allowlist 6 transitive dev-only CVEs in audit-ci.jsonc (flatted, picomatch, brace-expansion, yaml). --- .github/workflows/sbom-export.yaml | 2 +- audit-ci.jsonc | 38 ++++++++++++++++++- eslint.config.js | 6 +-- src/createTokenBridge-ethers.ts | 16 +++----- ...eueTokenBridgePrepareTransactionRequest.ts | 11 +++--- src/index.ts | 2 - 6 files changed, 52 insertions(+), 23 deletions(-) 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/createTokenBridge-ethers.ts b/src/createTokenBridge-ethers.ts index 698987ad..140654b0 100644 --- a/src/createTokenBridge-ethers.ts +++ b/src/createTokenBridge-ethers.ts @@ -50,20 +50,12 @@ export async function createTokenBridgeGetInputs< const { maxSubmissionCost: maxSubmissionCostForFactoryEstimation, maxGas: maxGasForFactoryEstimation, - } = await getEstimateForDeployingFactory( - l1TokenBridgeCreatorAddress, - l1Provider, - l2Provider, - ); + } = await getEstimateForDeployingFactory(l1TokenBridgeCreatorAddress, l1Provider, l2Provider); const { maxSubmissionCost: maxSubmissionCostForContractsEstimation, maxGas: maxGasForContractsEstimation, - } = await getEstimateForDeployingContracts( - l1TokenBridgeCreatorAddress, - l1Provider, - l2Provider, - ); + } = await getEstimateForDeployingContracts(l1TokenBridgeCreatorAddress, l1Provider, l2Provider); //// apply gas overrides const maxSubmissionCostForFactory = @@ -161,7 +153,9 @@ export async function getContractsDeploymentData( wethGateway: await l1Provider.getCode(await l1TokenBridgeCreator.l2WethGatewayTemplate()), aeWeth: await l1Provider.getCode(await l1TokenBridgeCreator.l2WethTemplate()), upgradeExecutor: await l1Provider.getCode( - (await l1TokenBridgeCreator.l1Templates()).upgradeExecutor, + ( + await l1TokenBridgeCreator.l1Templates() + ).upgradeExecutor, ), multicall: await l1Provider.getCode(await l1TokenBridgeCreator.l2MulticallTemplate()), }; diff --git a/src/enqueueTokenBridgePrepareTransactionRequest.ts b/src/enqueueTokenBridgePrepareTransactionRequest.ts index 8c5e48c5..d47a47bb 100644 --- a/src/enqueueTokenBridgePrepareTransactionRequest.ts +++ b/src/enqueueTokenBridgePrepareTransactionRequest.ts @@ -8,10 +8,7 @@ import { TransactionRequestGasOverrides, applyPercentIncrease } from './utils/ga import { Prettify } from './types/utils'; import { WithTokenBridgeCreatorAddressOverride } from './types/createTokenBridgeTypes'; import { getTokenBridgeCreatorAddress } from './utils/getTokenBridgeCreatorAddress'; -import { - enqueueDefaultMaxGasPrice, - enqueueDefaultMaxGasForContracts, -} from './constants'; +import { enqueueDefaultMaxGasPrice, enqueueDefaultMaxGasForContracts } from './constants'; import { getFactoryDeploymentDataSize, getContractsDeploymentData, @@ -91,7 +88,11 @@ export async function enqueueTokenBridgePrepareTransactionRequest< ); const [maxSubmissionCostForFactory, maxSubmissionCostForContracts] = await Promise.all([ - calculateRetryableSubmissionFee(parentChainPublicClient, inbox, BigInt(getFactoryDeploymentDataSize())), + calculateRetryableSubmissionFee( + parentChainPublicClient, + inbox, + BigInt(getFactoryDeploymentDataSize()), + ), calculateRetryableSubmissionFee(parentChainPublicClient, inbox, BigInt(contractsDataSize)), ]); diff --git a/src/index.ts b/src/index.ts index 6e7facc0..c9cce085 100644 --- a/src/index.ts +++ b/src/index.ts @@ -180,7 +180,6 @@ import { createTokenBridgeDefaultRetryablesFees, enqueueDefaultMaxGasPrice, enqueueDefaultMaxGasForContracts, - enqueueDefaultGasLimitForWethGateway, } from './constants'; import { @@ -362,7 +361,6 @@ export { // enqueueDefaultMaxGasPrice, enqueueDefaultMaxGasForContracts, - enqueueDefaultGasLimitForWethGateway, enqueueTokenBridgePrepareTransactionRequest, EnqueueTokenBridgePrepareTransactionRequestParams,