From f84fa738dc5d82d73f9ad93c6968c9624de5a33d Mon Sep 17 00:00:00 2001 From: douglance <4741454+douglance@users.noreply.github.com> Date: Wed, 24 Jun 2026 17:32:23 -0400 Subject: [PATCH 1/5] ci: migrate integration tests to arbitrum-testnode composite action Replace the live run-nitro-test-node build with the prebuilt OffchainLabs/arbitrum-testnode@v0.2.7 composite action across the v3.2 and v2.1 custom-gas-token integration matrix (6 & 18 decimals), and refresh the audit allowlist. --- .github/workflows/build-test.yml | 31 +++++++++++++------------ audit-ci.jsonc | 39 +++++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index b340c4e22..d9a399cbc 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -161,26 +161,27 @@ jobs: name: Test (Integration) - ${{ matrix.config.name }} runs-on: ubuntu-latest strategy: + fail-fast: false matrix: config: - name: Nitro contracts v3.2 - Custom gas token with 18 decimals - nitro-contracts-branch: v3.2.0 - args: --tokenbridge --l3node --l3-token-bridge --l3-fee-token + version: v0.2.7 + nitro-contracts-version: v3.2 decimals: 18 - name: Nitro contracts v3.2 - Custom gas token with 6 decimals - nitro-contracts-branch: v3.2.0 - args: --tokenbridge --l3node --l3-token-bridge --l3-fee-token --l3-fee-token-decimals 6 + version: v0.2.7 + nitro-contracts-version: v3.2 decimals: 6 - name: Nitro contracts v2.1 - Custom gas token with 18 decimals - nitro-contracts-branch: v2.1.3 - args: --tokenbridge --l3node --l3-token-bridge --l3-fee-token + version: v0.2.7 + nitro-contracts-version: v2.1 decimals: 18 - name: Nitro contracts v2.1 - Custom gas token with 6 decimals - nitro-contracts-branch: v2.1.3 - args: --tokenbridge --l3node --l3-token-bridge --l3-fee-token --l3-fee-token-decimals 6 + version: v0.2.7 + nitro-contracts-version: v2.1 decimals: 6 steps: @@ -205,11 +206,13 @@ jobs: cache-key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }} - name: Set up the local node - uses: OffchainLabs/actions/run-nitro-test-node@feat-simplify + uses: OffchainLabs/arbitrum-testnode@v0.2.7 with: - nitro-testnode-ref: release - nitro-contracts-branch: ${{ matrix.config.nitro-contracts-branch }} - args: ${{ matrix.config.args }} + version: ${{ matrix.config.version }} + l3-enabled: true + fee-token-decimals: ${{ matrix.config.decimals }} + nitro-contracts-version: ${{ matrix.config.nitro-contracts-version }} + github-token: ${{ secrets.GITHUB_TOKEN }} - name: Copy .env run: cp ./.env.example ./.env @@ -219,6 +222,6 @@ jobs: - name: Test run: | - INTEGRATION_TEST_DECIMALS=${{matrix.config.decimals}} \ - INTEGRATION_TEST_NITRO_CONTRACTS_BRANCH=${{matrix.config.nitro-contracts-branch}} \ + INTEGRATION_TEST_DECIMALS=${{ matrix.config.decimals }} \ + INTEGRATION_TEST_NITRO_CONTRACTS_BRANCH=${{ matrix.config.nitro-contracts-version }} \ pnpm test:integration diff --git a/audit-ci.jsonc b/audit-ci.jsonc index ed9515626..9f7d4f380 100644 --- a/audit-ci.jsonc +++ b/audit-ci.jsonc @@ -70,7 +70,44 @@ // 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" ] } From 76f9fa0f8e9e89e337db40d246db394042450582 Mon Sep 17 00:00:00 2001 From: douglance <4741454+douglance@users.noreply.github.com> Date: Wed, 24 Jun 2026 17:32:33 -0400 Subject: [PATCH 2/5] feat: support v2.1/v3.2 custom-gas rollups and creator-scoped token bridge reads - createRollup: thread RollupCreator v2.1 vs v3.2 differences through fee, call-value, custom-fee-token allowance/approval, and transaction-request building. - createTokenBridge: let getTokenBridgeContracts / createTokenBridge accept a tokenBridgeCreatorAddressOverride so the inbox->deployment mapping is read from the creator that performed the deployment, not the chain default. --- src/contracts/RollupCreator/v2.1.ts | 2 +- src/contracts/RollupCreator/v3.2.ts | 2 +- src/createRollup.ts | 10 ++++++++ ...eateRollupEnoughCustomFeeTokenAllowance.ts | 1 + src/createRollupGetCallValue.ts | 1 + src/createRollupGetRetryablesFees.ts | 25 ++++++++++++++++--- ...ustomFeeTokenApprovalTransactionRequest.ts | 1 + src/createRollupPrepareTransactionRequest.ts | 6 ++++- src/createTokenBridge.ts | 1 + ...ateTokenBridgePrepareTransactionReceipt.ts | 10 +++++++- wagmi.config.ts | 2 +- 11 files changed, 52 insertions(+), 9 deletions(-) diff --git a/src/contracts/RollupCreator/v2.1.ts b/src/contracts/RollupCreator/v2.1.ts index fb93b7317..61879d813 100644 --- a/src/contracts/RollupCreator/v2.1.ts +++ b/src/contracts/RollupCreator/v2.1.ts @@ -218,7 +218,7 @@ export const rollupCreatorAddress = { 42161: '0x79607f00e61E6d7C0E6330bd7E9c4AC320D50FC9', 42170: '0x9B523BF5F77e8d90e0E9eb0924aEA6E40B081aE6', 84532: '0x6e244cD02BBB8a6dbd7F626f05B2ef82151Ab502', - 412346: '0x4287839696d650A0cf93b98351e85199102335D0', + 412346: '0x1E08B9c3f94E9aBcc531f67F949d796eC76963b9', 421614: '0xd2Ec8376B1dF436fAb18120E416d3F2BeC61275b', 11155111: '0xfb774eA8A92ae528A596c8D90CBCF1bdBC4Cee79', } as const; diff --git a/src/contracts/RollupCreator/v3.2.ts b/src/contracts/RollupCreator/v3.2.ts index 0fa4e849e..cc7ae3cd2 100644 --- a/src/contracts/RollupCreator/v3.2.ts +++ b/src/contracts/RollupCreator/v3.2.ts @@ -266,7 +266,7 @@ export const rollupCreatorAddress = { 42161: '0xF5962AD061A1aD6F38F340F5b267b3593cC1Cd7B', 42170: '0xF5962AD061A1aD6F38F340F5b267b3593cC1Cd7B', 84532: '0x8d1668636D053C10F57367D68118bD624f41ffe6', - 412346: '0xbcF51F3AAb5D5Efa025b4A2B235BDc9F3f69b4d2', + 412346: '0x1E08B9c3f94E9aBcc531f67F949d796eC76963b9', 421614: '0xF5962AD061A1aD6F38F340F5b267b3593cC1Cd7B', 11155111: '0xe06Bc77336E201c4C08751918A4bB99ddf0e1Bf7', } as const; diff --git a/src/createRollup.ts b/src/createRollup.ts index cc52e7c9c..c43a2955d 100644 --- a/src/createRollup.ts +++ b/src/createRollup.ts @@ -21,6 +21,7 @@ type EnsureCustomGasTokenAllowanceGrantedToRollupCreatorParams; account: PrivateKeyAccount; rollupCreatorVersion?: RollupCreatorSupportedVersion; + rollupCreatorAddressOverride?: Address; }; /** @@ -38,12 +39,14 @@ async function ensureCustomGasTokenAllowanceGrantedToRollupCreator< parentChainPublicClient, account, rollupCreatorVersion = 'v3.2', + rollupCreatorAddressOverride, }: EnsureCustomGasTokenAllowanceGrantedToRollupCreatorParams) { const allowanceParams = { nativeToken, account: account.address, publicClient: parentChainPublicClient, rollupCreatorVersion, + rollupCreatorAddressOverride, }; if (!(await createRollupEnoughCustomFeeTokenAllowance(allowanceParams))) { @@ -82,18 +85,21 @@ export type CreateRollupFunctionParams = account: PrivateKeyAccount; parentChainPublicClient: PublicClient; rollupCreatorVersion: 'v2.1'; + rollupCreatorAddressOverride?: Address; } | { params: CreateRollupParams<'v3.2'>; account: PrivateKeyAccount; parentChainPublicClient: PublicClient; rollupCreatorVersion: 'v3.2'; + rollupCreatorAddressOverride?: Address; } | { params: CreateRollupParams<'v3.2'>; account: PrivateKeyAccount; parentChainPublicClient: PublicClient; rollupCreatorVersion?: never; + rollupCreatorAddressOverride?: Address; }; /** @@ -173,6 +179,7 @@ export async function createRollup({ account, parentChainPublicClient, rollupCreatorVersion = 'v3.2', + rollupCreatorAddressOverride, }: CreateRollupFunctionParams): Promise { validateParentChain(parentChainPublicClient); @@ -186,6 +193,7 @@ export async function createRollup({ parentChainPublicClient, account, rollupCreatorVersion, + rollupCreatorAddressOverride, }); } @@ -197,12 +205,14 @@ export async function createRollup({ account: account.address, publicClient: parentChainPublicClient, rollupCreatorVersion: 'v2.1', + rollupCreatorAddressOverride, }) : await createRollupPrepareTransactionRequest({ params: params as CreateRollupParams<'v3.2'>, account: account.address, publicClient: parentChainPublicClient, rollupCreatorVersion: 'v3.2', + rollupCreatorAddressOverride, }); // sign and send the transaction diff --git a/src/createRollupEnoughCustomFeeTokenAllowance.ts b/src/createRollupEnoughCustomFeeTokenAllowance.ts index c7521e5d1..99740607f 100644 --- a/src/createRollupEnoughCustomFeeTokenAllowance.ts +++ b/src/createRollupEnoughCustomFeeTokenAllowance.ts @@ -41,6 +41,7 @@ export async function createRollupEnoughCustomFeeTokenAllowance account: Address; nativeToken: Address; deployFactoriesToL2: boolean; + rollupCreatorAddressOverride?: Address; }, rollupCreatorVersion: RollupCreatorSupportedVersion = 'v3.2', ): Promise { diff --git a/src/createRollupGetRetryablesFees.ts b/src/createRollupGetRetryablesFees.ts index fca583fc5..3651c0033 100644 --- a/src/createRollupGetRetryablesFees.ts +++ b/src/createRollupGetRetryablesFees.ts @@ -102,10 +102,11 @@ const deployHelperABI = [ async function getTemplates( publicClient: PublicClient, rollupCreatorVersion: RollupCreatorSupportedVersion, + rollupCreatorAddress: Address, ) { const bridgeCreatorAddress = await publicClient.readContract({ abi: rollupCreatorABI, - address: getRollupCreatorAddress(publicClient, rollupCreatorVersion), + address: rollupCreatorAddress, functionName: 'bridgeCreator', }); @@ -150,6 +151,7 @@ export type CreateRollupGetRetryablesFeesParams = { account: Address; nativeToken?: Address; maxFeePerGasForRetryables?: bigint; + rollupCreatorAddressOverride?: Address; }; /** @@ -165,18 +167,27 @@ export type CreateRollupGetRetryablesFeesParams = { */ export async function createRollupGetRetryablesFees( publicClient: PublicClient, - { account, nativeToken, maxFeePerGasForRetryables }: CreateRollupGetRetryablesFeesParams, + { + account, + nativeToken, + maxFeePerGasForRetryables, + rollupCreatorAddressOverride, + }: CreateRollupGetRetryablesFeesParams, rollupCreatorVersion: RollupCreatorSupportedVersion = 'v3.2', ): Promise { + const rollupCreatorAddress = + rollupCreatorAddressOverride ?? getRollupCreatorAddress(publicClient, rollupCreatorVersion); + const deployHelperAddress = await publicClient.readContract({ abi: rollupCreatorABI, - address: getRollupCreatorAddress(publicClient, rollupCreatorVersion), + address: rollupCreatorAddress, functionName: 'l2FactoriesDeployer', }); const [ethTemplateInbox, erc20TemplateInbox] = await getTemplates( publicClient, rollupCreatorVersion, + rollupCreatorAddress, ); const isCustomGasToken = isNonZeroAddress(nativeToken); @@ -246,7 +257,12 @@ export async function createRollupGetRetryablesFees( publicClient: PublicClient, - { account, nativeToken, maxFeePerGasForRetryables }: CreateRollupGetRetryablesFeesParams, + { + account, + nativeToken, + maxFeePerGasForRetryables, + rollupCreatorAddressOverride, + }: CreateRollupGetRetryablesFeesParams, rollupCreatorVersion: RollupCreatorSupportedVersion = 'v3.2', ): Promise { try { @@ -256,6 +272,7 @@ export async function createRollupGetRetryablesFeesWithDefaults = { parentChainPublicClient: PublicClient; + // When the token bridge was deployed through a non-default creator (e.g. a freshly deployed + // one), the inbox->deployment mapping lives on that creator, so it must be queried there. + tokenBridgeCreatorAddressOverride?: Address; }; export type CreateTokenBridgeTransactionReceipt< @@ -108,7 +112,10 @@ export function createTokenBridgePrepareTransactionReceipt< ) as WaitForRetryablesResult ); }, - getTokenBridgeContracts: async function ({ parentChainPublicClient }) { + getTokenBridgeContracts: async function ({ + parentChainPublicClient, + tokenBridgeCreatorAddressOverride, + }) { const eventLog = findOrbitTokenBridgeCreatedEventLog(txReceipt); const decodedEventLog = decodeOrbitTokenBridgeCreatedEventLog(eventLog); const { inbox } = decodedEventLog.args; @@ -116,6 +123,7 @@ export function createTokenBridgePrepareTransactionReceipt< return createTokenBridgeFetchTokenBridgeContracts({ inbox, parentChainPublicClient, + tokenBridgeCreatorAddressOverride, }); }, }; diff --git a/wagmi.config.ts b/wagmi.config.ts index fc7a168ca..56c735aa0 100644 --- a/wagmi.config.ts +++ b/wagmi.config.ts @@ -101,7 +101,7 @@ const contracts: ContractConfig[] = [ [baseSepolia.id]: '0x8d1668636D053C10F57367D68118bD624f41ffe6', // local nitro-testnode (on "release" branch with --tokenbridge --l3node --l3-token-bridge flags) [nitroTestnodeL1.id]: '0xe6D50099f4d891240435143193d46581A1447202', - [nitroTestnodeL2.id]: '0xbcF51F3AAb5D5Efa025b4A2B235BDc9F3f69b4d2', + [nitroTestnodeL2.id]: '0x1E08B9c3f94E9aBcc531f67F949d796eC76963b9', }, }, { From e0eaf836d5ea6f202afd6a379119fb140695ed98 Mon Sep 17 00:00:00 2001 From: douglance <4741454+douglance@users.noreply.github.com> Date: Wed, 24 Jun 2026 17:32:43 -0400 Subject: [PATCH 3/5] test: adapt integration suite to the prebuilt arbitrum-testnode snapshot Read deployment metadata (rollup-creator, L3 chain-owner upgrade executor) from the testnode config, fund deployer accounts with the custom fee token (scaled to the token's decimals), route owner actions through the L3-side upgrade executor, and query the freshly-deployed token bridge creator. --- .../sequencerInbox.integration.test.ts | 10 +- src/createRollup.integration.test.ts | 4 + src/createTokenBridge.integration.test.ts | 15 +- ...ActionsUpgradeExecutor.integration.test.ts | 7 +- ...dminLogicPublicActions.integration.test.ts | 13 +- .../sequencerInboxActions.integration.test.ts | 5 +- src/getKeysets.integration.test.ts | 3 +- src/getValidators.integration.test.ts | 13 +- src/testHelpers.ts | 134 +++++++++++++++++- 9 files changed, 166 insertions(+), 38 deletions(-) diff --git a/src/actions/sequencerInbox.integration.test.ts b/src/actions/sequencerInbox.integration.test.ts index 56ad10743..815b1a18b 100644 --- a/src/actions/sequencerInbox.integration.test.ts +++ b/src/actions/sequencerInbox.integration.test.ts @@ -16,7 +16,8 @@ import { buildDisableBatchPoster, buildEnableBatchPoster } from './buildSetIsBat import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'; import { buildInvalidateKeysetHash } from './buildInvalidateKeysetHash'; -const { l3SequencerInbox, l3BatchPoster, l3UpgradeExecutor } = getInformationFromTestnode(); +const { l3SequencerInbox, l3BatchPoster, l3UpgradeExecutor, l2RollupCreator } = + getInformationFromTestnode(); const { l3TokenBridgeDeployer, deployer, l3RollupOwner } = getNitroTestnodePrivateKeyAccounts(); const client = createPublicClient({ @@ -26,9 +27,9 @@ const client = createPublicClient({ describe('max time variation management', () => { const defaultMaxTimeVariation = { - delayBlocks: 5_760n, - delaySeconds: 86_400n, - futureBlocks: 12n, + delayBlocks: 28_800n, + delaySeconds: 345_600n, + futureBlocks: 300n, futureSeconds: 3_600n, }; it('getMaxTimeVariation successfully fetches max time variation', async () => { @@ -161,6 +162,7 @@ describe('keyset management', () => { validators, nativeToken: zeroAddress, client, + rollupCreatorAddressOverride: l2RollupCreator, }); const { sequencerInbox, upgradeExecutor } = createRollupInformation.coreContracts; diff --git a/src/createRollup.integration.test.ts b/src/createRollup.integration.test.ts index 98114fafa..5281439d4 100644 --- a/src/createRollup.integration.test.ts +++ b/src/createRollup.integration.test.ts @@ -20,6 +20,8 @@ const l3TokenBridgeDeployer = testnodeAccounts.l3TokenBridgeDeployer; const batchPosters = [testnodeAccounts.deployer.address]; const validators = [testnodeAccounts.deployer.address]; +const { l2RollupCreator } = getInformationFromTestnode(); + describe(`create an AnyTrust chain that uses ETH as gas token`, async () => { const { createRollupConfig, createRollupInformation } = await createRollupHelper({ deployer: l3TokenBridgeDeployer, @@ -27,6 +29,7 @@ describe(`create an AnyTrust chain that uses ETH as gas token`, async () => { validators, nativeToken: zeroAddress, client: parentChainPublicClient, + rollupCreatorAddressOverride: l2RollupCreator, }); it(`successfully deploys core contracts through rollup creator`, async () => { @@ -66,6 +69,7 @@ describe(`create an AnyTrust chain that uses a custom gas token`, async () => { validators, nativeToken, client: parentChainPublicClient, + rollupCreatorAddressOverride: l2RollupCreator, }); it(`successfully deploys core contracts through rollup creator`, async () => { diff --git a/src/createTokenBridge.integration.test.ts b/src/createTokenBridge.integration.test.ts index c9778dc3c..85da7525b 100644 --- a/src/createTokenBridge.integration.test.ts +++ b/src/createTokenBridge.integration.test.ts @@ -20,7 +20,6 @@ import { publicClientToProvider } from './ethers-compat/publicClientToProvider'; const testnodeAccounts = getNitroTestnodePrivateKeyAccounts(); const l2RollupOwner = testnodeAccounts.l2RollupOwner; const l3RollupOwner = testnodeAccounts.l3RollupOwner; -const l3TokenBridgeDeployer = testnodeAccounts.l3TokenBridgeDeployer; const nitroTestnodeL1Client = createPublicClient({ chain: nitroTestnodeL1, @@ -168,9 +167,10 @@ describe('createTokenBridge utils function', () => { expect(orbitChainRetryableReceipts[0].status).toEqual('success'); expect(orbitChainRetryableReceipts[1].status).toEqual('success'); - // get contracts + // get contracts (query the freshly-deployed creator that holds the deployment mapping) const tokenBridgeContracts = await txReceipt.getTokenBridgeContracts({ parentChainPublicClient: nitroTestnodeL1Client, + tokenBridgeCreatorAddressOverride: tokenBridgeCreator, }); checkTokenBridgeContracts(tokenBridgeContracts); @@ -229,13 +229,13 @@ describe('createTokenBridge utils function', () => { ], }), value: BigInt(0), - account: l3TokenBridgeDeployer, + account: l3RollupOwner, }); // sign and send the transaction const fundTxRequest = { ...fundTxRequestRaw, chainId: nitroTestnodeL2Client.chain.id }; const fundTxHash = await nitroTestnodeL2Client.sendRawTransaction({ - serializedTransaction: await l3TokenBridgeDeployer.signTransaction(fundTxRequest), + serializedTransaction: await l3RollupOwner.signTransaction(fundTxRequest), }); // get the transaction receipt after waiting for the transaction to complete @@ -326,9 +326,10 @@ describe('createTokenBridge utils function', () => { expect(orbitChainRetryableReceipts[0].status).toEqual('success'); expect(orbitChainRetryableReceipts[1].status).toEqual('success'); - // get contracts + // get contracts (query the freshly-deployed creator that holds the deployment mapping) const tokenBridgeContracts = await txReceipt.getTokenBridgeContracts({ parentChainPublicClient: nitroTestnodeL2Client, + tokenBridgeCreatorAddressOverride: tokenBridgeCreator, }); checkTokenBridgeContracts(tokenBridgeContracts); @@ -404,13 +405,13 @@ describe('createTokenBridge', () => { ], }), value: BigInt(0), - account: l3TokenBridgeDeployer, + account: l3RollupOwner, }); // sign and send the transaction const fundTxRequest = { ...fundTxRequestRaw, chainId: nitroTestnodeL2Client.chain.id }; const fundTxHash = await nitroTestnodeL2Client.sendRawTransaction({ - serializedTransaction: await l3TokenBridgeDeployer.signTransaction(fundTxRequest), + serializedTransaction: await l3RollupOwner.signTransaction(fundTxRequest), }); // get the transaction receipt after waiting for the transaction to complete diff --git a/src/decorators/arbOwnerPublicActionsUpgradeExecutor.integration.test.ts b/src/decorators/arbOwnerPublicActionsUpgradeExecutor.integration.test.ts index 7de4d8d96..9dce3c237 100644 --- a/src/decorators/arbOwnerPublicActionsUpgradeExecutor.integration.test.ts +++ b/src/decorators/arbOwnerPublicActionsUpgradeExecutor.integration.test.ts @@ -4,13 +4,14 @@ import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'; import { nitroTestnodeL3 } from '../chains'; import { arbOwnerPublicActions } from './arbOwnerPublicActions'; import { arbGasInfoPublicActions } from './arbGasInfoPublicActions'; -import { getNitroTestnodePrivateKeyAccounts } from '../testHelpers'; +import { getInformationFromTestnode, getNitroTestnodePrivateKeyAccounts } from '../testHelpers'; // L3 Owner Private Key const devPrivateKey = getNitroTestnodePrivateKeyAccounts().l3RollupOwner.privateKey; -// L3 Upgrade Executor Address -const upgradeExecutorAddress: Address = '0x24198F8A339cd3C47AEa3A764A20d2dDaB4D1b5b'; +// L3 chain-owner Upgrade Executor Address (the orbit-chain UE that owns the L3 chain +// and grants l3owner the EXECUTOR_ROLE), read from the testnode deployment +const upgradeExecutorAddress: Address = getInformationFromTestnode().l3ChainOwnerUpgradeExecutor; const owner = privateKeyToAccount(devPrivateKey); const randomAccount = privateKeyToAccount(generatePrivateKey()); diff --git a/src/decorators/rollupAdminLogicPublicActions.integration.test.ts b/src/decorators/rollupAdminLogicPublicActions.integration.test.ts index bb3ad8227..0f5cc79cb 100644 --- a/src/decorators/rollupAdminLogicPublicActions.integration.test.ts +++ b/src/decorators/rollupAdminLogicPublicActions.integration.test.ts @@ -4,21 +4,14 @@ import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'; import { nitroTestnodeL2 } from '../chains'; import { rollupAdminLogicPublicActions } from './rollupAdminLogicPublicActions'; -import { - getInformationFromTestnode, - getNitroTestnodePrivateKeyAccounts, - testHelper_getRollupCreatorVersionFromEnv, -} from '../testHelpers'; +import { getInformationFromTestnode, getNitroTestnodePrivateKeyAccounts } from '../testHelpers'; import { getValidators } from '../getValidators'; const { l3RollupOwner } = getNitroTestnodePrivateKeyAccounts(); const { l3Rollup, l3UpgradeExecutor } = getInformationFromTestnode(); -const rollupCreatorVersion = testHelper_getRollupCreatorVersionFromEnv(); -// https://github.com/OffchainLabs/nitro-testnode/blob/release/test-node.bash#L634 -// https://github.com/OffchainLabs/nitro-contracts/blob/v3.2.0/scripts/rollupCreation.ts#L254-L261 -// https://github.com/OffchainLabs/nitro-contracts/blob/v2.1.3/scripts/rollupCreation.ts#L237-L243 -const expectedInitialValidators = rollupCreatorVersion === 'v3.2' ? 11 : 10; +// the arbitrum-testnode snapshot deploys the rollup with a single initial validator +const expectedInitialValidators = 1; const client = createPublicClient({ chain: nitroTestnodeL2, diff --git a/src/decorators/sequencerInboxActions.integration.test.ts b/src/decorators/sequencerInboxActions.integration.test.ts index 0af77eabb..3f09d9a0d 100644 --- a/src/decorators/sequencerInboxActions.integration.test.ts +++ b/src/decorators/sequencerInboxActions.integration.test.ts @@ -15,7 +15,7 @@ const { l3RollupOwner, l3TokenBridgeDeployer, deployer } = getNitroTestnodePriva const randomAccount = privateKeyToAccount(generatePrivateKey()); -const { l3SequencerInbox, l3Bridge, l3Rollup, l3BatchPoster, l3UpgradeExecutor } = +const { l3SequencerInbox, l3Bridge, l3Rollup, l3BatchPoster, l3UpgradeExecutor, l2RollupCreator } = getInformationFromTestnode(); const client = createPublicClient({ @@ -93,7 +93,7 @@ describe('sequencerInboxReadContract', () => { sequencerInbox: l3SequencerInbox, }); - expect(result).toEqual([5760n, 12n, 86400n, 3600n]); + expect(result).toEqual([28800n, 300n, 345600n, 3600n]); }); it('successfully call rollup', async () => { @@ -127,6 +127,7 @@ describe('sequencerInboxPrepareTransactionRequest', () => { validators, nativeToken: zeroAddress, client, + rollupCreatorAddressOverride: l2RollupCreator, }); const { sequencerInbox, upgradeExecutor } = createRollupInformation.coreContracts; diff --git a/src/getKeysets.integration.test.ts b/src/getKeysets.integration.test.ts index e839f4993..df12594b6 100644 --- a/src/getKeysets.integration.test.ts +++ b/src/getKeysets.integration.test.ts @@ -18,7 +18,7 @@ import { } from './testHelpers'; import { getKeysets } from './getKeysets'; -const { l3SequencerInbox } = getInformationFromTestnode(); +const { l3SequencerInbox, l2RollupCreator } = getInformationFromTestnode(); const { l3TokenBridgeDeployer, deployer } = getNitroTestnodePrivateKeyAccounts(); const client = createPublicClient({ @@ -103,6 +103,7 @@ async function createAnytrustRollup() { validators, nativeToken: zeroAddress, client, + rollupCreatorAddressOverride: l2RollupCreator, }); } diff --git a/src/getValidators.integration.test.ts b/src/getValidators.integration.test.ts index 8aa215ced..22826d1cd 100644 --- a/src/getValidators.integration.test.ts +++ b/src/getValidators.integration.test.ts @@ -4,21 +4,14 @@ import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'; import { nitroTestnodeL2 } from './chains'; import { rollupAdminLogicPublicActions } from './decorators/rollupAdminLogicPublicActions'; -import { - getInformationFromTestnode, - getNitroTestnodePrivateKeyAccounts, - testHelper_getRollupCreatorVersionFromEnv, -} from './testHelpers'; +import { getInformationFromTestnode, getNitroTestnodePrivateKeyAccounts } from './testHelpers'; import { getValidators } from './getValidators'; const { l3RollupOwner } = getNitroTestnodePrivateKeyAccounts(); const { l3Rollup, l3UpgradeExecutor } = getInformationFromTestnode(); -const rollupCreatorVersion = testHelper_getRollupCreatorVersionFromEnv(); -// https://github.com/OffchainLabs/nitro-testnode/blob/release/test-node.bash#L634 -// https://github.com/OffchainLabs/nitro-contracts/blob/v3.2.0/scripts/rollupCreation.ts#L254-L261 -// https://github.com/OffchainLabs/nitro-contracts/blob/v2.1.3/scripts/rollupCreation.ts#L237-L243 -const expectedInitialValidators = rollupCreatorVersion === 'v3.2' ? 11 : 10; +// the arbitrum-testnode snapshot deploys the rollup with a single initial validator +const expectedInitialValidators = 1; const client = createPublicClient({ chain: nitroTestnodeL2, diff --git a/src/testHelpers.ts b/src/testHelpers.ts index 8e0a8e510..626c9312a 100644 --- a/src/testHelpers.ts +++ b/src/testHelpers.ts @@ -1,13 +1,18 @@ -import { Address, Chain, PublicClient, zeroAddress } from 'viem'; +import { Address, Chain, PublicClient, zeroAddress, encodeFunctionData, parseEther } from 'viem'; import { PrivateKeyAccount, privateKeyToAccount, generatePrivateKey } from 'viem/accounts'; +import { erc20ABI } from './contracts/ERC20'; +import { isNonZeroAddress } from './utils/isNonZeroAddress'; import { config } from 'dotenv'; import { execSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; import { generateChainId, sanitizePrivateKey } from './utils'; import { createRollup } from './createRollup'; import { createRollupPrepareDeploymentParamsConfig } from './createRollupPrepareDeploymentParamsConfig'; import { prepareChainConfig } from './prepareChainConfig'; import { CreateRollupParams, RollupCreatorSupportedVersion } from './types/createRollupTypes'; +import { scaleFrom18DecimalsToNativeTokenDecimals } from './utils/decimals'; config(); @@ -86,11 +91,96 @@ type TestnodeInformation = { batchPoster: Address; l3BatchPoster: Address; l3UpgradeExecutor: Address; + l3ChainOwnerUpgradeExecutor: Address; l3Rollup: `0x${string}`; l3NativeToken: `0x${string}`; + l2RollupCreator: Address; }; +/** + * Discover the RollupCreator address by querying the parent chain for the + * RollupCreated event that deployed the given rollup. Falls back to zeroAddress. + */ +function discoverRollupCreatorAddress( + parentRpcUrl: string, + rollupAddress: Address, + deployedAtBlock: number, +): Address { + try { + // v3.2: RollupCreated(address,address,address,address,address,address,address,address,address,address,address) + // v2.1: RollupCreated(address,address,address,address,address,address,address,address,address,address,address,address) + const topics = [ + '0xd9bfd3bb3012f0caa103d1ba172692464d2de5c7b75877ce255c72147086a79d', + '0x481277de518d1f364b196166b90219b996fba76138a3dc84e7fe02540eb1cbdb', + ]; + const rollupTopic = '0x000000000000000000000000' + rollupAddress.slice(2).toLowerCase(); + const fromBlock = '0x' + Math.max(0, deployedAtBlock - 1).toString(16); + const toBlock = '0x' + (deployedAtBlock + 1).toString(16); + for (const eventTopic of topics) { + const body = JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method: 'eth_getLogs', + params: [{ fromBlock, toBlock, topics: [eventTopic, rollupTopic] }], + }); + const result = execSync( + `curl -sf -X POST -H "Content-Type: application/json" -d '${body}' "${parentRpcUrl}"`, + { timeout: 10000 }, + ).toString(); + const parsed = JSON.parse(result); + if (parsed.result?.length > 0) return parsed.result[0].address as Address; + } + } catch { + // empty on purpose + } + return zeroAddress; +} + +function getInformationFromConfigDir(configDir: string): TestnodeInformation { + const deploymentJson = JSON.parse(readFileSync(join(configDir, 'deployment.json'), 'utf-8')); + const l3DeploymentJson = JSON.parse(readFileSync(join(configDir, 'l3deployment.json'), 'utf-8')); + const sequencerConfig = JSON.parse(readFileSync(join(configDir, 'l2-nodeConfig.json'), 'utf-8')); + const l3NodeConfig = JSON.parse(readFileSync(join(configDir, 'l3-nodeConfig.json'), 'utf-8')); + + const batchPosterKey = sequencerConfig.node['batch-poster']['parent-chain-wallet']['private-key']; + const l3BatchPosterKey = l3NodeConfig.node['batch-poster']['parent-chain-wallet']['private-key']; + + // Read rollup-creator from config if available, otherwise discover from the chain + let l2RollupCreator: Address = l3DeploymentJson['rollup-creator'] ?? zeroAddress; + if ( + l2RollupCreator === zeroAddress && + l3DeploymentJson['rollup'] && + l3DeploymentJson['deployed-at'] != null + ) { + l2RollupCreator = discoverRollupCreatorAddress( + 'http://127.0.0.1:8547', + l3DeploymentJson['rollup'], + l3DeploymentJson['deployed-at'], + ); + } + + return { + bridge: deploymentJson['bridge'], + rollup: deploymentJson['rollup'], + sequencerInbox: deploymentJson['sequencer-inbox'], + batchPoster: privateKeyToAccount(`0x${batchPosterKey}`).address, + l3Bridge: l3DeploymentJson['bridge'], + l3Rollup: l3DeploymentJson['rollup'], + l3SequencerInbox: l3DeploymentJson['sequencer-inbox'], + l3NativeToken: l3DeploymentJson['native-token'], + l3BatchPoster: privateKeyToAccount(`0x${l3BatchPosterKey}`).address, + l3UpgradeExecutor: l3DeploymentJson['upgrade-executor'], + l3ChainOwnerUpgradeExecutor: l3DeploymentJson['chain-owner-upgrade-executor'], + l2RollupCreator, + }; +} + export function getInformationFromTestnode(): TestnodeInformation { + const configDir = process.env.ARBITRUM_TESTNODE_CONFIG_DIR; + if (configDir) { + return getInformationFromConfigDir(configDir); + } + const containers = [ 'nitro_sequencer_1', 'nitro-sequencer-1', @@ -127,6 +217,8 @@ export function getInformationFromTestnode(): TestnodeInformation { l3NativeToken: l3DeploymentJson['native-token'], l3BatchPoster: l3SequencerConfig.node['batch-poster']['parent-chain-wallet'].account, l3UpgradeExecutor: l3DeploymentJson['upgrade-executor'], + l3ChainOwnerUpgradeExecutor: l3DeploymentJson['chain-owner-upgrade-executor'], + l2RollupCreator: l3DeploymentJson['rollup-creator'] ?? zeroAddress, }; } catch { // empty on purpose @@ -145,6 +237,7 @@ export async function createRollupHelper< nativeToken = zeroAddress, client, rollupCreatorVersion = testHelper_getRollupCreatorVersionFromEnv() as TRollupCreatorVersion, + rollupCreatorAddressOverride, }: { deployer: PrivateKeyAccountWithPrivateKey; batchPosters: Address[]; @@ -152,6 +245,7 @@ export async function createRollupHelper< nativeToken: Address; client: PublicClient; rollupCreatorVersion?: TRollupCreatorVersion; + rollupCreatorAddressOverride?: Address; }) { const chainId = generateChainId(); @@ -171,6 +265,42 @@ export async function createRollupHelper< rollupCreatorVersion, ); + // On the arbitrum-testnode snapshot the custom fee token is held by the L3 rollup owner, not the + // deployer, so fund the deployer to cover the retryable fees paid in the custom fee token. + if (isNonZeroAddress(nativeToken)) { + const { l3RollupOwner } = getNitroTestnodePrivateKeyAccounts(); + // The fee token may have fewer than 18 decimals; scale the funding amount to the token's + // decimals so the transfer doesn't exceed the holder's balance (e.g. on a 6-decimal token). + const nativeTokenDecimals = await client.readContract({ + address: nativeToken, + abi: erc20ABI, + functionName: 'decimals', + }); + const fundRequest = await client.prepareTransactionRequest({ + chain: client.chain, + account: l3RollupOwner, + to: nativeToken, + data: encodeFunctionData({ + abi: erc20ABI, + functionName: 'transfer', + args: [ + deployer.address, + scaleFrom18DecimalsToNativeTokenDecimals({ + amount: parseEther('100000'), + decimals: nativeTokenDecimals, + }), + ], + }), + }); + const fundHash = await client.sendRawTransaction({ + serializedTransaction: await l3RollupOwner.signTransaction({ + ...fundRequest, + chainId: client.chain!.id, + }), + }); + await client.waitForTransactionReceipt({ hash: fundHash }); + } + const createRollupInformation = rollupCreatorVersion === 'v2.1' ? await createRollup({ @@ -183,6 +313,7 @@ export async function createRollupHelper< account: deployer, parentChainPublicClient: client, rollupCreatorVersion: 'v2.1', + rollupCreatorAddressOverride, }) : await createRollup({ params: { @@ -194,6 +325,7 @@ export async function createRollupHelper< account: deployer, parentChainPublicClient: client, rollupCreatorVersion: 'v3.2', + rollupCreatorAddressOverride, }); // create test rollup with ETH as gas token From 1d5203cde4a2d6eed17525ec1c886d2bbe36e19a Mon Sep 17 00:00:00 2001 From: douglance <4741454+douglance@users.noreply.github.com> Date: Thu, 25 Jun 2026 09:42:28 -0400 Subject: [PATCH 4/5] test: read RollupCreator from testnode metadata, drop on-chain discovery The testnode emits the RollupCreator address in l3deployment.json (rollup-creator) for every variant CI uses, so read it directly instead of scanning RollupCreated logs via eth_getLogs. discoverRollupCreatorAddress only ran when the value was zero, which never happens for the custom-fee variants, so this is a behavior-preserving cleanup. --- src/testHelpers.ts | 55 +++------------------------------------------- 1 file changed, 3 insertions(+), 52 deletions(-) diff --git a/src/testHelpers.ts b/src/testHelpers.ts index 626c9312a..8006952da 100644 --- a/src/testHelpers.ts +++ b/src/testHelpers.ts @@ -97,45 +97,6 @@ type TestnodeInformation = { l2RollupCreator: Address; }; -/** - * Discover the RollupCreator address by querying the parent chain for the - * RollupCreated event that deployed the given rollup. Falls back to zeroAddress. - */ -function discoverRollupCreatorAddress( - parentRpcUrl: string, - rollupAddress: Address, - deployedAtBlock: number, -): Address { - try { - // v3.2: RollupCreated(address,address,address,address,address,address,address,address,address,address,address) - // v2.1: RollupCreated(address,address,address,address,address,address,address,address,address,address,address,address) - const topics = [ - '0xd9bfd3bb3012f0caa103d1ba172692464d2de5c7b75877ce255c72147086a79d', - '0x481277de518d1f364b196166b90219b996fba76138a3dc84e7fe02540eb1cbdb', - ]; - const rollupTopic = '0x000000000000000000000000' + rollupAddress.slice(2).toLowerCase(); - const fromBlock = '0x' + Math.max(0, deployedAtBlock - 1).toString(16); - const toBlock = '0x' + (deployedAtBlock + 1).toString(16); - for (const eventTopic of topics) { - const body = JSON.stringify({ - jsonrpc: '2.0', - id: 1, - method: 'eth_getLogs', - params: [{ fromBlock, toBlock, topics: [eventTopic, rollupTopic] }], - }); - const result = execSync( - `curl -sf -X POST -H "Content-Type: application/json" -d '${body}' "${parentRpcUrl}"`, - { timeout: 10000 }, - ).toString(); - const parsed = JSON.parse(result); - if (parsed.result?.length > 0) return parsed.result[0].address as Address; - } - } catch { - // empty on purpose - } - return zeroAddress; -} - function getInformationFromConfigDir(configDir: string): TestnodeInformation { const deploymentJson = JSON.parse(readFileSync(join(configDir, 'deployment.json'), 'utf-8')); const l3DeploymentJson = JSON.parse(readFileSync(join(configDir, 'l3deployment.json'), 'utf-8')); @@ -145,19 +106,9 @@ function getInformationFromConfigDir(configDir: string): TestnodeInformation { const batchPosterKey = sequencerConfig.node['batch-poster']['parent-chain-wallet']['private-key']; const l3BatchPosterKey = l3NodeConfig.node['batch-poster']['parent-chain-wallet']['private-key']; - // Read rollup-creator from config if available, otherwise discover from the chain - let l2RollupCreator: Address = l3DeploymentJson['rollup-creator'] ?? zeroAddress; - if ( - l2RollupCreator === zeroAddress && - l3DeploymentJson['rollup'] && - l3DeploymentJson['deployed-at'] != null - ) { - l2RollupCreator = discoverRollupCreatorAddress( - 'http://127.0.0.1:8547', - l3DeploymentJson['rollup'], - l3DeploymentJson['deployed-at'], - ); - } + // The testnode emits the RollupCreator address as deployment metadata (l3deployment.json), + // so read it directly — this is the creator on L2 that deployed the L3 rollup. + const l2RollupCreator: Address = l3DeploymentJson['rollup-creator'] ?? zeroAddress; return { bridge: deploymentJson['bridge'], From 025c4c7d8ce8469321a8f273a9f20817616f745d Mon Sep 17 00:00:00 2001 From: Doug Lance <4741454+douglance@users.noreply.github.com> Date: Tue, 11 Aug 2026 03:40:16 -0400 Subject: [PATCH 5/5] fix(testnode): use chain metadata for rollup creator --- .../sequencerInbox.integration.test.ts | 8 +++----- src/createRollup.integration.test.ts | 8 ++------ src/createRollup.ts | 10 ---------- .../sequencerInboxActions.integration.test.ts | 7 +++---- src/getKeysets.integration.test.ts | 7 +++---- src/testHelpers.ts | 16 ++++++++++++---- src/utils/getRollupCreatorAddress.ts | 19 +++++++++---------- .../getRollupCreatorAddress.unit.test.ts | 16 ++++++++++++++++ 8 files changed, 48 insertions(+), 43 deletions(-) diff --git a/src/actions/sequencerInbox.integration.test.ts b/src/actions/sequencerInbox.integration.test.ts index 815b1a18b..923e35165 100644 --- a/src/actions/sequencerInbox.integration.test.ts +++ b/src/actions/sequencerInbox.integration.test.ts @@ -3,9 +3,9 @@ import { createRollupHelper, getInformationFromTestnode, getNitroTestnodePrivateKeyAccounts, + testHelper_getNitroTestnodeL2, } from '../testHelpers'; import { Hex, createPublicClient, http, zeroAddress } from 'viem'; -import { nitroTestnodeL2 } from '../chains'; import { getMaxTimeVariation } from './getMaxTimeVariation'; import { isBatchPoster } from './isBatchPoster'; import { sequencerInboxABI } from '../contracts/SequencerInbox'; @@ -16,12 +16,11 @@ import { buildDisableBatchPoster, buildEnableBatchPoster } from './buildSetIsBat import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'; import { buildInvalidateKeysetHash } from './buildInvalidateKeysetHash'; -const { l3SequencerInbox, l3BatchPoster, l3UpgradeExecutor, l2RollupCreator } = - getInformationFromTestnode(); +const { l3SequencerInbox, l3BatchPoster, l3UpgradeExecutor } = getInformationFromTestnode(); const { l3TokenBridgeDeployer, deployer, l3RollupOwner } = getNitroTestnodePrivateKeyAccounts(); const client = createPublicClient({ - chain: nitroTestnodeL2, + chain: testHelper_getNitroTestnodeL2(), transport: http(), }); @@ -162,7 +161,6 @@ describe('keyset management', () => { validators, nativeToken: zeroAddress, client, - rollupCreatorAddressOverride: l2RollupCreator, }); const { sequencerInbox, upgradeExecutor } = createRollupInformation.coreContracts; diff --git a/src/createRollup.integration.test.ts b/src/createRollup.integration.test.ts index 5281439d4..7fa414451 100644 --- a/src/createRollup.integration.test.ts +++ b/src/createRollup.integration.test.ts @@ -1,16 +1,16 @@ import { describe, it, expect } from 'vitest'; import { createPublicClient, http, parseGwei, zeroAddress } from 'viem'; -import { nitroTestnodeL2 } from './chains'; import { createRollupHelper, getNitroTestnodePrivateKeyAccounts, getInformationFromTestnode, + testHelper_getNitroTestnodeL2, } from './testHelpers'; import { createRollupFetchTransactionHash } from './createRollupFetchTransactionHash'; const parentChainPublicClient = createPublicClient({ - chain: nitroTestnodeL2, + chain: testHelper_getNitroTestnodeL2(), transport: http(), }); @@ -20,8 +20,6 @@ const l3TokenBridgeDeployer = testnodeAccounts.l3TokenBridgeDeployer; const batchPosters = [testnodeAccounts.deployer.address]; const validators = [testnodeAccounts.deployer.address]; -const { l2RollupCreator } = getInformationFromTestnode(); - describe(`create an AnyTrust chain that uses ETH as gas token`, async () => { const { createRollupConfig, createRollupInformation } = await createRollupHelper({ deployer: l3TokenBridgeDeployer, @@ -29,7 +27,6 @@ describe(`create an AnyTrust chain that uses ETH as gas token`, async () => { validators, nativeToken: zeroAddress, client: parentChainPublicClient, - rollupCreatorAddressOverride: l2RollupCreator, }); it(`successfully deploys core contracts through rollup creator`, async () => { @@ -69,7 +66,6 @@ describe(`create an AnyTrust chain that uses a custom gas token`, async () => { validators, nativeToken, client: parentChainPublicClient, - rollupCreatorAddressOverride: l2RollupCreator, }); it(`successfully deploys core contracts through rollup creator`, async () => { diff --git a/src/createRollup.ts b/src/createRollup.ts index c43a2955d..cc52e7c9c 100644 --- a/src/createRollup.ts +++ b/src/createRollup.ts @@ -21,7 +21,6 @@ type EnsureCustomGasTokenAllowanceGrantedToRollupCreatorParams; account: PrivateKeyAccount; rollupCreatorVersion?: RollupCreatorSupportedVersion; - rollupCreatorAddressOverride?: Address; }; /** @@ -39,14 +38,12 @@ async function ensureCustomGasTokenAllowanceGrantedToRollupCreator< parentChainPublicClient, account, rollupCreatorVersion = 'v3.2', - rollupCreatorAddressOverride, }: EnsureCustomGasTokenAllowanceGrantedToRollupCreatorParams) { const allowanceParams = { nativeToken, account: account.address, publicClient: parentChainPublicClient, rollupCreatorVersion, - rollupCreatorAddressOverride, }; if (!(await createRollupEnoughCustomFeeTokenAllowance(allowanceParams))) { @@ -85,21 +82,18 @@ export type CreateRollupFunctionParams = account: PrivateKeyAccount; parentChainPublicClient: PublicClient; rollupCreatorVersion: 'v2.1'; - rollupCreatorAddressOverride?: Address; } | { params: CreateRollupParams<'v3.2'>; account: PrivateKeyAccount; parentChainPublicClient: PublicClient; rollupCreatorVersion: 'v3.2'; - rollupCreatorAddressOverride?: Address; } | { params: CreateRollupParams<'v3.2'>; account: PrivateKeyAccount; parentChainPublicClient: PublicClient; rollupCreatorVersion?: never; - rollupCreatorAddressOverride?: Address; }; /** @@ -179,7 +173,6 @@ export async function createRollup({ account, parentChainPublicClient, rollupCreatorVersion = 'v3.2', - rollupCreatorAddressOverride, }: CreateRollupFunctionParams): Promise { validateParentChain(parentChainPublicClient); @@ -193,7 +186,6 @@ export async function createRollup({ parentChainPublicClient, account, rollupCreatorVersion, - rollupCreatorAddressOverride, }); } @@ -205,14 +197,12 @@ export async function createRollup({ account: account.address, publicClient: parentChainPublicClient, rollupCreatorVersion: 'v2.1', - rollupCreatorAddressOverride, }) : await createRollupPrepareTransactionRequest({ params: params as CreateRollupParams<'v3.2'>, account: account.address, publicClient: parentChainPublicClient, rollupCreatorVersion: 'v3.2', - rollupCreatorAddressOverride, }); // sign and send the transaction diff --git a/src/decorators/sequencerInboxActions.integration.test.ts b/src/decorators/sequencerInboxActions.integration.test.ts index 3f09d9a0d..fe4a952aa 100644 --- a/src/decorators/sequencerInboxActions.integration.test.ts +++ b/src/decorators/sequencerInboxActions.integration.test.ts @@ -2,11 +2,11 @@ import { describe, it, expect } from 'vitest'; import { createPublicClient, http, zeroAddress } from 'viem'; import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'; -import { nitroTestnodeL2 } from '../chains'; import { getNitroTestnodePrivateKeyAccounts, getInformationFromTestnode, createRollupHelper, + testHelper_getNitroTestnodeL2, } from '../testHelpers'; import { sequencerInboxActions } from './sequencerInboxActions'; import { sequencerInboxABI } from '../contracts/SequencerInbox'; @@ -15,11 +15,11 @@ const { l3RollupOwner, l3TokenBridgeDeployer, deployer } = getNitroTestnodePriva const randomAccount = privateKeyToAccount(generatePrivateKey()); -const { l3SequencerInbox, l3Bridge, l3Rollup, l3BatchPoster, l3UpgradeExecutor, l2RollupCreator } = +const { l3SequencerInbox, l3Bridge, l3Rollup, l3BatchPoster, l3UpgradeExecutor } = getInformationFromTestnode(); const client = createPublicClient({ - chain: nitroTestnodeL2, + chain: testHelper_getNitroTestnodeL2(), transport: http(), }).extend(sequencerInboxActions({ sequencerInbox: l3SequencerInbox })); @@ -127,7 +127,6 @@ describe('sequencerInboxPrepareTransactionRequest', () => { validators, nativeToken: zeroAddress, client, - rollupCreatorAddressOverride: l2RollupCreator, }); const { sequencerInbox, upgradeExecutor } = createRollupInformation.coreContracts; diff --git a/src/getKeysets.integration.test.ts b/src/getKeysets.integration.test.ts index df12594b6..943e4e59b 100644 --- a/src/getKeysets.integration.test.ts +++ b/src/getKeysets.integration.test.ts @@ -9,20 +9,20 @@ import { zeroAddress, } from 'viem'; -import { nitroTestnodeL2 } from './chains'; import { sequencerInboxActions } from './decorators/sequencerInboxActions'; import { createRollupHelper, getInformationFromTestnode, getNitroTestnodePrivateKeyAccounts, + testHelper_getNitroTestnodeL2, } from './testHelpers'; import { getKeysets } from './getKeysets'; -const { l3SequencerInbox, l2RollupCreator } = getInformationFromTestnode(); +const { l3SequencerInbox } = getInformationFromTestnode(); const { l3TokenBridgeDeployer, deployer } = getNitroTestnodePrivateKeyAccounts(); const client = createPublicClient({ - chain: nitroTestnodeL2, + chain: testHelper_getNitroTestnodeL2(), transport: http(), }).extend( sequencerInboxActions({ @@ -103,7 +103,6 @@ async function createAnytrustRollup() { validators, nativeToken: zeroAddress, client, - rollupCreatorAddressOverride: l2RollupCreator, }); } diff --git a/src/testHelpers.ts b/src/testHelpers.ts index 8006952da..aa68ffb08 100644 --- a/src/testHelpers.ts +++ b/src/testHelpers.ts @@ -13,6 +13,7 @@ import { createRollupPrepareDeploymentParamsConfig } from './createRollupPrepare import { prepareChainConfig } from './prepareChainConfig'; import { CreateRollupParams, RollupCreatorSupportedVersion } from './types/createRollupTypes'; import { scaleFrom18DecimalsToNativeTokenDecimals } from './utils/decimals'; +import { nitroTestnodeL2 } from './chains'; config(); @@ -179,6 +180,17 @@ export function getInformationFromTestnode(): TestnodeInformation { throw new Error('nitro-testnode sequencer not found'); } +export function testHelper_getNitroTestnodeL2() { + const { l2RollupCreator } = getInformationFromTestnode(); + + return { + ...nitroTestnodeL2, + contracts: { + rollupCreator: { address: l2RollupCreator }, + }, + }; +} + export async function createRollupHelper< TRollupCreatorVersion extends RollupCreatorSupportedVersion = 'v3.2', >({ @@ -188,7 +200,6 @@ export async function createRollupHelper< nativeToken = zeroAddress, client, rollupCreatorVersion = testHelper_getRollupCreatorVersionFromEnv() as TRollupCreatorVersion, - rollupCreatorAddressOverride, }: { deployer: PrivateKeyAccountWithPrivateKey; batchPosters: Address[]; @@ -196,7 +207,6 @@ export async function createRollupHelper< nativeToken: Address; client: PublicClient; rollupCreatorVersion?: TRollupCreatorVersion; - rollupCreatorAddressOverride?: Address; }) { const chainId = generateChainId(); @@ -264,7 +274,6 @@ export async function createRollupHelper< account: deployer, parentChainPublicClient: client, rollupCreatorVersion: 'v2.1', - rollupCreatorAddressOverride, }) : await createRollup({ params: { @@ -276,7 +285,6 @@ export async function createRollupHelper< account: deployer, parentChainPublicClient: client, rollupCreatorVersion: 'v3.2', - rollupCreatorAddressOverride, }); // create test rollup with ETH as gas token diff --git a/src/utils/getRollupCreatorAddress.ts b/src/utils/getRollupCreatorAddress.ts index 7b431f093..1466ac415 100644 --- a/src/utils/getRollupCreatorAddress.ts +++ b/src/utils/getRollupCreatorAddress.ts @@ -11,18 +11,17 @@ export function getRollupCreatorAddress( rollupCreatorVersion: RollupCreatorSupportedVersion = 'v3.2', ): Address { const { chainId: parentChainId, isCustom: parentChainIsCustom } = validateParentChain(client); + const configuredAddress = (client.chain?.contracts?.rollupCreator as ChainContract | undefined) + ?.address; - if (parentChainIsCustom) { - const contract = client.chain?.contracts?.rollupCreator as ChainContract | undefined; - const address = contract?.address; - - if (typeof address === 'undefined') { - throw new Error( - `Address for RollupCreator is missing on custom parent chain with id ${parentChainId}`, - ); - } + if (typeof configuredAddress !== 'undefined') { + return configuredAddress; + } - return address; + if (parentChainIsCustom) { + throw new Error( + `Address for RollupCreator is missing on custom parent chain with id ${parentChainId}`, + ); } const rollupCreatorAddress = diff --git a/src/utils/getRollupCreatorAddress.unit.test.ts b/src/utils/getRollupCreatorAddress.unit.test.ts index 902cd6494..8d112fdae 100644 --- a/src/utils/getRollupCreatorAddress.unit.test.ts +++ b/src/utils/getRollupCreatorAddress.unit.test.ts @@ -38,6 +38,22 @@ it(`successfully returns address for Sepolia (v2.1)`, () => { ); }); +it(`prefers a RollupCreator configured on a supported chain`, () => { + const rollupCreator = '0x31421C442c422BD16aef6ae44D3b11F404eeaBd9'; + const client = createPublicClient({ + chain: { + ...sepolia, + contracts: { + ...sepolia.contracts, + rollupCreator: { address: rollupCreator }, + }, + }, + transport: http(), + }); + + expect(getRollupCreatorAddress(client)).toEqual(rollupCreator); +}); + it(`fails to return address for an unrecognized parent chain`, () => { const chain = testHelper_createCustomParentChain();