From 085ff59602fe146e5d4356c1334d748ace5c6e2c Mon Sep 17 00:00:00 2001 From: alxdca Date: Wed, 22 Apr 2026 09:33:36 +0200 Subject: [PATCH 1/5] feat: verify foundry binaries util --- src/utils/index.ts | 2 ++ src/utils/verifyFoundry.ts | 47 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 src/utils/verifyFoundry.ts diff --git a/src/utils/index.ts b/src/utils/index.ts index 17b77bb7c..755d1ec09 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -7,6 +7,7 @@ import { getClientVersion } from './getClientVersion'; import { getRollupCreatorAddress } from './getRollupCreatorAddress'; import { getTokenBridgeCreatorAddress } from './getTokenBridgeCreatorAddress'; import { getWethAddress } from './getWethAddress'; +import { verifyFoundryBinaries } from './verifyFoundry'; export { generateChainId, @@ -18,4 +19,5 @@ export { getRollupCreatorAddress, getTokenBridgeCreatorAddress, getWethAddress, + verifyFoundryBinaries, }; diff --git a/src/utils/verifyFoundry.ts b/src/utils/verifyFoundry.ts new file mode 100644 index 000000000..e23ff0376 --- /dev/null +++ b/src/utils/verifyFoundry.ts @@ -0,0 +1,47 @@ +import { execFile } from 'node:child_process'; + +const FOUNDRY_BINARIES: ['forge', 'cast'] = ['forge', 'cast']; + +function runVersionCommand(binary: 'forge' | 'cast'): Promise { + return new Promise((resolve, reject) => { + execFile(binary, ['--version'], (error, stdout) => { + if (error) { + reject(error); + return; + } + + resolve(stdout); + }); + }); +} + +export async function verifyFoundryBinaries(stableOnly = true) { + const results = await Promise.allSettled( + FOUNDRY_BINARIES.map((binary) => runVersionCommand(binary)), + ); + + const binariesPresent = results.every((result) => result.status === 'fulfilled'); + + const stableReleaseInstalled = results.every( + (result) => + result.status === 'fulfilled' && result.value && isStableFoundryRelease(result.value), + ); + + if (!binariesPresent) { + throw new Error( + 'Foundry is required to run this operation. Install Foundry and make sure forge and cast are available on PATH.', + ); + } + + if (stableOnly && !stableReleaseInstalled) { + throw new Error( + 'Foundry stable releases are required to run this operation. Please install the stable versions of forge and cast.', + ); + } +} + +function isStableFoundryRelease(version: string): boolean { + return !['nightly', 'dev', 'alpha', 'beta', 'rc', 'preview'].some((tag) => + version.toLowerCase().includes(tag), + ); +} From a542a0352f384dc2535f9743e18c74b8e9fee8a7 Mon Sep 17 00:00:00 2001 From: alxdca Date: Tue, 1 Sep 2026 10:33:02 +0200 Subject: [PATCH 2/5] feat: add versioner to CLI --- src/getNitroContractVersions.unit.test.ts | 4 ++-- src/scripting/commands.ts | 3 +++ src/scripting/schemaCoverage.ts | 3 +++ .../schemas/getChainContractVersions.ts | 17 +++++++++++++++++ src/scripting/schemas/index.ts | 1 + 5 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 src/scripting/schemas/getChainContractVersions.ts diff --git a/src/getNitroContractVersions.unit.test.ts b/src/getNitroContractVersions.unit.test.ts index a7557d31e..46231b90e 100644 --- a/src/getNitroContractVersions.unit.test.ts +++ b/src/getNitroContractVersions.unit.test.ts @@ -42,7 +42,7 @@ describe('getNitroContractVersions', () => { ); }); - it('throws when the JSON payload is missing top level fields', async () => { + it('throws when the result is missing top-level fields', async () => { vi.mocked(runChainVersioner).mockResolvedValueOnce({ versions: { Inbox: 'v1.1.1' }, } as never); @@ -52,7 +52,7 @@ describe('getNitroContractVersions', () => { ).rejects.toThrow('Failed to parse Nitro contract versions'); }); - it('accepts any JSON payload that includes versions and upgradeRecommendation', async () => { + it('accepts any result that includes versions and upgradeRecommendation', async () => { vi.mocked(runChainVersioner).mockResolvedValueOnce({ versions: 'not-validated', upgradeRecommendation: null, diff --git a/src/scripting/commands.ts b/src/scripting/commands.ts index cf783e81a..7f53be63f 100644 --- a/src/scripting/commands.ts +++ b/src/scripting/commands.ts @@ -130,6 +130,8 @@ import { schema as deployFullChainSchema, execute as deployFullChainExecute, } from './workflows/deployFullChain'; +import { getChainContractVersions } from '../getChainContractVersions'; +import { getChainContractVersionsSchema } from './schemas/getChainContractVersions'; import { contractRegistry } from './contractRegistry'; import { buildContractCommandSchema } from './contractCommandSchema'; @@ -366,4 +368,5 @@ export const commands: readonly Command[] = [ ), ...contractCommands, + command('getChainContractVersions', getChainContractVersionsSchema, getChainContractVersions), ]; diff --git a/src/scripting/schemaCoverage.ts b/src/scripting/schemaCoverage.ts index dcaf8c920..30280474f 100644 --- a/src/scripting/schemaCoverage.ts +++ b/src/scripting/schemaCoverage.ts @@ -407,6 +407,9 @@ vi.mock('viem', async (importOriginal) => { }; }); +vi.mock('../getChainContractVersions', () => ({ + getChainContractVersions: _mocks.fn('getChainContractVersions'), +})); /** * A testable leaf of a schema -- a scalar field the harness will vary when * running coverage. diff --git a/src/scripting/schemas/getChainContractVersions.ts b/src/scripting/schemas/getChainContractVersions.ts new file mode 100644 index 000000000..bd839a76b --- /dev/null +++ b/src/scripting/schemas/getChainContractVersions.ts @@ -0,0 +1,17 @@ +import { z } from 'zod'; + +import { getChainContractVersions } from '../../getChainContractVersions'; +import { addressSchema } from './common'; + +export const getChainContractVersionsSchema = z + .object({ + inboxAddress: addressSchema, + parentChainRpc: z.url(), + }) + .strict() + .transform( + (input): Parameters => [ + input.inboxAddress, + input.parentChainRpc, + ], + ); diff --git a/src/scripting/schemas/index.ts b/src/scripting/schemas/index.ts index 5f73f2b11..6d016e29f 100644 --- a/src/scripting/schemas/index.ts +++ b/src/scripting/schemas/index.ts @@ -81,3 +81,4 @@ export { isAllowListEnabledSchema, isAllowedSchema, } from './actions'; +export { getChainContractVersionsSchema } from './getChainContractVersions'; From 219fc2fe8bc53f61a067dbaa551f9cbd125d82aa Mon Sep 17 00:00:00 2001 From: alxdca Date: Tue, 1 Sep 2026 10:33:02 +0200 Subject: [PATCH 3/5] feat: add versioner to CLI --- src/getNitroContractVersions.ts | 3 +++ src/scripting/commands.ts | 6 +++--- src/scripting/schemaCoverage.ts | 4 ++-- ...ChainContractVersions.ts => getNitroContractVersions.ts} | 6 +++--- src/scripting/schemas/index.ts | 2 +- 5 files changed, 12 insertions(+), 9 deletions(-) rename src/scripting/schemas/{getChainContractVersions.ts => getNitroContractVersions.ts} (59%) diff --git a/src/getNitroContractVersions.ts b/src/getNitroContractVersions.ts index c25af9b2b..d0c52fff8 100644 --- a/src/getNitroContractVersions.ts +++ b/src/getNitroContractVersions.ts @@ -1,6 +1,7 @@ import type { Address } from 'viem'; import { runChainVersioner } from '@arbitrum/chain-actions'; +import { verifyFoundryBinaries } from './utils/verifyFoundry'; export type GetNitroContractVersionsResult = { versions: Record; @@ -37,6 +38,8 @@ export async function getNitroContractVersions( inboxAddress: Address, parentChainRpc: string, ): Promise { + await verifyFoundryBinaries(); + const result = await runChainVersioner(inboxAddress, parentChainRpc, true); return parseNitroContractVersionsResult(result); diff --git a/src/scripting/commands.ts b/src/scripting/commands.ts index 7f53be63f..b43acb3bb 100644 --- a/src/scripting/commands.ts +++ b/src/scripting/commands.ts @@ -130,8 +130,8 @@ import { schema as deployFullChainSchema, execute as deployFullChainExecute, } from './workflows/deployFullChain'; -import { getChainContractVersions } from '../getChainContractVersions'; -import { getChainContractVersionsSchema } from './schemas/getChainContractVersions'; +import { getNitroContractVersions } from '../getNitroContractVersions'; +import { getNitroContractVersionsSchema } from './schemas/getNitroContractVersions'; import { contractRegistry } from './contractRegistry'; import { buildContractCommandSchema } from './contractCommandSchema'; @@ -368,5 +368,5 @@ export const commands: readonly Command[] = [ ), ...contractCommands, - command('getChainContractVersions', getChainContractVersionsSchema, getChainContractVersions), + command('getNitroContractVersions', getNitroContractVersionsSchema, getNitroContractVersions), ]; diff --git a/src/scripting/schemaCoverage.ts b/src/scripting/schemaCoverage.ts index 30280474f..ca6bf9d63 100644 --- a/src/scripting/schemaCoverage.ts +++ b/src/scripting/schemaCoverage.ts @@ -407,8 +407,8 @@ vi.mock('viem', async (importOriginal) => { }; }); -vi.mock('../getChainContractVersions', () => ({ - getChainContractVersions: _mocks.fn('getChainContractVersions'), +vi.mock('../getNitroContractVersions', () => ({ + getNitroContractVersions: _mocks.fn('getNitroContractVersions'), })); /** * A testable leaf of a schema -- a scalar field the harness will vary when diff --git a/src/scripting/schemas/getChainContractVersions.ts b/src/scripting/schemas/getNitroContractVersions.ts similarity index 59% rename from src/scripting/schemas/getChainContractVersions.ts rename to src/scripting/schemas/getNitroContractVersions.ts index bd839a76b..75717ad8c 100644 --- a/src/scripting/schemas/getChainContractVersions.ts +++ b/src/scripting/schemas/getNitroContractVersions.ts @@ -1,16 +1,16 @@ import { z } from 'zod'; -import { getChainContractVersions } from '../../getChainContractVersions'; +import { getNitroContractVersions } from '../../getNitroContractVersions'; import { addressSchema } from './common'; -export const getChainContractVersionsSchema = z +export const getNitroContractVersionsSchema = z .object({ inboxAddress: addressSchema, parentChainRpc: z.url(), }) .strict() .transform( - (input): Parameters => [ + (input): Parameters => [ input.inboxAddress, input.parentChainRpc, ], diff --git a/src/scripting/schemas/index.ts b/src/scripting/schemas/index.ts index 6d016e29f..fb73607cc 100644 --- a/src/scripting/schemas/index.ts +++ b/src/scripting/schemas/index.ts @@ -81,4 +81,4 @@ export { isAllowListEnabledSchema, isAllowedSchema, } from './actions'; -export { getChainContractVersionsSchema } from './getChainContractVersions'; +export { getNitroContractVersionsSchema } from './getNitroContractVersions'; From d23a420d82df808093f193935f0077ef93ec60e9 Mon Sep 17 00:00:00 2001 From: alxdca Date: Wed, 5 Aug 2026 18:33:20 +0200 Subject: [PATCH 4/5] feat: add a run forge script util --- src/utils/runForgeScript.ts | 84 +++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 src/utils/runForgeScript.ts diff --git a/src/utils/runForgeScript.ts b/src/utils/runForgeScript.ts new file mode 100644 index 000000000..c85650e4d --- /dev/null +++ b/src/utils/runForgeScript.ts @@ -0,0 +1,84 @@ +import { spawn } from 'node:child_process'; + +import { verifyFoundryBinaries } from './verifyFoundry'; + +export type RunForgeScriptParameters = { + script: string; + rpcUrl: string; + cwd?: string; + forgeArgs?: string[]; + env?: Record; +}; + +export type RunForgeScriptResult = { + stdout: string; + stderr: string; + exitCode: number; +}; + +/** Runs a Forge script after verifying that Foundry is installed. */ +export async function runForgeScript({ + script, + rpcUrl, + cwd, + forgeArgs = [], + env = {}, +}: RunForgeScriptParameters): Promise { + await verifyFoundryBinaries(); + + const args = ['script', script, '--rpc-url', rpcUrl, ...forgeArgs]; + const environment = { + ...process.env, + ...Object.fromEntries( + Object.entries(env).filter((entry): entry is [string, string] => entry[1] !== undefined), + ), + }; + + return new Promise((resolve, reject) => { + const child = spawn('forge', args, { + cwd, + env: environment, + stdio: ['ignore', 'pipe', 'pipe'], + }); + let stdout = ''; + let stderr = ''; + + child.stdout.setEncoding('utf8'); + child.stderr.setEncoding('utf8'); + child.stdout.on('data', (chunk: string) => { + stdout += chunk; + }); + child.stderr.on('data', (chunk: string) => { + stderr += chunk; + }); + + child.once('error', (cause) => { + reject( + Object.assign(new Error(`Unable to start Forge script: ${cause.message}`), { + name: 'RunForgeScriptError', + stdout, + stderr, + exitCode: 1, + cause, + }), + ); + }); + + child.once('close', (exitCode, signal) => { + if (exitCode !== 0) { + const reason = signal ? `signal ${signal}` : `exit code ${exitCode ?? 1}`; + reject( + Object.assign(new Error(`Forge script failed with ${reason}`), { + name: 'RunForgeScriptError', + stdout, + stderr, + exitCode: exitCode ?? 1, + }), + ); + return; + } + + resolve({ stdout, stderr, exitCode }); + }); + }); +} From df4738126a5d57472c4362fb020ce2eced5a91dc Mon Sep 17 00:00:00 2001 From: alxdca Date: Tue, 25 Aug 2026 15:55:08 +0200 Subject: [PATCH 5/5] feat: support 3.2.0 upgrade action --- src/index.ts | 16 ++++++ src/nitroContractsUpgrade.ts | 97 ++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 src/nitroContractsUpgrade.ts diff --git a/src/index.ts b/src/index.ts index 2201fabb5..efb9c1ad2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -207,6 +207,15 @@ import { } from './utils/erc20'; import { prepareArbitrumNetwork } from './utils/registerNewNetwork'; import { getNitroContractVersions } from './getNitroContractVersions'; +import { + NitroContractsUpgradeVersion, + DeployNitroContractsUpgradeActionParameters, + ExecuteNitroContractsUpgradeParameters, + VerifyNitroContractsUpgradeParameters, + deployNitroContractsUpgradeAction, + executeNitroContractsUpgrade, + verifyNitroContractsUpgrade, +} from './nitroContractsUpgrade'; export { arbOwnerPublicActions, @@ -383,6 +392,13 @@ export { prepareArbitrumNetwork, // getNitroContractVersions, + NitroContractsUpgradeVersion, + deployNitroContractsUpgradeAction, + DeployNitroContractsUpgradeActionParameters, + executeNitroContractsUpgrade, + ExecuteNitroContractsUpgradeParameters, + verifyNitroContractsUpgrade, + VerifyNitroContractsUpgradeParameters, }; export * from './scripting/scriptUtils'; diff --git a/src/nitroContractsUpgrade.ts b/src/nitroContractsUpgrade.ts new file mode 100644 index 000000000..443d21c7c --- /dev/null +++ b/src/nitroContractsUpgrade.ts @@ -0,0 +1,97 @@ +import { dirname, resolve } from 'node:path'; + +import type { Address } from 'viem'; + +import { runForgeScript, type RunForgeScriptResult } from './utils/runForgeScript'; + +export enum NitroContractsUpgradeVersion { + V3_2_0 = '3.2.0', +} + +export type DeployNitroContractsUpgradeActionParameters = { + version: NitroContractsUpgradeVersion; + parentChainRpcUrl: string; + forgeArgs?: string[]; +}; + +export type ExecuteNitroContractsUpgradeParameters = DeployNitroContractsUpgradeActionParameters & { + rollupAddress: Address; + parentUpgradeExecutorAddress: Address; + upgradeActionAddress: Address; +}; + +export type VerifyNitroContractsUpgradeParameters = DeployNitroContractsUpgradeActionParameters & { + rollupAddress: Address; +}; + +type UpgradeOperation = 'Deploy' | 'Execute' | 'Verify'; + +const upgradeScripts = { + [NitroContractsUpgradeVersion.V3_2_0]: { + Deploy: require.resolve( + '@arbitrum/chain-actions/scripts/foundry/contract-upgrades/3.2.0/DeployNitroContracts3Point2Point0UpgradeAction.s.sol', + ), + Execute: require.resolve( + '@arbitrum/chain-actions/scripts/foundry/contract-upgrades/3.2.0/ExecuteNitroContracts3Point2Point0Upgrade.s.sol', + ), + Verify: require.resolve( + '@arbitrum/chain-actions/scripts/foundry/contract-upgrades/3.2.0/VerifyNitroContracts3Point2Point0Upgrade.s.sol', + ), + }, +} satisfies Record>; + +function runUpgradeScript( + operation: UpgradeOperation, + params: DeployNitroContractsUpgradeActionParameters, + scriptEnvironment: Record = {}, +): Promise { + const chainActionsRoot = resolve(dirname(require.resolve('@arbitrum/chain-actions')), '../../..'); + const upgradeExecutorRoot = resolve( + dirname( + require.resolve('@offchainlabs/upgrade-executor/src/IUpgradeExecutor.sol', { + paths: [chainActionsRoot], + }), + ), + '..', + ); + + return runForgeScript({ + script: upgradeScripts[params.version][operation], + rpcUrl: params.parentChainRpcUrl, + forgeArgs: [ + '--root', + chainActionsRoot, + '--remappings', + `@offchainlabs/upgrade-executor/=${upgradeExecutorRoot}/`, + ...(params.forgeArgs ?? []), + ], + env: scriptEnvironment, + }); +} + +/** Deploys the upgrade action for a Nitro contracts version. */ +export function deployNitroContractsUpgradeAction( + params: DeployNitroContractsUpgradeActionParameters, +): Promise { + return runUpgradeScript('Deploy', params); +} + +/** Executes a Nitro contracts upgrade through the parent chain UpgradeExecutor. */ +export function executeNitroContractsUpgrade( + params: ExecuteNitroContractsUpgradeParameters, +): Promise { + return runUpgradeScript('Execute', params, { + ROLLUP_ADDRESS: params.rollupAddress, + PARENT_UPGRADE_EXECUTOR_ADDRESS: params.parentUpgradeExecutorAddress, + UPGRADE_ACTION_ADDRESS: params.upgradeActionAddress, + }); +} + +/** Verifies that a Nitro contracts upgrade was applied to the rollup. */ +export function verifyNitroContractsUpgrade( + params: VerifyNitroContractsUpgradeParameters, +): Promise { + return runUpgradeScript('Verify', params, { + ROLLUP_ADDRESS: params.rollupAddress, + }); +}