From c750bdacfc44a7ea02cee303e7fcb511cff3e4d0 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Thu, 5 Feb 2026 17:26:05 +0000 Subject: [PATCH 01/44] feat: add Docker support for running upgrade scripts Add Dockerfile and CI workflow to enable running orbit-actions commands in a containerized environment without requiring local installation of Foundry and Node.js. - Add Dockerfile with Node 18, Foundry, and pre-installed dependencies - Add smoke tests to verify tools and scripts are accessible - Add GitHub Actions workflow to build and test Docker image on PRs - Update README with Docker usage instructions --- .dockerignore | 30 +++++++++ .github/workflows/test-docker.yml | 30 +++++++++ Dockerfile | 30 +++++++++ README.md | 59 ++++++++++++++++++ package.json | 1 + test/docker/test-docker.bash | 100 ++++++++++++++++++++++++++++++ 6 files changed, 250 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/workflows/test-docker.yml create mode 100644 Dockerfile create mode 100755 test/docker/test-docker.bash diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..20bf558b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,30 @@ +# Dependencies (will be installed fresh in container) +node_modules/ + +# Build artifacts (will be built fresh in container) +out/ +cache_forge/ +cache/ +artifacts/ +typechain-types/ + +# Environment files (contain secrets) +.env +.env.* +!.env.example + +# Git +.git/ +.gitignore + +# IDE +.vscode/ +.idea/ + +# Test artifacts +broadcast/ +coverage/ + +# Docker +Dockerfile +.dockerignore diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml new file mode 100644 index 00000000..0d19a7c7 --- /dev/null +++ b/.github/workflows/test-docker.yml @@ -0,0 +1,30 @@ +name: Test Docker + +on: + pull_request: + workflow_dispatch: + +jobs: + test-docker: + name: Test Docker Image + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker image + uses: docker/build-push-action@v5 + with: + context: . + load: true + tags: orbit-actions:test + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Run Docker smoke tests + run: ./test/docker/test-docker.bash + env: + DOCKER_IMAGE: orbit-actions:test diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..bbf14796 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +FROM node:18-slim + +# Install dependencies for Foundry and git +RUN apt-get update && apt-get install -y \ + curl \ + git \ + && rm -rf /var/lib/apt/lists/* + +# Install Foundry +RUN curl -L https://foundry.paradigm.xyz | bash +ENV PATH="/root/.foundry/bin:${PATH}" +RUN foundryup + +# Enable Yarn via corepack +RUN corepack enable && corepack prepare yarn@stable --activate + +# Set working directory +WORKDIR /app + +# Copy package files first for better caching +COPY package.json yarn.lock ./ + +# Install dependencies +RUN yarn install --frozen-lockfile + +# Copy the rest of the repository +COPY . . + +# Build contracts +RUN forge build diff --git a/README.md b/README.md index 19be6454..d3d05e17 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,65 @@ For token bridge related operations, these are the additional requirements: yarn install ``` +## Using Docker + +The orbit actions are also available via docker. + +### Build the image + +```bash +docker build -t orbit-actions . +``` + +### Run commands + +Pass the command you want to run directly to Docker: + +```bash +# Check contract versions +docker run --rm \ + -e INBOX_ADDRESS=0xYourInboxAddress \ + -e INFURA_KEY=your_infura_key \ + orbit-actions \ + yarn orbit:contracts:version --network arb1 + +# Run forge script +docker run --rm \ + --env-file orbit.env \ + -v $(pwd)/broadcast:/app/broadcast \ + orbit-actions \ + forge script --sender 0xYourAddress --rpc-url $PARENT_CHAIN_RPC --broadcast \ + scripts/foundry/contract-upgrades/2.1.3/DeployNitroContracts2Point1Point3UpgradeAction.s.sol -vvv + +# Run cast commands +docker run --rm \ + orbit-actions \ + cast call --rpc-url https://arb1.arbitrum.io/rpc 0xYourRollup "wasmModuleRoot()" +``` + +### Environment variables + +Create an `orbit.env` file with your configuration and pass it using `--env-file`: + +```bash +PARENT_CHAIN_RPC=https://arb1.arbitrum.io/rpc +INBOX_ADDRESS=0x... +PROXY_ADMIN_ADDRESS=0x... +PARENT_UPGRADE_EXECUTOR_ADDRESS=0x... +``` + +### Getting output artifacts + +Mount a volume to retrieve broadcast artifacts: + +```bash +docker run --rm \ + --env-file orbit.env \ + -v $(pwd)/broadcast:/app/broadcast \ + orbit-actions \ + forge script ... +``` + ## Check Version and Upgrade Path Run the follow command to check the version of Nitro contracts deployed on the parent chain of your Orbit chain. diff --git a/package.json b/package.json index 74e3f3ba..9c12cdb1 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "test:gas-check": "forge snapshot --check --tolerance 1 --match-path \"test/unit/**/*.t.sol\"", "test:sigs": "./test/signatures/test-sigs.bash", "test:storage": "./test/storage/test-storage.bash", + "test:docker": "./test/docker/test-docker.bash", "orbit:contracts:version": "hardhat run scripts/orbit-versioner/orbitVersioner.ts", "gas-snapshot": "forge snapshot --match-path \"test/unit/**/*.t.sol\"", "fix": "yarn format; yarn test:sigs; yarn test:storage; yarn gas-snapshot" diff --git a/test/docker/test-docker.bash b/test/docker/test-docker.bash new file mode 100755 index 00000000..25f54431 --- /dev/null +++ b/test/docker/test-docker.bash @@ -0,0 +1,100 @@ +#!/bin/bash +set -euo pipefail + +# Docker smoke tests for orbit-actions +# Verifies that all required tools and scripts are accessible in the Docker image + +IMAGE_NAME="${DOCKER_IMAGE:-orbit-actions:test}" + +echo "=== Docker Smoke Tests ===" +echo "Image: $IMAGE_NAME" +echo "" + +# Track failures +FAILURES=0 + +run_test() { + local name="$1" + shift + echo -n "Testing $name... " + if "$@" > /dev/null 2>&1; then + echo "OK" + else + echo "FAILED" + FAILURES=$((FAILURES + 1)) + fi +} + +# Test 1: Tools are installed +echo "--- Tool Availability ---" +run_test "forge" docker run --rm "$IMAGE_NAME" forge --version +run_test "cast" docker run --rm "$IMAGE_NAME" cast --version +run_test "yarn" docker run --rm "$IMAGE_NAME" yarn --version +run_test "node" docker run --rm "$IMAGE_NAME" node --version + +# Test 2: Dependencies are installed +echo "" +echo "--- Dependencies ---" +run_test "node_modules exists" docker run --rm "$IMAGE_NAME" test -d node_modules +run_test "forge dependencies" docker run --rm "$IMAGE_NAME" test -d node_modules/@arbitrum + +# Test 3: Contracts compile +echo "" +echo "--- Contract Compilation ---" +run_test "contracts built" docker run --rm "$IMAGE_NAME" test -d out + +# Test 4: Scripts are accessible +echo "" +echo "--- Script Accessibility ---" + +DEPLOY_SCRIPTS=( + "scripts/foundry/contract-upgrades/1.2.1/DeployNitroContracts1Point2Point1UpgradeAction.s.sol" + "scripts/foundry/contract-upgrades/2.1.0/DeployNitroContracts2Point1Point0UpgradeAction.s.sol" + "scripts/foundry/contract-upgrades/2.1.2/DeployNitroContracts2Point1Point2UpgradeAction.s.sol" + "scripts/foundry/contract-upgrades/2.1.3/DeployNitroContracts2Point1Point3UpgradeAction.s.sol" + "scripts/foundry/arbos-upgrades/at-timestamp/DeployUpgradeArbOSVersionAtTimestampAction.s.sol" +) + +for script in "${DEPLOY_SCRIPTS[@]}"; do + script_name=$(basename "$script") + run_test "$script_name exists" docker run --rm "$IMAGE_NAME" test -f "$script" +done + +EXECUTE_SCRIPTS=( + "scripts/foundry/contract-upgrades/1.2.1/ExecuteNitroContracts1Point2Point1Upgrade.s.sol" + "scripts/foundry/contract-upgrades/2.1.0/ExecuteNitroContracts2Point1Point0Upgrade.s.sol" + "scripts/foundry/contract-upgrades/2.1.2/ExecuteNitroContracts2Point1Point2Upgrade.s.sol" + "scripts/foundry/contract-upgrades/2.1.3/ExecuteNitroContracts2Point1Point3Upgrade.s.sol" +) + +for script in "${EXECUTE_SCRIPTS[@]}"; do + script_name=$(basename "$script") + run_test "$script_name exists" docker run --rm "$IMAGE_NAME" test -f "$script" +done + +# Test 5: Yarn scripts work +echo "" +echo "--- Yarn Scripts ---" +run_test "yarn orbit:contracts:version --help" docker run --rm "$IMAGE_NAME" yarn orbit:contracts:version --help + +# Test 6: Unit tests pass +echo "" +echo "--- Unit Tests ---" +echo "Running unit tests inside container..." +if docker run --rm "$IMAGE_NAME" yarn test:unit; then + echo "Unit tests: OK" +else + echo "Unit tests: FAILED" + FAILURES=$((FAILURES + 1)) +fi + +# Summary +echo "" +echo "=== Summary ===" +if [ $FAILURES -eq 0 ]; then + echo "All tests passed!" + exit 0 +else + echo "$FAILURES test(s) failed" + exit 1 +fi From 1f0f4f378dd587fd2aa561da21b64a22f3872ce6 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Thu, 5 Feb 2026 17:32:14 +0000 Subject: [PATCH 02/44] chore: add .DS_Store to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8e35eb36..973fff40 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules .env +.DS_Store # Hardhat files /cache From eea7623101dce1276aa379b8b8a3abb977232f9a Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Thu, 5 Feb 2026 17:37:10 +0000 Subject: [PATCH 03/44] fix: use Yarn Classic (v1) in Dockerfile for lockfile compatibility --- Dockerfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index bbf14796..6e502b1f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,8 +11,8 @@ RUN curl -L https://foundry.paradigm.xyz | bash ENV PATH="/root/.foundry/bin:${PATH}" RUN foundryup -# Enable Yarn via corepack -RUN corepack enable && corepack prepare yarn@stable --activate +# Install Yarn Classic (v1) - matches the repo's yarn.lock format +RUN npm install -g --force yarn@1.22.22 # Set working directory WORKDIR /app @@ -20,8 +20,8 @@ WORKDIR /app # Copy package files first for better caching COPY package.json yarn.lock ./ -# Install dependencies -RUN yarn install --frozen-lockfile +# Install dependencies (using --ignore-scripts like CI does, then forge install separately) +RUN yarn install --frozen-lockfile --ignore-scripts # Copy the rest of the repository COPY . . From c5948fafb940d21b006e9249fa5d7404f2c4dcb2 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Thu, 5 Feb 2026 17:38:38 +0000 Subject: [PATCH 04/44] fix: add submodule checkout for Docker build The Docker build requires lib/ (forge-std, arbitrum-sdk) which comes from git submodules. Update CI to checkout with submodules and document the prerequisite for local builds. --- .github/workflows/test-docker.yml | 2 ++ README.md | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 0d19a7c7..eea090e4 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -11,6 +11,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + submodules: recursive - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 diff --git a/README.md b/README.md index d3d05e17..09227ba2 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,14 @@ The orbit actions are also available via docker. ### Build the image +First, ensure submodules are initialized (required for Foundry dependencies): + +```bash +git submodule update --init --recursive +``` + +Then build the image: + ```bash docker build -t orbit-actions . ``` From 4c8edb87fef5f35cb4f9f9ae43db72d375628ade Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Thu, 5 Feb 2026 17:40:22 +0000 Subject: [PATCH 05/44] fix: use forge install instead of git submodules for Docker build prereq --- .github/workflows/test-docker.yml | 8 +++++++- README.md | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index eea090e4..04a7f78a 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -11,8 +11,14 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 with: - submodules: recursive + version: stable + + - name: Install forge dependencies + run: forge install - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 diff --git a/README.md b/README.md index 09227ba2..93753919 100644 --- a/README.md +++ b/README.md @@ -31,10 +31,10 @@ The orbit actions are also available via docker. ### Build the image -First, ensure submodules are initialized (required for Foundry dependencies): +First, ensure Foundry dependencies are installed: ```bash -git submodule update --init --recursive +forge install ``` Then build the image: From 9cb07c5754dbfe8c71f795b9a56f0b7794f6e927 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Thu, 5 Feb 2026 17:47:43 +0000 Subject: [PATCH 06/44] Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 93753919..90383e73 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ yarn install ## Using Docker -The orbit actions are also available via docker. +The Orbit actions are also available via Docker. ### Build the image From c445d7d2709b6db32f017d3cc84fca056dbaa05b Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Fri, 6 Feb 2026 17:05:48 +0000 Subject: [PATCH 07/44] feat: add browsable CLI for upgrade scripts Add a path-based CLI that allows users to browse and execute scripts from the foundry directory: - Browse directories: `docker run orbit-actions contract-upgrades/1.2.1` - View files: `docker run orbit-actions contract-upgrades/1.2.1/README.md` - Run upgrades: `docker run orbit-actions contract-upgrades/1.2.1/deploy-execute-verify` Structure: - entrypoint.sh: thin shim that sources .env and delegates to router - bin/router: path parsing, directory listing, command dispatch - bin/contract-upgrade: deploy, execute, deploy-execute-verify commands - bin/arbos-upgrade: deploy, execute, verify, deploy-execute-verify commands - lib/common.sh: shared utilities for auth parsing and forge helpers Upgrade commands read configuration from mounted .env file and accept auth flags (--deploy-key, --execute-key, --ledger, etc.) for signing. --- Dockerfile | 9 +- bin/arbos-upgrade | 320 +++++++++++++++++++++++++++++++++++ bin/contract-upgrade | 281 ++++++++++++++++++++++++++++++ bin/router | 188 ++++++++++++++++++++ entrypoint.sh | 4 + lib/common.sh | 135 +++++++++++++++ test/docker/test-docker.bash | 132 +++++++++++---- 7 files changed, 1036 insertions(+), 33 deletions(-) create mode 100644 bin/arbos-upgrade create mode 100644 bin/contract-upgrade create mode 100644 bin/router create mode 100755 entrypoint.sh create mode 100644 lib/common.sh diff --git a/Dockerfile b/Dockerfile index 6e502b1f..9d306176 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,10 @@ FROM node:18-slim -# Install dependencies for Foundry and git +# Install dependencies for Foundry, git, and jq (for JSON parsing in upgrade scripts) RUN apt-get update && apt-get install -y \ curl \ git \ + jq \ && rm -rf /var/lib/apt/lists/* # Install Foundry @@ -28,3 +29,9 @@ COPY . . # Build contracts RUN forge build + +# Make scripts executable +RUN chmod +x /app/entrypoint.sh /app/bin/* /app/lib/* + +# Set entrypoint for command routing +ENTRYPOINT ["/app/entrypoint.sh"] diff --git a/bin/arbos-upgrade b/bin/arbos-upgrade new file mode 100644 index 00000000..3cccd9a5 --- /dev/null +++ b/bin/arbos-upgrade @@ -0,0 +1,320 @@ +#!/bin/bash +set -euo pipefail + +# ArbOS upgrades script for orbit-actions +# Handles deploy, execute, verify, and deploy-execute-verify commands + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/../lib/common.sh" + +ARBOS_DIR="/app/scripts/foundry/arbos-upgrades/at-timestamp" + +# ============================================================================= +# Commands +# ============================================================================= + +cmd_deploy() { + local version="$1" + shift + + parse_auth_args "$@" + + require_env CHILD_CHAIN_RPC + + export ARBOS_VERSION="$version" + + local deploy_script="$ARBOS_DIR/DeployUpgradeArbOSVersionAtTimestampAction.s.sol" + if [[ ! -f "$deploy_script" ]]; then + die "Deploy script not found: $deploy_script" + fi + + log "Running: $(basename "$deploy_script") for ArbOS $version" + + local cmd="forge script $deploy_script --rpc-url $CHILD_CHAIN_RPC --slow -vvv" + + if [[ -n "$AUTH_ARGS" ]]; then + cmd="$cmd --broadcast $AUTH_ARGS" + fi + + eval $cmd +} + +cmd_execute() { + shift # version not needed for execute + + parse_auth_args "$@" + + require_env CHILD_CHAIN_RPC + require_env CHILD_UPGRADE_EXECUTOR_ADDRESS + require_env UPGRADE_ACTION_ADDRESS + + log "Executing ArbOS upgrade action: $UPGRADE_ACTION_ADDRESS" + + local perform_calldata="0xb0a75d36" + + if [[ -z "$AUTH_ARGS" ]]; then + # No auth - output calldata for multisig + local execute_calldata + execute_calldata=$(cast calldata "execute(address,bytes)" "$UPGRADE_ACTION_ADDRESS" "$perform_calldata") + + log "Calldata for UpgradeExecutor.execute():" + echo "" + echo "To: $CHILD_UPGRADE_EXECUTOR_ADDRESS" + echo "Calldata: $execute_calldata" + echo "" + log "Submit this to your multisig/Safe to execute the upgrade" + else + local cast_cmd="cast send $CHILD_UPGRADE_EXECUTOR_ADDRESS \"execute(address,bytes)\" $UPGRADE_ACTION_ADDRESS $perform_calldata --rpc-url $CHILD_CHAIN_RPC $AUTH_ARGS" + + eval $cast_cmd + + log "ArbOS upgrade scheduled successfully" + fi +} + +cmd_verify() { + shift # version not needed for verify + + require_env CHILD_CHAIN_RPC + + log "Checking ArbOS upgrade status..." + + local scheduled + scheduled=$(cast call --rpc-url "$CHILD_CHAIN_RPC" \ + "0x000000000000000000000000000000000000006b" \ + "getScheduledUpgrade()(uint64,uint64)" 2>/dev/null || echo "N/A") + log "Scheduled upgrade (version, timestamp): $scheduled" + + local current_raw + current_raw=$(cast call --rpc-url "$CHILD_CHAIN_RPC" \ + "0x0000000000000000000000000000000000000064" \ + "arbOSVersion()(uint64)" 2>/dev/null || echo "0") + local current_version=$((current_raw - 55)) + log "Current ArbOS version: $current_version" +} + +cmd_deploy_execute_verify() { + local version="$1" + shift + + parse_deploy_execute_auth "$@" + + log "ArbOS version: $version" + + local deploy_script="$ARBOS_DIR/DeployUpgradeArbOSVersionAtTimestampAction.s.sol" + if [[ ! -f "$deploy_script" ]]; then + die "Deploy script not found: $deploy_script" + fi + + # Auto-detect skip_deploy if UPGRADE_ACTION_ADDRESS is set + local skip_deploy=false + if [[ -n "${UPGRADE_ACTION_ADDRESS:-}" ]]; then + skip_deploy=true + log "Using existing action from .env: $UPGRADE_ACTION_ADDRESS" + fi + + # Validate required env vars + require_env CHILD_CHAIN_RPC + require_env CHILD_UPGRADE_EXECUTOR_ADDRESS + require_env SCHEDULE_TIMESTAMP + + export ARBOS_VERSION="$version" + + # Validate auth + local deploy_auth=$(get_deploy_auth) + local execute_auth=$(get_execute_auth) + + if [[ "$skip_deploy" != "true" && "$DRY_RUN" != "true" && -z "$deploy_auth" ]]; then + die "Deploy auth required. Use --deploy-key, --deploy-account, --deploy-ledger, or --deploy-interactive" + fi + if [[ "$SKIP_EXECUTE" != "true" && "$DRY_RUN" != "true" && -z "$execute_auth" ]]; then + die "Execute auth required. Use --execute-key, --execute-account, --execute-ledger, or --execute-interactive" + fi + + log "Scheduled timestamp: $SCHEDULE_TIMESTAMP" + + # Step 1: Deploy + local chain_id="" + if [[ "$skip_deploy" != "true" ]]; then + chain_id=$(get_chain_id "$CHILD_CHAIN_RPC") + log "Target chain ID: $chain_id" + log "Step 1: Deploying ArbOS upgrade action..." + + local forge_cmd="forge script $deploy_script --rpc-url $CHILD_CHAIN_RPC --slow -vvv" + + if [[ "$DRY_RUN" != "true" ]]; then + forge_cmd="$forge_cmd --broadcast $deploy_auth" + fi + if [[ "$VERIFY_CONTRACTS" == "true" ]]; then + forge_cmd="$forge_cmd --verify" + fi + + eval $forge_cmd + + if [[ "$DRY_RUN" != "true" ]]; then + UPGRADE_ACTION_ADDRESS=$(parse_action_address "$deploy_script" "$chain_id") + log "Deployed action at: $UPGRADE_ACTION_ADDRESS" + else + log "Dry run - no action deployed" + if [[ "$SKIP_EXECUTE" != "true" ]]; then + log "Note: Set UPGRADE_ACTION_ADDRESS in .env to run execute step" + return 0 + fi + fi + else + log "Step 1: Skipped deploy" + fi + + # Step 2: Execute via cast send + if [[ "$SKIP_EXECUTE" != "true" ]]; then + log "Step 2: Executing ArbOS upgrade..." + + local perform_calldata="0xb0a75d36" + + if [[ "$DRY_RUN" == "true" ]]; then + local execute_calldata + execute_calldata=$(cast calldata "execute(address,bytes)" "$UPGRADE_ACTION_ADDRESS" "$perform_calldata") + + log "Dry run - calldata for UpgradeExecutor.execute():" + echo "" + echo "To: $CHILD_UPGRADE_EXECUTOR_ADDRESS" + echo "Calldata: $execute_calldata" + echo "" + log "Submit this to your multisig/Safe to execute the upgrade" + else + local cast_cmd="cast send $CHILD_UPGRADE_EXECUTOR_ADDRESS \"execute(address,bytes)\" $UPGRADE_ACTION_ADDRESS $perform_calldata --rpc-url $CHILD_CHAIN_RPC" + + if [[ -n "$EXECUTE_KEY" ]]; then + cast_cmd="$cast_cmd --private-key $EXECUTE_KEY" + elif [[ -n "$EXECUTE_ACCOUNT" ]]; then + cast_cmd="$cast_cmd --account $EXECUTE_ACCOUNT" + elif [[ "$EXECUTE_LEDGER" == "true" ]]; then + cast_cmd="$cast_cmd --ledger" + elif [[ "$EXECUTE_INTERACTIVE" == "true" ]]; then + cast_cmd="$cast_cmd --interactive" + fi + + eval $cast_cmd + + log "ArbOS upgrade scheduled successfully" + fi + else + log "Step 2: Skipped execute" + fi + + # Step 3: Verify + if [[ "$DRY_RUN" != "true" && "$SKIP_EXECUTE" != "true" ]]; then + log "Step 3: Verifying scheduled upgrade..." + + local scheduled + scheduled=$(cast call --rpc-url "$CHILD_CHAIN_RPC" \ + "0x000000000000000000000000000000000000006b" \ + "getScheduledUpgrade()(uint64,uint64)" 2>/dev/null || echo "N/A") + log "Scheduled upgrade (version, timestamp): $scheduled" + + local current_raw + current_raw=$(cast call --rpc-url "$CHILD_CHAIN_RPC" \ + "0x0000000000000000000000000000000000000064" \ + "arbOSVersion()(uint64)" 2>/dev/null || echo "0") + local current_version=$((current_raw - 55)) + log "Current ArbOS version: $current_version" + fi + + log "Done" +} + +# ============================================================================= +# Main +# ============================================================================= + +show_help() { + cat <<'EOF' +Usage: arbos-upgrade [options] + +Commands: + deploy Run deploy script only + execute Execute upgrade action (schedule the upgrade) + verify Check scheduled upgrade status + deploy-execute-verify Full upgrade flow (deploy, execute, verify) + +Options for deploy: + --private-key KEY Private key + --account NAME Keystore account + --ledger Use Ledger + --interactive Prompt for key + +Options for deploy-execute-verify: + --deploy-key KEY Private key for deploy step + --deploy-account NAME Keystore account for deploy + --deploy-ledger Use Ledger for deploy + --execute-key KEY Private key for execute step + --execute-account NAME Keystore account for execute + --execute-ledger Use Ledger for execute + --dry-run, -n Simulate without broadcasting + --skip-execute Deploy only + --verify, -v Verify on block explorer + +Required .env variables: + CHILD_CHAIN_RPC Child chain RPC URL + CHILD_UPGRADE_EXECUTOR_ADDRESS UpgradeExecutor address + SCHEDULE_TIMESTAMP Unix timestamp for upgrade + +Optional .env variables: + ARBOS_VERSION ArbOS version (alternative to positional arg) + UPGRADE_ACTION_ADDRESS Pre-deployed action (skips deploy) +EOF +} + +main() { + if [[ $# -lt 1 ]]; then + show_help + exit 1 + fi + + local version="$1" + shift + + if [[ "$version" == "--help" || "$version" == "-h" ]]; then + show_help + exit 0 + fi + + # Version can also come from env + if [[ -z "$version" && -n "${ARBOS_VERSION:-}" ]]; then + version="$ARBOS_VERSION" + fi + + if [[ -z "$version" ]]; then + die "ArbOS version required" + fi + + local command="${1:-deploy-execute-verify}" + if [[ $# -gt 0 ]]; then + shift + fi + + case "$command" in + deploy) + cmd_deploy "$version" "$@" + ;; + execute) + cmd_execute "$version" "$@" + ;; + verify) + cmd_verify "$version" "$@" + ;; + deploy-execute-verify) + cmd_deploy_execute_verify "$version" "$@" + ;; + --help|-h) + show_help + ;; + *) + die "Unknown command: $command + +Commands: deploy, execute, verify, deploy-execute-verify" + ;; + esac +} + +main "$@" diff --git a/bin/contract-upgrade b/bin/contract-upgrade new file mode 100644 index 00000000..f2344552 --- /dev/null +++ b/bin/contract-upgrade @@ -0,0 +1,281 @@ +#!/bin/bash +set -euo pipefail + +# Contract upgrades script for orbit-actions +# Handles deploy, execute, and deploy-execute-verify commands + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/../lib/common.sh" + +CONTRACTS_DIR="/app/scripts/foundry/contract-upgrades" + +# ============================================================================= +# Commands +# ============================================================================= + +cmd_deploy() { + local version_dir="$1" + shift + + parse_auth_args "$@" + + require_env PARENT_CHAIN_RPC + + local deploy_script=$(find "$version_dir" -maxdepth 1 -name "Deploy*.s.sol" -type f 2>/dev/null | head -1) + if [[ -z "$deploy_script" ]]; then + die "No deploy script found in $version_dir" + fi + + log "Running: $(basename "$deploy_script")" + + local cmd="forge script $deploy_script --rpc-url $PARENT_CHAIN_RPC --slow -vvv --skip-simulation" + + if [[ -n "$AUTH_ARGS" ]]; then + cmd="$cmd --broadcast $AUTH_ARGS" + fi + + eval $cmd +} + +cmd_execute() { + local version_dir="$1" + shift + + parse_auth_args "$@" + + require_env PARENT_CHAIN_RPC + require_env UPGRADE_ACTION_ADDRESS + + local execute_script=$(find "$version_dir" -maxdepth 1 -name "Execute*.s.sol" -type f 2>/dev/null | head -1) + if [[ -z "$execute_script" ]]; then + die "No execute script found in $version_dir" + fi + + log "Running: $(basename "$execute_script")" + + local cmd="forge script $execute_script --rpc-url $PARENT_CHAIN_RPC -vvv" + + if [[ -n "$AUTH_ARGS" ]]; then + cmd="$cmd --broadcast $AUTH_ARGS" + fi + + eval $cmd +} + +cmd_deploy_execute_verify() { + local version_dir="$1" + local version=$(basename "$version_dir") + shift + + parse_deploy_execute_auth "$@" + + # Find scripts + local deploy_script=$(find "$version_dir" -maxdepth 1 -name "Deploy*.s.sol" -type f 2>/dev/null | head -1) + local execute_script=$(find "$version_dir" -maxdepth 1 -name "Execute*.s.sol" -type f 2>/dev/null | head -1) + + if [[ -z "$deploy_script" ]]; then + die "No deploy script found in $version_dir" + fi + if [[ -z "$execute_script" ]]; then + die "No execute script found in $version_dir" + fi + + log "Version: $version" + log "Deploy script: $(basename "$deploy_script")" + log "Execute script: $(basename "$execute_script")" + + # Auto-detect skip_deploy if UPGRADE_ACTION_ADDRESS is set + local skip_deploy=false + if [[ -n "${UPGRADE_ACTION_ADDRESS:-}" ]]; then + skip_deploy=true + log "Using existing action from .env: $UPGRADE_ACTION_ADDRESS" + fi + + # Validate required env vars + require_env PARENT_CHAIN_RPC + require_env INBOX_ADDRESS + require_env PROXY_ADMIN_ADDRESS + require_env PARENT_UPGRADE_EXECUTOR_ADDRESS + + # Validate auth + local deploy_auth=$(get_deploy_auth) + local execute_auth=$(get_execute_auth) + + if [[ "$skip_deploy" != "true" && "$DRY_RUN" != "true" && -z "$deploy_auth" ]]; then + die "Deploy auth required. Use --deploy-key, --deploy-account, --deploy-ledger, or --deploy-interactive" + fi + if [[ "$SKIP_EXECUTE" != "true" && "$DRY_RUN" != "true" && -z "$execute_auth" ]]; then + die "Execute auth required. Use --execute-key, --execute-account, --execute-ledger, or --execute-interactive" + fi + + local chain_id + chain_id=$(get_chain_id "$PARENT_CHAIN_RPC") + log "Target chain ID: $chain_id" + + # Step 1: Deploy + if [[ "$skip_deploy" != "true" ]]; then + log "Step 1: Deploying upgrade action..." + + local forge_cmd="forge script $deploy_script --rpc-url $PARENT_CHAIN_RPC --slow -vvv --skip-simulation" + + if [[ "$DRY_RUN" != "true" ]]; then + forge_cmd="$forge_cmd --broadcast $deploy_auth" + fi + if [[ "$VERIFY_CONTRACTS" == "true" ]]; then + forge_cmd="$forge_cmd --verify" + fi + + eval $forge_cmd + + if [[ "$DRY_RUN" != "true" ]]; then + UPGRADE_ACTION_ADDRESS=$(parse_action_address "$deploy_script" "$chain_id") + log "Deployed action at: $UPGRADE_ACTION_ADDRESS" + else + log "Dry run - no action deployed" + if [[ "$SKIP_EXECUTE" != "true" ]]; then + log "Note: Set UPGRADE_ACTION_ADDRESS in .env to run execute step" + return 0 + fi + fi + else + log "Step 1: Skipped deploy" + fi + + export UPGRADE_ACTION_ADDRESS + + # Step 2: Execute + if [[ "$SKIP_EXECUTE" != "true" ]]; then + log "Step 2: Executing upgrade..." + + local forge_cmd="forge script $execute_script --rpc-url $PARENT_CHAIN_RPC -vvv" + + if [[ "$DRY_RUN" != "true" ]]; then + forge_cmd="$forge_cmd --broadcast $execute_auth" + fi + + eval $forge_cmd + + if [[ "$DRY_RUN" == "true" ]]; then + log "Dry run - upgrade not executed" + else + log "Upgrade executed successfully" + fi + else + log "Step 2: Skipped execute" + fi + + # Step 3: Verify + if [[ "$DRY_RUN" != "true" && "$SKIP_EXECUTE" != "true" ]]; then + log "Step 3: Verifying upgrade..." + + case "$version" in + 2.1.2|2.1.3) + local bridge + bridge=$(cast call --rpc-url "$PARENT_CHAIN_RPC" "$INBOX_ADDRESS" "bridge()(address)" 2>/dev/null || echo "") + if [[ -n "$bridge" && "$bridge" != "0x" ]]; then + local decimals + decimals=$(cast call --rpc-url "$PARENT_CHAIN_RPC" "$bridge" "nativeTokenDecimals()(uint8)" 2>/dev/null || echo "N/A") + log "Verification: nativeTokenDecimals = $decimals" + fi + ;; + 1.2.1|2.1.0) + if [[ -n "${ROLLUP:-}" ]]; then + local wasm_root + wasm_root=$(cast call --rpc-url "$PARENT_CHAIN_RPC" "$ROLLUP" "wasmModuleRoot()(bytes32)" 2>/dev/null || echo "N/A") + log "Verification: wasmModuleRoot = $wasm_root" + else + log "Verification: Set ROLLUP in .env to check wasmModuleRoot" + fi + ;; + esac + fi + + log "Done" +} + +# ============================================================================= +# Main +# ============================================================================= + +show_help() { + cat <<'EOF' +Usage: contract-upgrade [options] + +Commands: + deploy Run deploy script only + execute Run execute script only + deploy-execute-verify Full upgrade flow (deploy, execute, verify) + +Options for deploy/execute: + --private-key KEY Private key + --account NAME Keystore account + --ledger Use Ledger + --interactive Prompt for key + +Options for deploy-execute-verify: + --deploy-key KEY Private key for deploy step + --deploy-account NAME Keystore account for deploy + --deploy-ledger Use Ledger for deploy + --execute-key KEY Private key for execute step + --execute-account NAME Keystore account for execute + --execute-ledger Use Ledger for execute + --dry-run, -n Simulate without broadcasting + --skip-execute Deploy only + --verify, -v Verify on block explorer + +Required .env variables: + PARENT_CHAIN_RPC Parent chain RPC URL + INBOX_ADDRESS Inbox contract address + PROXY_ADMIN_ADDRESS ProxyAdmin address + PARENT_UPGRADE_EXECUTOR_ADDRESS UpgradeExecutor address + +Optional .env variables: + UPGRADE_ACTION_ADDRESS Pre-deployed action (skips deploy) + ROLLUP Rollup address (for verification) +EOF +} + +main() { + if [[ $# -lt 2 ]]; then + show_help + exit 1 + fi + + local version="$1" + local command="$2" + shift 2 + + if [[ "$version" == "--help" || "$version" == "-h" ]]; then + show_help + exit 0 + fi + + local version_dir="$CONTRACTS_DIR/$version" + if [[ ! -d "$version_dir" ]]; then + die "Unknown version: $version + +Available versions: $(ls "$CONTRACTS_DIR" 2>/dev/null | tr '\n' ' ' || echo "none found")" + fi + + case "$command" in + deploy) + cmd_deploy "$version_dir" "$@" + ;; + execute) + cmd_execute "$version_dir" "$@" + ;; + deploy-execute-verify) + cmd_deploy_execute_verify "$version_dir" "$@" + ;; + --help|-h) + show_help + ;; + *) + die "Unknown command: $command + +Commands: deploy, execute, deploy-execute-verify" + ;; + esac +} + +main "$@" diff --git a/bin/router b/bin/router new file mode 100644 index 00000000..609873a8 --- /dev/null +++ b/bin/router @@ -0,0 +1,188 @@ +#!/bin/bash +set -euo pipefail + +# ============================================================================= +# orbit-actions router +# Handles path parsing, directory listing, file viewing, and command dispatch +# ============================================================================= + +SCRIPTS_DIR="/app/scripts/foundry" + +# ============================================================================= +# Utility Functions +# ============================================================================= + +die() { + echo "Error: $1" >&2 + exit 1 +} + +# ============================================================================= +# Directory Listing +# ============================================================================= + +list_directory() { + local dir="$1" + local rel_path="${dir#$SCRIPTS_DIR/}" + # Normalize: remove leading ./ + rel_path="${rel_path#./}" + + # List actual contents + ls -1 "$dir" + + # Add virtual commands based on directory type + case "$rel_path" in + contract-upgrades/[0-9]*) + echo "---" + echo "deploy (run Deploy script)" + echo "execute (run Execute script)" + echo "deploy-execute-verify (full upgrade flow)" + ;; + arbos-upgrades/at-timestamp) + echo "---" + echo "deploy (run Deploy script)" + echo "execute (execute upgrade action)" + echo "verify (check upgrade status)" + echo "deploy-execute-verify (full upgrade flow)" + ;; + esac +} + +# ============================================================================= +# Help +# ============================================================================= + +show_help() { + cat <<'EOF' +Usage: docker run orbit-actions [path] [args...] + +Browse and execute scripts from the foundry scripts directory. + +Browsing: + . List top-level directories + contract-upgrades List available versions + contract-upgrades/1.2.1 List version contents + commands + contract-upgrades/1.2.1/env-templates List env templates + +Viewing files: + contract-upgrades/1.2.1/README.md View README + contract-upgrades/1.2.1/env-templates/.env.example View env template + contract-upgrades/2.1.0/.env.sample View env sample + +Running upgrade scripts: + contract-upgrades//deploy [--private-key KEY] + contract-upgrades//execute [--private-key KEY] + contract-upgrades//deploy-execute-verify [--deploy-key KEY] [--execute-key KEY] + + arbos-upgrades/at-timestamp/deploy [--private-key KEY] + arbos-upgrades/at-timestamp/deploy-execute-verify [--deploy-key KEY] [--execute-key KEY] + +Options for deploy-execute-verify: + --deploy-key KEY Private key for deploy step + --deploy-account NAME Keystore account for deploy + --deploy-ledger Use Ledger for deploy + --execute-key KEY Private key for execute step + --execute-account NAME Keystore account for execute + --execute-ledger Use Ledger for execute + --dry-run, -n Simulate without broadcasting + --skip-execute Deploy only + --verify, -v Verify on block explorer + +Passthrough commands: + forge ... Run forge directly + cast ... Run cast directly + yarn ... Run yarn directly + +Examples: + docker run orbit-actions contract-upgrades/1.2.1 + docker run orbit-actions contract-upgrades/1.2.1/README.md + docker run -v $(pwd)/.env:/app/.env orbit-actions contract-upgrades/1.2.1/deploy-execute-verify --dry-run +EOF +} + +# ============================================================================= +# Main Router +# ============================================================================= + +main() { + # No args - list top level + if [[ $# -eq 0 ]]; then + ls -1 "$SCRIPTS_DIR" + exit 0 + fi + + local path="$1" + shift + + # Help + if [[ "$path" == "help" || "$path" == "--help" || "$path" == "-h" ]]; then + show_help + exit 0 + fi + + # Passthrough: forge, cast, yarn, node, bash, etc. + if command -v "$path" &>/dev/null && [[ ! -e "$SCRIPTS_DIR/$path" ]]; then + exec "$path" "$@" + fi + + # Parse path for virtual commands + local full_path="$SCRIPTS_DIR/$path" + local parent_path=$(dirname "$full_path") + local basename=$(basename "$path") + + # Check for virtual commands (deploy, execute, deploy-execute-verify) + if [[ ! -e "$full_path" && -d "$parent_path" ]]; then + local rel_parent="${parent_path#$SCRIPTS_DIR/}" + # Normalize: remove leading ./ + rel_parent="${rel_parent#./}" + + case "$rel_parent" in + contract-upgrades/[0-9]*) + local version=$(basename "$rel_parent") + case "$basename" in + deploy|execute|deploy-execute-verify) + exec /app/bin/contract-upgrade "$version" "$basename" "$@" + ;; + esac + ;; + arbos-upgrades/at-timestamp) + case "$basename" in + deploy|deploy-execute-verify) + # These commands need a version argument + local version="${1:-}" + if [[ -z "$version" ]]; then + echo "Error: ArbOS version required" >&2 + echo "Usage: arbos-upgrades/at-timestamp/$basename [options]" >&2 + exit 1 + fi + shift + exec /app/bin/arbos-upgrade "$version" "$basename" "$@" + ;; + execute|verify) + # These commands don't need a version argument + exec /app/bin/arbos-upgrade "" "$basename" "$@" + ;; + esac + ;; + esac + fi + + # Directory - list contents + if [[ -d "$full_path" ]]; then + list_directory "$full_path" + exit 0 + fi + + # Regular file - cat it + if [[ -f "$full_path" ]]; then + cat "$full_path" + exit 0 + fi + + # Not found + die "Not found: $path + +Use 'help' to see available commands." +} + +main "$@" diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 00000000..965dfa52 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,4 @@ +#!/bin/bash +# Source .env if mounted, then delegate to router +[[ -f /app/.env ]] && set -a && source /app/.env && set +a +exec /app/bin/router "$@" diff --git a/lib/common.sh b/lib/common.sh new file mode 100644 index 00000000..9a4ac560 --- /dev/null +++ b/lib/common.sh @@ -0,0 +1,135 @@ +#!/bin/bash +# Shared utilities for upgrade scripts + +# ============================================================================= +# Utility Functions +# ============================================================================= + +die() { + echo "Error: $1" >&2 + exit 1 +} + +log() { + echo "[orbit-actions] $1" +} + +require_env() { + local name="$1" + local value="${!name:-}" + if [[ -z "$value" ]]; then + die "Required env var not set: $name (check your .env file)" + fi +} + +# ============================================================================= +# Auth Helpers +# ============================================================================= + +# Build forge/cast auth args from CLI flags +# Sets global: AUTH_ARGS +parse_auth_args() { + AUTH_ARGS="" + while [[ $# -gt 0 ]]; do + case "$1" in + --private-key|--account) + AUTH_ARGS="$1 $2" + shift 2 + ;; + --ledger|--interactive) + AUTH_ARGS="$1" + shift + ;; + *) + shift + ;; + esac + done +} + +# Build auth args for deploy step (from --deploy-* flags) +get_deploy_auth() { + if [[ -n "${DEPLOY_KEY:-}" ]]; then + echo "--private-key $DEPLOY_KEY" + elif [[ -n "${DEPLOY_ACCOUNT:-}" ]]; then + echo "--account $DEPLOY_ACCOUNT" + elif [[ "${DEPLOY_LEDGER:-}" == "true" ]]; then + echo "--ledger" + elif [[ "${DEPLOY_INTERACTIVE:-}" == "true" ]]; then + echo "--interactive" + fi +} + +# Build auth args for execute step (from --execute-* flags) +get_execute_auth() { + if [[ -n "${EXECUTE_KEY:-}" ]]; then + echo "--private-key $EXECUTE_KEY" + elif [[ -n "${EXECUTE_ACCOUNT:-}" ]]; then + echo "--account $EXECUTE_ACCOUNT" + elif [[ "${EXECUTE_LEDGER:-}" == "true" ]]; then + echo "--ledger" + elif [[ "${EXECUTE_INTERACTIVE:-}" == "true" ]]; then + echo "--interactive" + fi +} + +# Parse --deploy-* and --execute-* flags into variables +parse_deploy_execute_auth() { + DEPLOY_KEY="" + DEPLOY_ACCOUNT="" + DEPLOY_LEDGER=false + DEPLOY_INTERACTIVE=false + EXECUTE_KEY="" + EXECUTE_ACCOUNT="" + EXECUTE_LEDGER=false + EXECUTE_INTERACTIVE=false + DRY_RUN=false + SKIP_EXECUTE=false + VERIFY_CONTRACTS=false + REMAINING_ARGS=() + + while [[ $# -gt 0 ]]; do + case "$1" in + --deploy-key) DEPLOY_KEY="$2"; shift 2 ;; + --deploy-account) DEPLOY_ACCOUNT="$2"; shift 2 ;; + --deploy-ledger) DEPLOY_LEDGER=true; shift ;; + --deploy-interactive) DEPLOY_INTERACTIVE=true; shift ;; + --execute-key) EXECUTE_KEY="$2"; shift 2 ;; + --execute-account) EXECUTE_ACCOUNT="$2"; shift 2 ;; + --execute-ledger) EXECUTE_LEDGER=true; shift ;; + --execute-interactive) EXECUTE_INTERACTIVE=true; shift ;; + --dry-run|-n) DRY_RUN=true; shift ;; + --skip-execute) SKIP_EXECUTE=true; shift ;; + --verify|-v) VERIFY_CONTRACTS=true; shift ;; + *) REMAINING_ARGS+=("$1"); shift ;; + esac + done +} + +# ============================================================================= +# Forge Script Helpers +# ============================================================================= + +get_chain_id() { + local rpc="$1" + cast chain-id --rpc-url "$rpc" +} + +parse_action_address() { + local script_path="$1" + local chain_id="$2" + local script_name=$(basename "$script_path") + local broadcast_file="/app/broadcast/${script_name}/${chain_id}/run-latest.json" + + if [[ ! -f "$broadcast_file" ]]; then + die "Broadcast file not found: $broadcast_file" + fi + + local address=$(jq -r '.transactions | map(select(.transactionType == "CREATE")) | last | .contractAddress' "$broadcast_file") + + if [[ -z "$address" || "$address" == "null" ]]; then + die "Could not parse action address from broadcast file" + fi + + echo "$address" +} diff --git a/test/docker/test-docker.bash b/test/docker/test-docker.bash index 25f54431..03167239 100755 --- a/test/docker/test-docker.bash +++ b/test/docker/test-docker.bash @@ -25,8 +25,8 @@ run_test() { fi } -# Test 1: Tools are installed -echo "--- Tool Availability ---" +# Test 1: Tools are installed (passthrough) +echo "--- Tool Passthrough ---" run_test "forge" docker run --rm "$IMAGE_NAME" forge --version run_test "cast" docker run --rm "$IMAGE_NAME" cast --version run_test "yarn" docker run --rm "$IMAGE_NAME" yarn --version @@ -43,41 +43,85 @@ echo "" echo "--- Contract Compilation ---" run_test "contracts built" docker run --rm "$IMAGE_NAME" test -d out -# Test 4: Scripts are accessible +# Test 4: Browsing - list directories echo "" -echo "--- Script Accessibility ---" - -DEPLOY_SCRIPTS=( - "scripts/foundry/contract-upgrades/1.2.1/DeployNitroContracts1Point2Point1UpgradeAction.s.sol" - "scripts/foundry/contract-upgrades/2.1.0/DeployNitroContracts2Point1Point0UpgradeAction.s.sol" - "scripts/foundry/contract-upgrades/2.1.2/DeployNitroContracts2Point1Point2UpgradeAction.s.sol" - "scripts/foundry/contract-upgrades/2.1.3/DeployNitroContracts2Point1Point3UpgradeAction.s.sol" - "scripts/foundry/arbos-upgrades/at-timestamp/DeployUpgradeArbOSVersionAtTimestampAction.s.sol" -) - -for script in "${DEPLOY_SCRIPTS[@]}"; do - script_name=$(basename "$script") - run_test "$script_name exists" docker run --rm "$IMAGE_NAME" test -f "$script" -done - -EXECUTE_SCRIPTS=( - "scripts/foundry/contract-upgrades/1.2.1/ExecuteNitroContracts1Point2Point1Upgrade.s.sol" - "scripts/foundry/contract-upgrades/2.1.0/ExecuteNitroContracts2Point1Point0Upgrade.s.sol" - "scripts/foundry/contract-upgrades/2.1.2/ExecuteNitroContracts2Point1Point2Upgrade.s.sol" - "scripts/foundry/contract-upgrades/2.1.3/ExecuteNitroContracts2Point1Point3Upgrade.s.sol" -) - -for script in "${EXECUTE_SCRIPTS[@]}"; do - script_name=$(basename "$script") - run_test "$script_name exists" docker run --rm "$IMAGE_NAME" test -f "$script" -done - -# Test 5: Yarn scripts work +echo "--- Directory Browsing ---" + +# List top level +echo -n "Testing list top level... " +TOP_OUTPUT=$(docker run --rm "$IMAGE_NAME" 2>&1) +if echo "$TOP_OUTPUT" | grep "contract-upgrades" > /dev/null; then + echo "OK" +else + echo "FAILED" + FAILURES=$((FAILURES + 1)) +fi + +# List contract-upgrades versions +echo -n "Testing list contract-upgrades... " +VERSIONS_OUTPUT=$(docker run --rm "$IMAGE_NAME" contract-upgrades 2>&1) +if echo "$VERSIONS_OUTPUT" | grep "1.2.1" > /dev/null && echo "$VERSIONS_OUTPUT" | grep "2.1.0" > /dev/null; then + echo "OK" +else + echo "FAILED" + FAILURES=$((FAILURES + 1)) +fi + +# List version contents (should show virtual commands) +echo -n "Testing list contract-upgrades/1.2.1... " +CONTENTS_OUTPUT=$(docker run --rm "$IMAGE_NAME" contract-upgrades/1.2.1 2>&1) +if echo "$CONTENTS_OUTPUT" | grep "deploy-execute-verify" > /dev/null; then + echo "OK" +else + echo "FAILED" + FAILURES=$((FAILURES + 1)) +fi + +# Test 5: File viewing +echo "" +echo "--- File Viewing ---" + +# View README +echo -n "Testing view README... " +README_OUTPUT=$(docker run --rm "$IMAGE_NAME" contract-upgrades/1.2.1/README.md 2>&1) +if echo "$README_OUTPUT" | grep -i "nitro" > /dev/null; then + echo "OK" +else + echo "FAILED" + FAILURES=$((FAILURES + 1)) +fi + +# View env template (1.2.1 has env-templates/) +echo -n "Testing view env template... " +ENV_OUTPUT=$(docker run --rm "$IMAGE_NAME" contract-upgrades/1.2.1/env-templates/.env.local-upgrade.example 2>&1) +if echo "$ENV_OUTPUT" | grep "INBOX_ADDRESS" > /dev/null; then + echo "OK" +else + echo "FAILED" + FAILURES=$((FAILURES + 1)) +fi + +# View .env.sample (2.1.0+ has .env.sample) +echo -n "Testing view .env.sample... " +SAMPLE_OUTPUT=$(docker run --rm "$IMAGE_NAME" contract-upgrades/2.1.0/.env.sample 2>&1) +if echo "$SAMPLE_OUTPUT" | grep "UPGRADE_ACTION_ADDRESS" > /dev/null; then + echo "OK" +else + echo "FAILED" + FAILURES=$((FAILURES + 1)) +fi + +# Test 6: Help +echo "" +echo "--- Help ---" +run_test "help command" docker run --rm "$IMAGE_NAME" help + +# Test 7: Yarn scripts work echo "" echo "--- Yarn Scripts ---" run_test "yarn orbit:contracts:version --help" docker run --rm "$IMAGE_NAME" yarn orbit:contracts:version --help -# Test 6: Unit tests pass +# Test 8: Unit tests pass echo "" echo "--- Unit Tests ---" echo "Running unit tests inside container..." @@ -88,6 +132,30 @@ else FAILURES=$((FAILURES + 1)) fi +# Test 9: Dry run tests (requires .env file) +echo "" +echo "--- Dry Run Tests ---" + +# Create a temporary .env file for testing arbos +TEMP_ENV=$(mktemp) +cat > "$TEMP_ENV" <&1) +if echo "$DRYRUN_OUTPUT" | grep "Calldata:" > /dev/null; then + echo "OK" +else + echo "FAILED" + FAILURES=$((FAILURES + 1)) +fi + +rm -f "$TEMP_ENV" + # Summary echo "" echo "=== Summary ===" From 15b87cfc5a37167eca3b0d09cfb8181868df8969 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Mon, 9 Feb 2026 11:06:01 +0000 Subject: [PATCH 08/44] feat: add Verify scripts for contract upgrades Add Verify*.s.sol Forge scripts to each contract-upgrade version folder, replacing hardcoded verification logic with discoverable scripts. - Add verify command to bin/contract-upgrade and bin/router - Create Verify scripts for 1.2.1, 2.1.0, 2.1.2, 2.1.3 - Update READMEs to reference forge script verification --- bin/contract-upgrade | 48 +++++++++++-------- bin/router | 3 +- .../foundry/contract-upgrades/1.2.1/README.md | 4 +- ...fyNitroContracts1Point2Point1Upgrade.s.sol | 21 ++++++++ .../foundry/contract-upgrades/2.1.0/README.md | 4 +- ...fyNitroContracts2Point1Point0Upgrade.s.sol | 21 ++++++++ .../foundry/contract-upgrades/2.1.2/README.md | 5 +- ...fyNitroContracts2Point1Point2Upgrade.s.sol | 25 ++++++++++ .../foundry/contract-upgrades/2.1.3/README.md | 5 +- ...fyNitroContracts2Point1Point3Upgrade.s.sol | 25 ++++++++++ 10 files changed, 129 insertions(+), 32 deletions(-) create mode 100644 scripts/foundry/contract-upgrades/1.2.1/VerifyNitroContracts1Point2Point1Upgrade.s.sol create mode 100644 scripts/foundry/contract-upgrades/2.1.0/VerifyNitroContracts2Point1Point0Upgrade.s.sol create mode 100644 scripts/foundry/contract-upgrades/2.1.2/VerifyNitroContracts2Point1Point2Upgrade.s.sol create mode 100644 scripts/foundry/contract-upgrades/2.1.3/VerifyNitroContracts2Point1Point3Upgrade.s.sol diff --git a/bin/contract-upgrade b/bin/contract-upgrade index f2344552..b7f9b305 100644 --- a/bin/contract-upgrade +++ b/bin/contract-upgrade @@ -62,6 +62,22 @@ cmd_execute() { eval $cmd } +cmd_verify() { + local version_dir="$1" + shift + + require_env PARENT_CHAIN_RPC + + local verify_script=$(find "$version_dir" -maxdepth 1 -name "Verify*.s.sol" -type f 2>/dev/null | head -1) + if [[ -z "$verify_script" ]]; then + die "No verify script found in $version_dir - check README for manual verification" + fi + + log "Running: $(basename "$verify_script")" + + forge script "$verify_script" --rpc-url "$PARENT_CHAIN_RPC" -vvv +} + cmd_deploy_execute_verify() { local version_dir="$1" local version=$(basename "$version_dir") @@ -168,26 +184,12 @@ cmd_deploy_execute_verify() { if [[ "$DRY_RUN" != "true" && "$SKIP_EXECUTE" != "true" ]]; then log "Step 3: Verifying upgrade..." - case "$version" in - 2.1.2|2.1.3) - local bridge - bridge=$(cast call --rpc-url "$PARENT_CHAIN_RPC" "$INBOX_ADDRESS" "bridge()(address)" 2>/dev/null || echo "") - if [[ -n "$bridge" && "$bridge" != "0x" ]]; then - local decimals - decimals=$(cast call --rpc-url "$PARENT_CHAIN_RPC" "$bridge" "nativeTokenDecimals()(uint8)" 2>/dev/null || echo "N/A") - log "Verification: nativeTokenDecimals = $decimals" - fi - ;; - 1.2.1|2.1.0) - if [[ -n "${ROLLUP:-}" ]]; then - local wasm_root - wasm_root=$(cast call --rpc-url "$PARENT_CHAIN_RPC" "$ROLLUP" "wasmModuleRoot()(bytes32)" 2>/dev/null || echo "N/A") - log "Verification: wasmModuleRoot = $wasm_root" - else - log "Verification: Set ROLLUP in .env to check wasmModuleRoot" - fi - ;; - esac + local verify_script=$(find "$version_dir" -maxdepth 1 -name "Verify*.s.sol" -type f 2>/dev/null | head -1) + if [[ -n "$verify_script" ]]; then + forge script "$verify_script" --rpc-url "$PARENT_CHAIN_RPC" -vvv + else + log "No Verify script found - check README for manual verification" + fi fi log "Done" @@ -204,6 +206,7 @@ Usage: contract-upgrade [options] Commands: deploy Run deploy script only execute Run execute script only + verify Run verify script only deploy-execute-verify Full upgrade flow (deploy, execute, verify) Options for deploy/execute: @@ -264,6 +267,9 @@ Available versions: $(ls "$CONTRACTS_DIR" 2>/dev/null | tr '\n' ' ' || echo "non execute) cmd_execute "$version_dir" "$@" ;; + verify) + cmd_verify "$version_dir" "$@" + ;; deploy-execute-verify) cmd_deploy_execute_verify "$version_dir" "$@" ;; @@ -273,7 +279,7 @@ Available versions: $(ls "$CONTRACTS_DIR" 2>/dev/null | tr '\n' ' ' || echo "non *) die "Unknown command: $command -Commands: deploy, execute, deploy-execute-verify" +Commands: deploy, execute, verify, deploy-execute-verify" ;; esac } diff --git a/bin/router b/bin/router index 609873a8..8b847226 100644 --- a/bin/router +++ b/bin/router @@ -36,6 +36,7 @@ list_directory() { echo "---" echo "deploy (run Deploy script)" echo "execute (run Execute script)" + echo "verify (run Verify script)" echo "deploy-execute-verify (full upgrade flow)" ;; arbos-upgrades/at-timestamp) @@ -140,7 +141,7 @@ main() { contract-upgrades/[0-9]*) local version=$(basename "$rel_parent") case "$basename" in - deploy|execute|deploy-execute-verify) + deploy|execute|verify|deploy-execute-verify) exec /app/bin/contract-upgrade "$version" "$basename" "$@" ;; esac diff --git a/scripts/foundry/contract-upgrades/1.2.1/README.md b/scripts/foundry/contract-upgrades/1.2.1/README.md index 4c9b75c2..78d7a63b 100644 --- a/scripts/foundry/contract-upgrades/1.2.1/README.md +++ b/scripts/foundry/contract-upgrades/1.2.1/README.md @@ -57,7 +57,7 @@ forge script --sender $EXECUTOR --rpc-url $PARENT_CHAIN_RPC --broadcast ./Execut ``` If you have a multisig as executor, you can still run the above command without broadcasting to get the payload for the multisig transaction. -4. That's it, upgrade has been performed. You can make sure it has successfully executed by checking wasm module root: +4. That's it, upgrade has been performed. You can verify by running: ```bash -cast call --rpc-url $PARENT_CHAIN_RPC $ROLLUP "wasmModuleRoot()" +forge script --rpc-url $PARENT_CHAIN_RPC VerifyNitroContracts1Point2Point1Upgrade -vvv ``` diff --git a/scripts/foundry/contract-upgrades/1.2.1/VerifyNitroContracts1Point2Point1Upgrade.s.sol b/scripts/foundry/contract-upgrades/1.2.1/VerifyNitroContracts1Point2Point1Upgrade.s.sol new file mode 100644 index 00000000..00457948 --- /dev/null +++ b/scripts/foundry/contract-upgrades/1.2.1/VerifyNitroContracts1Point2Point1Upgrade.s.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import "forge-std/Script.sol"; + +interface IRollupCore { + function wasmModuleRoot() external view returns (bytes32); +} + +/** + * @title VerifyNitroContracts1Point2Point1Upgrade + * @notice Verifies the upgrade to Nitro Contracts 1.2.1 by checking the wasmModuleRoot + */ +contract VerifyNitroContracts1Point2Point1Upgrade is Script { + function run() public view { + address rollup = vm.envAddress("ROLLUP"); + bytes32 wasmRoot = IRollupCore(rollup).wasmModuleRoot(); + console.log("wasmModuleRoot:"); + console.logBytes32(wasmRoot); + } +} diff --git a/scripts/foundry/contract-upgrades/2.1.0/README.md b/scripts/foundry/contract-upgrades/2.1.0/README.md index 99c055f5..1762eabe 100644 --- a/scripts/foundry/contract-upgrades/2.1.0/README.md +++ b/scripts/foundry/contract-upgrades/2.1.0/README.md @@ -68,10 +68,10 @@ forge script --sender $EXECUTOR --rpc-url $PARENT_CHAIN_RPC --broadcast ExecuteN If you have a multisig as executor, you can still run the above command without broadcasting to get the payload for the multisig transaction. -4. That's it, upgrade has been performed. You can make sure it has successfully executed by checking wasm module root: +4. That's it, upgrade has been performed. You can verify by running: ```bash -cast call --rpc-url $PARENT_CHAIN_RPC $ROLLUP "wasmModuleRoot()" +forge script --rpc-url $PARENT_CHAIN_RPC VerifyNitroContracts2Point1Point0Upgrade -vvv ``` ## FAQ diff --git a/scripts/foundry/contract-upgrades/2.1.0/VerifyNitroContracts2Point1Point0Upgrade.s.sol b/scripts/foundry/contract-upgrades/2.1.0/VerifyNitroContracts2Point1Point0Upgrade.s.sol new file mode 100644 index 00000000..4f2a8a63 --- /dev/null +++ b/scripts/foundry/contract-upgrades/2.1.0/VerifyNitroContracts2Point1Point0Upgrade.s.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import "forge-std/Script.sol"; + +interface IRollupCore { + function wasmModuleRoot() external view returns (bytes32); +} + +/** + * @title VerifyNitroContracts2Point1Point0Upgrade + * @notice Verifies the upgrade to Nitro Contracts 2.1.0 by checking the wasmModuleRoot + */ +contract VerifyNitroContracts2Point1Point0Upgrade is Script { + function run() public view { + address rollup = vm.envAddress("ROLLUP"); + bytes32 wasmRoot = IRollupCore(rollup).wasmModuleRoot(); + console.log("wasmModuleRoot:"); + console.logBytes32(wasmRoot); + } +} diff --git a/scripts/foundry/contract-upgrades/2.1.2/README.md b/scripts/foundry/contract-upgrades/2.1.2/README.md index 16b04a6c..9d16797f 100644 --- a/scripts/foundry/contract-upgrades/2.1.2/README.md +++ b/scripts/foundry/contract-upgrades/2.1.2/README.md @@ -73,11 +73,10 @@ forge script --sender $EXECUTOR --rpc-url $PARENT_CHAIN_RPC --broadcast ExecuteN If you have a multisig as executor, you can still run the above command without broadcasting to get the payload for the multisig transaction. -4. That's it, upgrade has been performed. You can make sure it has successfully executed by checking the native token decimals. +4. That's it, upgrade has been performed. You can verify by running: ```bash -# should return 18 -cast call --rpc-url $PARENT_CHAIN_RPC $BRIDGE "nativeTokenDecimals()(uint8)" +forge script --rpc-url $PARENT_CHAIN_RPC VerifyNitroContracts2Point1Point2Upgrade -vvv ``` ## FAQ diff --git a/scripts/foundry/contract-upgrades/2.1.2/VerifyNitroContracts2Point1Point2Upgrade.s.sol b/scripts/foundry/contract-upgrades/2.1.2/VerifyNitroContracts2Point1Point2Upgrade.s.sol new file mode 100644 index 00000000..4e7b1691 --- /dev/null +++ b/scripts/foundry/contract-upgrades/2.1.2/VerifyNitroContracts2Point1Point2Upgrade.s.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import "forge-std/Script.sol"; + +interface IInbox { + function bridge() external view returns (address); +} + +interface IBridge { + function nativeTokenDecimals() external view returns (uint8); +} + +/** + * @title VerifyNitroContracts2Point1Point2Upgrade + * @notice Verifies the upgrade to Nitro Contracts 2.1.2 by checking nativeTokenDecimals + */ +contract VerifyNitroContracts2Point1Point2Upgrade is Script { + function run() public view { + address inbox = vm.envAddress("INBOX_ADDRESS"); + address bridge = IInbox(inbox).bridge(); + uint8 decimals = IBridge(bridge).nativeTokenDecimals(); + console.log("nativeTokenDecimals:", decimals); + } +} diff --git a/scripts/foundry/contract-upgrades/2.1.3/README.md b/scripts/foundry/contract-upgrades/2.1.3/README.md index af8e4087..adff9181 100644 --- a/scripts/foundry/contract-upgrades/2.1.3/README.md +++ b/scripts/foundry/contract-upgrades/2.1.3/README.md @@ -72,11 +72,10 @@ forge script --sender $EXECUTOR --rpc-url $PARENT_CHAIN_RPC --broadcast ExecuteN If you have a multisig as executor, you can still run the above command without broadcasting to get the payload for the multisig transaction. -4. That's it, upgrade has been performed. You can make sure it has successfully executed by checking the native token decimals. +4. That's it, upgrade has been performed. You can verify by running: ```bash -# should return 18 -cast call --rpc-url $PARENT_CHAIN_RPC $BRIDGE "nativeTokenDecimals()(uint8)" +forge script --rpc-url $PARENT_CHAIN_RPC VerifyNitroContracts2Point1Point3Upgrade -vvv ``` ## FAQ diff --git a/scripts/foundry/contract-upgrades/2.1.3/VerifyNitroContracts2Point1Point3Upgrade.s.sol b/scripts/foundry/contract-upgrades/2.1.3/VerifyNitroContracts2Point1Point3Upgrade.s.sol new file mode 100644 index 00000000..353130df --- /dev/null +++ b/scripts/foundry/contract-upgrades/2.1.3/VerifyNitroContracts2Point1Point3Upgrade.s.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import "forge-std/Script.sol"; + +interface IInbox { + function bridge() external view returns (address); +} + +interface IBridge { + function nativeTokenDecimals() external view returns (uint8); +} + +/** + * @title VerifyNitroContracts2Point1Point3Upgrade + * @notice Verifies the upgrade to Nitro Contracts 2.1.3 by checking nativeTokenDecimals + */ +contract VerifyNitroContracts2Point1Point3Upgrade is Script { + function run() public view { + address inbox = vm.envAddress("INBOX_ADDRESS"); + address bridge = IInbox(inbox).bridge(); + uint8 decimals = IBridge(bridge).nativeTokenDecimals(); + console.log("nativeTokenDecimals:", decimals); + } +} From a8adfeb8e243ada18fefbf46eb3455a32f9951c0 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Mon, 9 Feb 2026 11:23:10 +0000 Subject: [PATCH 09/44] ci: add Docker Hub publishing workflow Publish offchainlabs/chain-actions image to Docker Hub: - On push to main: tag as latest - On release tags (v*): tag as version (e.g., 1.2.3, 1.2) - Manual trigger: tag with branch name (for testing) Requires DOCKERHUB_USERNAME and DOCKERHUB_TOKEN secrets. --- .github/workflows/publish-docker.yml | 53 ++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 .github/workflows/publish-docker.yml diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml new file mode 100644 index 00000000..d5e2d674 --- /dev/null +++ b/.github/workflows/publish-docker.yml @@ -0,0 +1,53 @@ +name: Publish Docker + +on: + push: + branches: [main] + tags: ['v*'] + workflow_dispatch: + +jobs: + publish: + name: Build and Push Docker Image + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: stable + + - name: Install forge dependencies + run: forge install + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Extract metadata for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: offchainlabs/chain-actions + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=ref,event=branch + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max From 4d391f93351512bde9043ec6125461a3797c4ed8 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Mon, 9 Feb 2026 13:04:34 +0000 Subject: [PATCH 10/44] refactor: rewrite CLI in TypeScript Replace bash CLI (bin/router, bin/contract-upgrade, bin/arbos-upgrade, lib/common.sh) with TypeScript implementation in src/cli/. - Add commander for argument parsing - Add execa for subprocess execution - Update Dockerfile to use node entrypoint directly - Remove entrypoint.sh (no longer needed) --- .gitignore | 3 + Dockerfile | 8 +- bin/arbos-upgrade | 320 --------------------------- bin/contract-upgrade | 287 ------------------------ bin/router | 189 ---------------- entrypoint.sh | 4 - lib/common.sh | 135 ----------- package.json | 9 + src/cli/commands/arbos-upgrade.ts | 303 +++++++++++++++++++++++++ src/cli/commands/contract-upgrade.ts | 269 ++++++++++++++++++++++ src/cli/index.ts | 29 +++ src/cli/router.ts | 227 +++++++++++++++++++ src/cli/utils/auth.ts | 110 +++++++++ src/cli/utils/env.ts | 84 +++++++ src/cli/utils/forge.ts | 167 ++++++++++++++ src/cli/utils/log.ts | 20 ++ tsconfig.cli.json | 9 + yarn.lock | 106 ++++++++- 18 files changed, 1337 insertions(+), 942 deletions(-) delete mode 100644 bin/arbos-upgrade delete mode 100644 bin/contract-upgrade delete mode 100644 bin/router delete mode 100755 entrypoint.sh delete mode 100644 lib/common.sh create mode 100644 src/cli/commands/arbos-upgrade.ts create mode 100644 src/cli/commands/contract-upgrade.ts create mode 100644 src/cli/index.ts create mode 100644 src/cli/router.ts create mode 100644 src/cli/utils/auth.ts create mode 100644 src/cli/utils/env.ts create mode 100644 src/cli/utils/forge.ts create mode 100644 src/cli/utils/log.ts create mode 100644 tsconfig.cli.json diff --git a/.gitignore b/.gitignore index 973fff40..e00e0e6f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ node_modules .env .DS_Store +# TypeScript build output +/dist + # Hardhat files /cache /artifacts diff --git a/Dockerfile b/Dockerfile index 9d306176..b8fe17f2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,8 +30,8 @@ COPY . . # Build contracts RUN forge build -# Make scripts executable -RUN chmod +x /app/entrypoint.sh /app/bin/* /app/lib/* +# Build CLI +RUN yarn build:cli -# Set entrypoint for command routing -ENTRYPOINT ["/app/entrypoint.sh"] +# Direct node entrypoint (no shell wrapper) +ENTRYPOINT ["node", "/app/dist/cli/index.js"] diff --git a/bin/arbos-upgrade b/bin/arbos-upgrade deleted file mode 100644 index 3cccd9a5..00000000 --- a/bin/arbos-upgrade +++ /dev/null @@ -1,320 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# ArbOS upgrades script for orbit-actions -# Handles deploy, execute, verify, and deploy-execute-verify commands - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$SCRIPT_DIR/../lib/common.sh" - -ARBOS_DIR="/app/scripts/foundry/arbos-upgrades/at-timestamp" - -# ============================================================================= -# Commands -# ============================================================================= - -cmd_deploy() { - local version="$1" - shift - - parse_auth_args "$@" - - require_env CHILD_CHAIN_RPC - - export ARBOS_VERSION="$version" - - local deploy_script="$ARBOS_DIR/DeployUpgradeArbOSVersionAtTimestampAction.s.sol" - if [[ ! -f "$deploy_script" ]]; then - die "Deploy script not found: $deploy_script" - fi - - log "Running: $(basename "$deploy_script") for ArbOS $version" - - local cmd="forge script $deploy_script --rpc-url $CHILD_CHAIN_RPC --slow -vvv" - - if [[ -n "$AUTH_ARGS" ]]; then - cmd="$cmd --broadcast $AUTH_ARGS" - fi - - eval $cmd -} - -cmd_execute() { - shift # version not needed for execute - - parse_auth_args "$@" - - require_env CHILD_CHAIN_RPC - require_env CHILD_UPGRADE_EXECUTOR_ADDRESS - require_env UPGRADE_ACTION_ADDRESS - - log "Executing ArbOS upgrade action: $UPGRADE_ACTION_ADDRESS" - - local perform_calldata="0xb0a75d36" - - if [[ -z "$AUTH_ARGS" ]]; then - # No auth - output calldata for multisig - local execute_calldata - execute_calldata=$(cast calldata "execute(address,bytes)" "$UPGRADE_ACTION_ADDRESS" "$perform_calldata") - - log "Calldata for UpgradeExecutor.execute():" - echo "" - echo "To: $CHILD_UPGRADE_EXECUTOR_ADDRESS" - echo "Calldata: $execute_calldata" - echo "" - log "Submit this to your multisig/Safe to execute the upgrade" - else - local cast_cmd="cast send $CHILD_UPGRADE_EXECUTOR_ADDRESS \"execute(address,bytes)\" $UPGRADE_ACTION_ADDRESS $perform_calldata --rpc-url $CHILD_CHAIN_RPC $AUTH_ARGS" - - eval $cast_cmd - - log "ArbOS upgrade scheduled successfully" - fi -} - -cmd_verify() { - shift # version not needed for verify - - require_env CHILD_CHAIN_RPC - - log "Checking ArbOS upgrade status..." - - local scheduled - scheduled=$(cast call --rpc-url "$CHILD_CHAIN_RPC" \ - "0x000000000000000000000000000000000000006b" \ - "getScheduledUpgrade()(uint64,uint64)" 2>/dev/null || echo "N/A") - log "Scheduled upgrade (version, timestamp): $scheduled" - - local current_raw - current_raw=$(cast call --rpc-url "$CHILD_CHAIN_RPC" \ - "0x0000000000000000000000000000000000000064" \ - "arbOSVersion()(uint64)" 2>/dev/null || echo "0") - local current_version=$((current_raw - 55)) - log "Current ArbOS version: $current_version" -} - -cmd_deploy_execute_verify() { - local version="$1" - shift - - parse_deploy_execute_auth "$@" - - log "ArbOS version: $version" - - local deploy_script="$ARBOS_DIR/DeployUpgradeArbOSVersionAtTimestampAction.s.sol" - if [[ ! -f "$deploy_script" ]]; then - die "Deploy script not found: $deploy_script" - fi - - # Auto-detect skip_deploy if UPGRADE_ACTION_ADDRESS is set - local skip_deploy=false - if [[ -n "${UPGRADE_ACTION_ADDRESS:-}" ]]; then - skip_deploy=true - log "Using existing action from .env: $UPGRADE_ACTION_ADDRESS" - fi - - # Validate required env vars - require_env CHILD_CHAIN_RPC - require_env CHILD_UPGRADE_EXECUTOR_ADDRESS - require_env SCHEDULE_TIMESTAMP - - export ARBOS_VERSION="$version" - - # Validate auth - local deploy_auth=$(get_deploy_auth) - local execute_auth=$(get_execute_auth) - - if [[ "$skip_deploy" != "true" && "$DRY_RUN" != "true" && -z "$deploy_auth" ]]; then - die "Deploy auth required. Use --deploy-key, --deploy-account, --deploy-ledger, or --deploy-interactive" - fi - if [[ "$SKIP_EXECUTE" != "true" && "$DRY_RUN" != "true" && -z "$execute_auth" ]]; then - die "Execute auth required. Use --execute-key, --execute-account, --execute-ledger, or --execute-interactive" - fi - - log "Scheduled timestamp: $SCHEDULE_TIMESTAMP" - - # Step 1: Deploy - local chain_id="" - if [[ "$skip_deploy" != "true" ]]; then - chain_id=$(get_chain_id "$CHILD_CHAIN_RPC") - log "Target chain ID: $chain_id" - log "Step 1: Deploying ArbOS upgrade action..." - - local forge_cmd="forge script $deploy_script --rpc-url $CHILD_CHAIN_RPC --slow -vvv" - - if [[ "$DRY_RUN" != "true" ]]; then - forge_cmd="$forge_cmd --broadcast $deploy_auth" - fi - if [[ "$VERIFY_CONTRACTS" == "true" ]]; then - forge_cmd="$forge_cmd --verify" - fi - - eval $forge_cmd - - if [[ "$DRY_RUN" != "true" ]]; then - UPGRADE_ACTION_ADDRESS=$(parse_action_address "$deploy_script" "$chain_id") - log "Deployed action at: $UPGRADE_ACTION_ADDRESS" - else - log "Dry run - no action deployed" - if [[ "$SKIP_EXECUTE" != "true" ]]; then - log "Note: Set UPGRADE_ACTION_ADDRESS in .env to run execute step" - return 0 - fi - fi - else - log "Step 1: Skipped deploy" - fi - - # Step 2: Execute via cast send - if [[ "$SKIP_EXECUTE" != "true" ]]; then - log "Step 2: Executing ArbOS upgrade..." - - local perform_calldata="0xb0a75d36" - - if [[ "$DRY_RUN" == "true" ]]; then - local execute_calldata - execute_calldata=$(cast calldata "execute(address,bytes)" "$UPGRADE_ACTION_ADDRESS" "$perform_calldata") - - log "Dry run - calldata for UpgradeExecutor.execute():" - echo "" - echo "To: $CHILD_UPGRADE_EXECUTOR_ADDRESS" - echo "Calldata: $execute_calldata" - echo "" - log "Submit this to your multisig/Safe to execute the upgrade" - else - local cast_cmd="cast send $CHILD_UPGRADE_EXECUTOR_ADDRESS \"execute(address,bytes)\" $UPGRADE_ACTION_ADDRESS $perform_calldata --rpc-url $CHILD_CHAIN_RPC" - - if [[ -n "$EXECUTE_KEY" ]]; then - cast_cmd="$cast_cmd --private-key $EXECUTE_KEY" - elif [[ -n "$EXECUTE_ACCOUNT" ]]; then - cast_cmd="$cast_cmd --account $EXECUTE_ACCOUNT" - elif [[ "$EXECUTE_LEDGER" == "true" ]]; then - cast_cmd="$cast_cmd --ledger" - elif [[ "$EXECUTE_INTERACTIVE" == "true" ]]; then - cast_cmd="$cast_cmd --interactive" - fi - - eval $cast_cmd - - log "ArbOS upgrade scheduled successfully" - fi - else - log "Step 2: Skipped execute" - fi - - # Step 3: Verify - if [[ "$DRY_RUN" != "true" && "$SKIP_EXECUTE" != "true" ]]; then - log "Step 3: Verifying scheduled upgrade..." - - local scheduled - scheduled=$(cast call --rpc-url "$CHILD_CHAIN_RPC" \ - "0x000000000000000000000000000000000000006b" \ - "getScheduledUpgrade()(uint64,uint64)" 2>/dev/null || echo "N/A") - log "Scheduled upgrade (version, timestamp): $scheduled" - - local current_raw - current_raw=$(cast call --rpc-url "$CHILD_CHAIN_RPC" \ - "0x0000000000000000000000000000000000000064" \ - "arbOSVersion()(uint64)" 2>/dev/null || echo "0") - local current_version=$((current_raw - 55)) - log "Current ArbOS version: $current_version" - fi - - log "Done" -} - -# ============================================================================= -# Main -# ============================================================================= - -show_help() { - cat <<'EOF' -Usage: arbos-upgrade [options] - -Commands: - deploy Run deploy script only - execute Execute upgrade action (schedule the upgrade) - verify Check scheduled upgrade status - deploy-execute-verify Full upgrade flow (deploy, execute, verify) - -Options for deploy: - --private-key KEY Private key - --account NAME Keystore account - --ledger Use Ledger - --interactive Prompt for key - -Options for deploy-execute-verify: - --deploy-key KEY Private key for deploy step - --deploy-account NAME Keystore account for deploy - --deploy-ledger Use Ledger for deploy - --execute-key KEY Private key for execute step - --execute-account NAME Keystore account for execute - --execute-ledger Use Ledger for execute - --dry-run, -n Simulate without broadcasting - --skip-execute Deploy only - --verify, -v Verify on block explorer - -Required .env variables: - CHILD_CHAIN_RPC Child chain RPC URL - CHILD_UPGRADE_EXECUTOR_ADDRESS UpgradeExecutor address - SCHEDULE_TIMESTAMP Unix timestamp for upgrade - -Optional .env variables: - ARBOS_VERSION ArbOS version (alternative to positional arg) - UPGRADE_ACTION_ADDRESS Pre-deployed action (skips deploy) -EOF -} - -main() { - if [[ $# -lt 1 ]]; then - show_help - exit 1 - fi - - local version="$1" - shift - - if [[ "$version" == "--help" || "$version" == "-h" ]]; then - show_help - exit 0 - fi - - # Version can also come from env - if [[ -z "$version" && -n "${ARBOS_VERSION:-}" ]]; then - version="$ARBOS_VERSION" - fi - - if [[ -z "$version" ]]; then - die "ArbOS version required" - fi - - local command="${1:-deploy-execute-verify}" - if [[ $# -gt 0 ]]; then - shift - fi - - case "$command" in - deploy) - cmd_deploy "$version" "$@" - ;; - execute) - cmd_execute "$version" "$@" - ;; - verify) - cmd_verify "$version" "$@" - ;; - deploy-execute-verify) - cmd_deploy_execute_verify "$version" "$@" - ;; - --help|-h) - show_help - ;; - *) - die "Unknown command: $command - -Commands: deploy, execute, verify, deploy-execute-verify" - ;; - esac -} - -main "$@" diff --git a/bin/contract-upgrade b/bin/contract-upgrade deleted file mode 100644 index b7f9b305..00000000 --- a/bin/contract-upgrade +++ /dev/null @@ -1,287 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# Contract upgrades script for orbit-actions -# Handles deploy, execute, and deploy-execute-verify commands - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$SCRIPT_DIR/../lib/common.sh" - -CONTRACTS_DIR="/app/scripts/foundry/contract-upgrades" - -# ============================================================================= -# Commands -# ============================================================================= - -cmd_deploy() { - local version_dir="$1" - shift - - parse_auth_args "$@" - - require_env PARENT_CHAIN_RPC - - local deploy_script=$(find "$version_dir" -maxdepth 1 -name "Deploy*.s.sol" -type f 2>/dev/null | head -1) - if [[ -z "$deploy_script" ]]; then - die "No deploy script found in $version_dir" - fi - - log "Running: $(basename "$deploy_script")" - - local cmd="forge script $deploy_script --rpc-url $PARENT_CHAIN_RPC --slow -vvv --skip-simulation" - - if [[ -n "$AUTH_ARGS" ]]; then - cmd="$cmd --broadcast $AUTH_ARGS" - fi - - eval $cmd -} - -cmd_execute() { - local version_dir="$1" - shift - - parse_auth_args "$@" - - require_env PARENT_CHAIN_RPC - require_env UPGRADE_ACTION_ADDRESS - - local execute_script=$(find "$version_dir" -maxdepth 1 -name "Execute*.s.sol" -type f 2>/dev/null | head -1) - if [[ -z "$execute_script" ]]; then - die "No execute script found in $version_dir" - fi - - log "Running: $(basename "$execute_script")" - - local cmd="forge script $execute_script --rpc-url $PARENT_CHAIN_RPC -vvv" - - if [[ -n "$AUTH_ARGS" ]]; then - cmd="$cmd --broadcast $AUTH_ARGS" - fi - - eval $cmd -} - -cmd_verify() { - local version_dir="$1" - shift - - require_env PARENT_CHAIN_RPC - - local verify_script=$(find "$version_dir" -maxdepth 1 -name "Verify*.s.sol" -type f 2>/dev/null | head -1) - if [[ -z "$verify_script" ]]; then - die "No verify script found in $version_dir - check README for manual verification" - fi - - log "Running: $(basename "$verify_script")" - - forge script "$verify_script" --rpc-url "$PARENT_CHAIN_RPC" -vvv -} - -cmd_deploy_execute_verify() { - local version_dir="$1" - local version=$(basename "$version_dir") - shift - - parse_deploy_execute_auth "$@" - - # Find scripts - local deploy_script=$(find "$version_dir" -maxdepth 1 -name "Deploy*.s.sol" -type f 2>/dev/null | head -1) - local execute_script=$(find "$version_dir" -maxdepth 1 -name "Execute*.s.sol" -type f 2>/dev/null | head -1) - - if [[ -z "$deploy_script" ]]; then - die "No deploy script found in $version_dir" - fi - if [[ -z "$execute_script" ]]; then - die "No execute script found in $version_dir" - fi - - log "Version: $version" - log "Deploy script: $(basename "$deploy_script")" - log "Execute script: $(basename "$execute_script")" - - # Auto-detect skip_deploy if UPGRADE_ACTION_ADDRESS is set - local skip_deploy=false - if [[ -n "${UPGRADE_ACTION_ADDRESS:-}" ]]; then - skip_deploy=true - log "Using existing action from .env: $UPGRADE_ACTION_ADDRESS" - fi - - # Validate required env vars - require_env PARENT_CHAIN_RPC - require_env INBOX_ADDRESS - require_env PROXY_ADMIN_ADDRESS - require_env PARENT_UPGRADE_EXECUTOR_ADDRESS - - # Validate auth - local deploy_auth=$(get_deploy_auth) - local execute_auth=$(get_execute_auth) - - if [[ "$skip_deploy" != "true" && "$DRY_RUN" != "true" && -z "$deploy_auth" ]]; then - die "Deploy auth required. Use --deploy-key, --deploy-account, --deploy-ledger, or --deploy-interactive" - fi - if [[ "$SKIP_EXECUTE" != "true" && "$DRY_RUN" != "true" && -z "$execute_auth" ]]; then - die "Execute auth required. Use --execute-key, --execute-account, --execute-ledger, or --execute-interactive" - fi - - local chain_id - chain_id=$(get_chain_id "$PARENT_CHAIN_RPC") - log "Target chain ID: $chain_id" - - # Step 1: Deploy - if [[ "$skip_deploy" != "true" ]]; then - log "Step 1: Deploying upgrade action..." - - local forge_cmd="forge script $deploy_script --rpc-url $PARENT_CHAIN_RPC --slow -vvv --skip-simulation" - - if [[ "$DRY_RUN" != "true" ]]; then - forge_cmd="$forge_cmd --broadcast $deploy_auth" - fi - if [[ "$VERIFY_CONTRACTS" == "true" ]]; then - forge_cmd="$forge_cmd --verify" - fi - - eval $forge_cmd - - if [[ "$DRY_RUN" != "true" ]]; then - UPGRADE_ACTION_ADDRESS=$(parse_action_address "$deploy_script" "$chain_id") - log "Deployed action at: $UPGRADE_ACTION_ADDRESS" - else - log "Dry run - no action deployed" - if [[ "$SKIP_EXECUTE" != "true" ]]; then - log "Note: Set UPGRADE_ACTION_ADDRESS in .env to run execute step" - return 0 - fi - fi - else - log "Step 1: Skipped deploy" - fi - - export UPGRADE_ACTION_ADDRESS - - # Step 2: Execute - if [[ "$SKIP_EXECUTE" != "true" ]]; then - log "Step 2: Executing upgrade..." - - local forge_cmd="forge script $execute_script --rpc-url $PARENT_CHAIN_RPC -vvv" - - if [[ "$DRY_RUN" != "true" ]]; then - forge_cmd="$forge_cmd --broadcast $execute_auth" - fi - - eval $forge_cmd - - if [[ "$DRY_RUN" == "true" ]]; then - log "Dry run - upgrade not executed" - else - log "Upgrade executed successfully" - fi - else - log "Step 2: Skipped execute" - fi - - # Step 3: Verify - if [[ "$DRY_RUN" != "true" && "$SKIP_EXECUTE" != "true" ]]; then - log "Step 3: Verifying upgrade..." - - local verify_script=$(find "$version_dir" -maxdepth 1 -name "Verify*.s.sol" -type f 2>/dev/null | head -1) - if [[ -n "$verify_script" ]]; then - forge script "$verify_script" --rpc-url "$PARENT_CHAIN_RPC" -vvv - else - log "No Verify script found - check README for manual verification" - fi - fi - - log "Done" -} - -# ============================================================================= -# Main -# ============================================================================= - -show_help() { - cat <<'EOF' -Usage: contract-upgrade [options] - -Commands: - deploy Run deploy script only - execute Run execute script only - verify Run verify script only - deploy-execute-verify Full upgrade flow (deploy, execute, verify) - -Options for deploy/execute: - --private-key KEY Private key - --account NAME Keystore account - --ledger Use Ledger - --interactive Prompt for key - -Options for deploy-execute-verify: - --deploy-key KEY Private key for deploy step - --deploy-account NAME Keystore account for deploy - --deploy-ledger Use Ledger for deploy - --execute-key KEY Private key for execute step - --execute-account NAME Keystore account for execute - --execute-ledger Use Ledger for execute - --dry-run, -n Simulate without broadcasting - --skip-execute Deploy only - --verify, -v Verify on block explorer - -Required .env variables: - PARENT_CHAIN_RPC Parent chain RPC URL - INBOX_ADDRESS Inbox contract address - PROXY_ADMIN_ADDRESS ProxyAdmin address - PARENT_UPGRADE_EXECUTOR_ADDRESS UpgradeExecutor address - -Optional .env variables: - UPGRADE_ACTION_ADDRESS Pre-deployed action (skips deploy) - ROLLUP Rollup address (for verification) -EOF -} - -main() { - if [[ $# -lt 2 ]]; then - show_help - exit 1 - fi - - local version="$1" - local command="$2" - shift 2 - - if [[ "$version" == "--help" || "$version" == "-h" ]]; then - show_help - exit 0 - fi - - local version_dir="$CONTRACTS_DIR/$version" - if [[ ! -d "$version_dir" ]]; then - die "Unknown version: $version - -Available versions: $(ls "$CONTRACTS_DIR" 2>/dev/null | tr '\n' ' ' || echo "none found")" - fi - - case "$command" in - deploy) - cmd_deploy "$version_dir" "$@" - ;; - execute) - cmd_execute "$version_dir" "$@" - ;; - verify) - cmd_verify "$version_dir" "$@" - ;; - deploy-execute-verify) - cmd_deploy_execute_verify "$version_dir" "$@" - ;; - --help|-h) - show_help - ;; - *) - die "Unknown command: $command - -Commands: deploy, execute, verify, deploy-execute-verify" - ;; - esac -} - -main "$@" diff --git a/bin/router b/bin/router deleted file mode 100644 index 8b847226..00000000 --- a/bin/router +++ /dev/null @@ -1,189 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# ============================================================================= -# orbit-actions router -# Handles path parsing, directory listing, file viewing, and command dispatch -# ============================================================================= - -SCRIPTS_DIR="/app/scripts/foundry" - -# ============================================================================= -# Utility Functions -# ============================================================================= - -die() { - echo "Error: $1" >&2 - exit 1 -} - -# ============================================================================= -# Directory Listing -# ============================================================================= - -list_directory() { - local dir="$1" - local rel_path="${dir#$SCRIPTS_DIR/}" - # Normalize: remove leading ./ - rel_path="${rel_path#./}" - - # List actual contents - ls -1 "$dir" - - # Add virtual commands based on directory type - case "$rel_path" in - contract-upgrades/[0-9]*) - echo "---" - echo "deploy (run Deploy script)" - echo "execute (run Execute script)" - echo "verify (run Verify script)" - echo "deploy-execute-verify (full upgrade flow)" - ;; - arbos-upgrades/at-timestamp) - echo "---" - echo "deploy (run Deploy script)" - echo "execute (execute upgrade action)" - echo "verify (check upgrade status)" - echo "deploy-execute-verify (full upgrade flow)" - ;; - esac -} - -# ============================================================================= -# Help -# ============================================================================= - -show_help() { - cat <<'EOF' -Usage: docker run orbit-actions [path] [args...] - -Browse and execute scripts from the foundry scripts directory. - -Browsing: - . List top-level directories - contract-upgrades List available versions - contract-upgrades/1.2.1 List version contents + commands - contract-upgrades/1.2.1/env-templates List env templates - -Viewing files: - contract-upgrades/1.2.1/README.md View README - contract-upgrades/1.2.1/env-templates/.env.example View env template - contract-upgrades/2.1.0/.env.sample View env sample - -Running upgrade scripts: - contract-upgrades//deploy [--private-key KEY] - contract-upgrades//execute [--private-key KEY] - contract-upgrades//deploy-execute-verify [--deploy-key KEY] [--execute-key KEY] - - arbos-upgrades/at-timestamp/deploy [--private-key KEY] - arbos-upgrades/at-timestamp/deploy-execute-verify [--deploy-key KEY] [--execute-key KEY] - -Options for deploy-execute-verify: - --deploy-key KEY Private key for deploy step - --deploy-account NAME Keystore account for deploy - --deploy-ledger Use Ledger for deploy - --execute-key KEY Private key for execute step - --execute-account NAME Keystore account for execute - --execute-ledger Use Ledger for execute - --dry-run, -n Simulate without broadcasting - --skip-execute Deploy only - --verify, -v Verify on block explorer - -Passthrough commands: - forge ... Run forge directly - cast ... Run cast directly - yarn ... Run yarn directly - -Examples: - docker run orbit-actions contract-upgrades/1.2.1 - docker run orbit-actions contract-upgrades/1.2.1/README.md - docker run -v $(pwd)/.env:/app/.env orbit-actions contract-upgrades/1.2.1/deploy-execute-verify --dry-run -EOF -} - -# ============================================================================= -# Main Router -# ============================================================================= - -main() { - # No args - list top level - if [[ $# -eq 0 ]]; then - ls -1 "$SCRIPTS_DIR" - exit 0 - fi - - local path="$1" - shift - - # Help - if [[ "$path" == "help" || "$path" == "--help" || "$path" == "-h" ]]; then - show_help - exit 0 - fi - - # Passthrough: forge, cast, yarn, node, bash, etc. - if command -v "$path" &>/dev/null && [[ ! -e "$SCRIPTS_DIR/$path" ]]; then - exec "$path" "$@" - fi - - # Parse path for virtual commands - local full_path="$SCRIPTS_DIR/$path" - local parent_path=$(dirname "$full_path") - local basename=$(basename "$path") - - # Check for virtual commands (deploy, execute, deploy-execute-verify) - if [[ ! -e "$full_path" && -d "$parent_path" ]]; then - local rel_parent="${parent_path#$SCRIPTS_DIR/}" - # Normalize: remove leading ./ - rel_parent="${rel_parent#./}" - - case "$rel_parent" in - contract-upgrades/[0-9]*) - local version=$(basename "$rel_parent") - case "$basename" in - deploy|execute|verify|deploy-execute-verify) - exec /app/bin/contract-upgrade "$version" "$basename" "$@" - ;; - esac - ;; - arbos-upgrades/at-timestamp) - case "$basename" in - deploy|deploy-execute-verify) - # These commands need a version argument - local version="${1:-}" - if [[ -z "$version" ]]; then - echo "Error: ArbOS version required" >&2 - echo "Usage: arbos-upgrades/at-timestamp/$basename [options]" >&2 - exit 1 - fi - shift - exec /app/bin/arbos-upgrade "$version" "$basename" "$@" - ;; - execute|verify) - # These commands don't need a version argument - exec /app/bin/arbos-upgrade "" "$basename" "$@" - ;; - esac - ;; - esac - fi - - # Directory - list contents - if [[ -d "$full_path" ]]; then - list_directory "$full_path" - exit 0 - fi - - # Regular file - cat it - if [[ -f "$full_path" ]]; then - cat "$full_path" - exit 0 - fi - - # Not found - die "Not found: $path - -Use 'help' to see available commands." -} - -main "$@" diff --git a/entrypoint.sh b/entrypoint.sh deleted file mode 100755 index 965dfa52..00000000 --- a/entrypoint.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -# Source .env if mounted, then delegate to router -[[ -f /app/.env ]] && set -a && source /app/.env && set +a -exec /app/bin/router "$@" diff --git a/lib/common.sh b/lib/common.sh deleted file mode 100644 index 9a4ac560..00000000 --- a/lib/common.sh +++ /dev/null @@ -1,135 +0,0 @@ -#!/bin/bash -# Shared utilities for upgrade scripts - -# ============================================================================= -# Utility Functions -# ============================================================================= - -die() { - echo "Error: $1" >&2 - exit 1 -} - -log() { - echo "[orbit-actions] $1" -} - -require_env() { - local name="$1" - local value="${!name:-}" - if [[ -z "$value" ]]; then - die "Required env var not set: $name (check your .env file)" - fi -} - -# ============================================================================= -# Auth Helpers -# ============================================================================= - -# Build forge/cast auth args from CLI flags -# Sets global: AUTH_ARGS -parse_auth_args() { - AUTH_ARGS="" - while [[ $# -gt 0 ]]; do - case "$1" in - --private-key|--account) - AUTH_ARGS="$1 $2" - shift 2 - ;; - --ledger|--interactive) - AUTH_ARGS="$1" - shift - ;; - *) - shift - ;; - esac - done -} - -# Build auth args for deploy step (from --deploy-* flags) -get_deploy_auth() { - if [[ -n "${DEPLOY_KEY:-}" ]]; then - echo "--private-key $DEPLOY_KEY" - elif [[ -n "${DEPLOY_ACCOUNT:-}" ]]; then - echo "--account $DEPLOY_ACCOUNT" - elif [[ "${DEPLOY_LEDGER:-}" == "true" ]]; then - echo "--ledger" - elif [[ "${DEPLOY_INTERACTIVE:-}" == "true" ]]; then - echo "--interactive" - fi -} - -# Build auth args for execute step (from --execute-* flags) -get_execute_auth() { - if [[ -n "${EXECUTE_KEY:-}" ]]; then - echo "--private-key $EXECUTE_KEY" - elif [[ -n "${EXECUTE_ACCOUNT:-}" ]]; then - echo "--account $EXECUTE_ACCOUNT" - elif [[ "${EXECUTE_LEDGER:-}" == "true" ]]; then - echo "--ledger" - elif [[ "${EXECUTE_INTERACTIVE:-}" == "true" ]]; then - echo "--interactive" - fi -} - -# Parse --deploy-* and --execute-* flags into variables -parse_deploy_execute_auth() { - DEPLOY_KEY="" - DEPLOY_ACCOUNT="" - DEPLOY_LEDGER=false - DEPLOY_INTERACTIVE=false - EXECUTE_KEY="" - EXECUTE_ACCOUNT="" - EXECUTE_LEDGER=false - EXECUTE_INTERACTIVE=false - DRY_RUN=false - SKIP_EXECUTE=false - VERIFY_CONTRACTS=false - REMAINING_ARGS=() - - while [[ $# -gt 0 ]]; do - case "$1" in - --deploy-key) DEPLOY_KEY="$2"; shift 2 ;; - --deploy-account) DEPLOY_ACCOUNT="$2"; shift 2 ;; - --deploy-ledger) DEPLOY_LEDGER=true; shift ;; - --deploy-interactive) DEPLOY_INTERACTIVE=true; shift ;; - --execute-key) EXECUTE_KEY="$2"; shift 2 ;; - --execute-account) EXECUTE_ACCOUNT="$2"; shift 2 ;; - --execute-ledger) EXECUTE_LEDGER=true; shift ;; - --execute-interactive) EXECUTE_INTERACTIVE=true; shift ;; - --dry-run|-n) DRY_RUN=true; shift ;; - --skip-execute) SKIP_EXECUTE=true; shift ;; - --verify|-v) VERIFY_CONTRACTS=true; shift ;; - *) REMAINING_ARGS+=("$1"); shift ;; - esac - done -} - -# ============================================================================= -# Forge Script Helpers -# ============================================================================= - -get_chain_id() { - local rpc="$1" - cast chain-id --rpc-url "$rpc" -} - -parse_action_address() { - local script_path="$1" - local chain_id="$2" - local script_name=$(basename "$script_path") - local broadcast_file="/app/broadcast/${script_name}/${chain_id}/run-latest.json" - - if [[ ! -f "$broadcast_file" ]]; then - die "Broadcast file not found: $broadcast_file" - fi - - local address=$(jq -r '.transactions | map(select(.transactionType == "CREATE")) | last | .contractAddress' "$broadcast_file") - - if [[ -z "$address" || "$address" == "null" ]]; then - die "Could not parse action address from broadcast file" - fi - - echo "$address" -} diff --git a/package.json b/package.json index 9c12cdb1..69e020fa 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,12 @@ "version": "1.0.0", "repository": "https://github.com/OffchainLabs/blockchain-eng-template.git", "license": "Apache 2.0", + "bin": { + "orbit-actions": "./dist/cli/index.js" + }, "scripts": { + "build:cli": "tsc -p tsconfig.cli.json", + "cli": "ts-node src/cli/index.ts", "prepare": "forge install && cd lib/arbitrum-sdk && yarn", "minimal-publish": "./scripts/publish.bash", "minimal-install": "yarn --ignore-scripts && forge install", @@ -62,5 +67,9 @@ "ts-node": ">=8.0.0", "typechain": "^8.3.0", "typescript": ">=4.5.0" + }, + "dependencies": { + "commander": "^12.0.0", + "execa": "^5.1.1" } } diff --git a/src/cli/commands/arbos-upgrade.ts b/src/cli/commands/arbos-upgrade.ts new file mode 100644 index 00000000..6724cacf --- /dev/null +++ b/src/cli/commands/arbos-upgrade.ts @@ -0,0 +1,303 @@ +/** + * ArbOS upgrade commands + */ + +import { Command } from 'commander'; +import * as path from 'path'; +import * as fs from 'fs'; +import { log, die } from '../utils/log'; +import { requireEnv, getEnv, getScriptsDir } from '../utils/env'; +import { parseAuthArgs, createDeployExecuteAuth, getDeployAuth, getExecuteAuth } from '../utils/auth'; +import { + runForgeScript, + runCastSend, + runCastCall, + castCalldata, + getChainId, + parseActionAddress, +} from '../utils/forge'; + +const ARBOS_DIR = path.join(getScriptsDir(), 'arbos-upgrades', 'at-timestamp'); +const DEPLOY_SCRIPT = path.join(ARBOS_DIR, 'DeployUpgradeArbOSVersionAtTimestampAction.s.sol'); + +async function cmdDeploy(version: string, args: string[]): Promise { + const authArgs = parseAuthArgs(args); + + const rpcUrl = requireEnv('CHILD_CHAIN_RPC'); + + if (!fs.existsSync(DEPLOY_SCRIPT)) { + die(`Deploy script not found: ${DEPLOY_SCRIPT}`); + } + + // Export version for the script + process.env.ARBOS_VERSION = version; + + log(`Running: ${path.basename(DEPLOY_SCRIPT)} for ArbOS ${version}`); + + await runForgeScript({ + script: DEPLOY_SCRIPT, + rpcUrl, + authArgs, + broadcast: !!authArgs, + slow: true, + }); +} + +async function cmdExecute(args: string[]): Promise { + const authArgs = parseAuthArgs(args); + + const rpcUrl = requireEnv('CHILD_CHAIN_RPC'); + const upgradeExecutor = requireEnv('CHILD_UPGRADE_EXECUTOR_ADDRESS'); + const actionAddress = requireEnv('UPGRADE_ACTION_ADDRESS'); + + log(`Executing ArbOS upgrade action: ${actionAddress}`); + + const performCalldata = '0xb0a75d36'; // perform() selector + + if (!authArgs) { + // No auth - output calldata for multisig + const executeCalldata = await castCalldata('execute(address,bytes)', actionAddress, performCalldata); + + log('Calldata for UpgradeExecutor.execute():'); + console.log(''); + console.log(`To: ${upgradeExecutor}`); + console.log(`Calldata: ${executeCalldata}`); + console.log(''); + log('Submit this to your multisig/Safe to execute the upgrade'); + } else { + await runCastSend({ + to: upgradeExecutor, + sig: 'execute(address,bytes)', + args: [actionAddress, performCalldata], + rpcUrl, + authArgs, + }); + + log('ArbOS upgrade scheduled successfully'); + } +} + +async function cmdVerify(): Promise { + const rpcUrl = requireEnv('CHILD_CHAIN_RPC'); + + log('Checking ArbOS upgrade status...'); + + const scheduled = await runCastCall({ + to: '0x000000000000000000000000000000000000006b', + sig: 'getScheduledUpgrade()(uint64,uint64)', + rpcUrl, + }); + log(`Scheduled upgrade (version, timestamp): ${scheduled}`); + + const currentRaw = await runCastCall({ + to: '0x0000000000000000000000000000000000000064', + sig: 'arbOSVersion()(uint64)', + rpcUrl, + }); + + let currentVersion: number; + if (currentRaw === 'N/A') { + currentVersion = 0; + } else { + const rawNum = parseInt(currentRaw, 10); + currentVersion = rawNum - 55; + } + + log(`Current ArbOS version: ${currentVersion}`); +} + +async function cmdDeployExecuteVerify( + version: string, + options: { + deployKey?: string; + deployAccount?: string; + deployLedger?: boolean; + deployInteractive?: boolean; + executeKey?: string; + executeAccount?: string; + executeLedger?: boolean; + executeInteractive?: boolean; + dryRun?: boolean; + skipExecute?: boolean; + verify?: boolean; + } +): Promise { + const auth = createDeployExecuteAuth(options); + + log(`ArbOS version: ${version}`); + + if (!fs.existsSync(DEPLOY_SCRIPT)) { + die(`Deploy script not found: ${DEPLOY_SCRIPT}`); + } + + // Auto-detect skip_deploy if UPGRADE_ACTION_ADDRESS is set + let skipDeploy = !!getEnv('UPGRADE_ACTION_ADDRESS'); + let upgradeActionAddress = getEnv('UPGRADE_ACTION_ADDRESS') || ''; + + if (skipDeploy) { + log(`Using existing action from .env: ${upgradeActionAddress}`); + } + + // Validate required env vars + const rpcUrl = requireEnv('CHILD_CHAIN_RPC'); + const upgradeExecutor = requireEnv('CHILD_UPGRADE_EXECUTOR_ADDRESS'); + requireEnv('SCHEDULE_TIMESTAMP'); + + // Export version for the script + process.env.ARBOS_VERSION = version; + + // Validate auth + const deployAuth = getDeployAuth(auth); + const executeAuth = getExecuteAuth(auth); + + if (!skipDeploy && !auth.dryRun && !deployAuth) { + die('Deploy auth required. Use --deploy-key, --deploy-account, --deploy-ledger, or --deploy-interactive'); + } + if (!auth.skipExecute && !auth.dryRun && !executeAuth) { + die('Execute auth required. Use --execute-key, --execute-account, --execute-ledger, or --execute-interactive'); + } + + log(`Scheduled timestamp: ${process.env.SCHEDULE_TIMESTAMP}`); + + // Step 1: Deploy + let chainId = ''; + if (!skipDeploy) { + chainId = await getChainId(rpcUrl); + log(`Target chain ID: ${chainId}`); + log('Step 1: Deploying ArbOS upgrade action...'); + + await runForgeScript({ + script: DEPLOY_SCRIPT, + rpcUrl, + authArgs: deployAuth, + broadcast: !auth.dryRun, + verify: auth.verifyContracts, + slow: true, + }); + + if (!auth.dryRun) { + upgradeActionAddress = parseActionAddress(DEPLOY_SCRIPT, chainId); + log(`Deployed action at: ${upgradeActionAddress}`); + } else { + log('Dry run - no action deployed'); + if (!auth.skipExecute) { + log('Note: Set UPGRADE_ACTION_ADDRESS in .env to run execute step'); + return; + } + } + } else { + log('Step 1: Skipped deploy'); + } + + // Step 2: Execute via cast send + if (!auth.skipExecute) { + log('Step 2: Executing ArbOS upgrade...'); + + const performCalldata = '0xb0a75d36'; + + if (auth.dryRun) { + const executeCalldata = await castCalldata('execute(address,bytes)', upgradeActionAddress, performCalldata); + + log('Dry run - calldata for UpgradeExecutor.execute():'); + console.log(''); + console.log(`To: ${upgradeExecutor}`); + console.log(`Calldata: ${executeCalldata}`); + console.log(''); + log('Submit this to your multisig/Safe to execute the upgrade'); + } else { + await runCastSend({ + to: upgradeExecutor, + sig: 'execute(address,bytes)', + args: [upgradeActionAddress, performCalldata], + rpcUrl, + authArgs: executeAuth, + }); + + log('ArbOS upgrade scheduled successfully'); + } + } else { + log('Step 2: Skipped execute'); + } + + // Step 3: Verify + if (!auth.dryRun && !auth.skipExecute) { + log('Step 3: Verifying scheduled upgrade...'); + + const scheduled = await runCastCall({ + to: '0x000000000000000000000000000000000000006b', + sig: 'getScheduledUpgrade()(uint64,uint64)', + rpcUrl, + }); + log(`Scheduled upgrade (version, timestamp): ${scheduled}`); + + const currentRaw = await runCastCall({ + to: '0x0000000000000000000000000000000000000064', + sig: 'arbOSVersion()(uint64)', + rpcUrl, + }); + + let currentVersion: number; + if (currentRaw === 'N/A') { + currentVersion = 0; + } else { + const rawNum = parseInt(currentRaw, 10); + currentVersion = rawNum - 55; + } + + log(`Current ArbOS version: ${currentVersion}`); + } + + log('Done'); +} + +export function createArbosUpgradeCommand(): Command { + const cmd = new Command('arbos-upgrade') + .description('ArbOS upgrade operations') + .argument('', 'ArbOS version number') + .argument('[command]', 'Command: deploy, execute, verify, deploy-execute-verify', 'deploy-execute-verify') + .option('--private-key ', 'Private key (for deploy/execute)') + .option('--account ', 'Keystore account (for deploy/execute)') + .option('--ledger', 'Use Ledger (for deploy/execute)') + .option('--interactive', 'Prompt for key (for deploy/execute)') + .option('--deploy-key ', 'Private key for deploy step') + .option('--deploy-account ', 'Keystore account for deploy') + .option('--deploy-ledger', 'Use Ledger for deploy') + .option('--deploy-interactive', 'Prompt for key for deploy') + .option('--execute-key ', 'Private key for execute step') + .option('--execute-account ', 'Keystore account for execute') + .option('--execute-ledger', 'Use Ledger for execute') + .option('--execute-interactive', 'Prompt for key for execute') + .option('-n, --dry-run', 'Simulate without broadcasting') + .option('--skip-execute', 'Deploy only') + .option('-v, --verify', 'Verify on block explorer') + .action(async (version: string, command: string, options) => { + // Build args array from remaining options for simple commands + const args: string[] = []; + if (options.privateKey) args.push('--private-key', options.privateKey); + if (options.account) args.push('--account', options.account); + if (options.ledger) args.push('--ledger'); + if (options.interactive) args.push('--interactive'); + + switch (command) { + case 'deploy': + await cmdDeploy(version, args); + break; + case 'execute': + await cmdExecute(args); + break; + case 'verify': + await cmdVerify(); + break; + case 'deploy-execute-verify': + await cmdDeployExecuteVerify(version, options); + break; + default: + die(`Unknown command: ${command}\n\nCommands: deploy, execute, verify, deploy-execute-verify`); + } + }); + + return cmd; +} + +// Export individual functions for use by router +export { cmdDeploy, cmdExecute, cmdVerify, cmdDeployExecuteVerify }; diff --git a/src/cli/commands/contract-upgrade.ts b/src/cli/commands/contract-upgrade.ts new file mode 100644 index 00000000..7aa66d17 --- /dev/null +++ b/src/cli/commands/contract-upgrade.ts @@ -0,0 +1,269 @@ +/** + * Contract upgrade commands + */ + +import { Command } from 'commander'; +import * as path from 'path'; +import * as fs from 'fs'; +import { log, die } from '../utils/log'; +import { requireEnv, getEnv, getScriptsDir, getRepoRoot } from '../utils/env'; +import { parseAuthArgs, createDeployExecuteAuth, getDeployAuth, getExecuteAuth } from '../utils/auth'; +import { runForgeScript, getChainId, parseActionAddress, findScript } from '../utils/forge'; + +const CONTRACTS_DIR = path.join(getScriptsDir(), 'contract-upgrades'); + +function getVersionDir(version: string): string { + const versionDir = path.join(CONTRACTS_DIR, version); + if (!fs.existsSync(versionDir)) { + const available = fs.existsSync(CONTRACTS_DIR) + ? fs.readdirSync(CONTRACTS_DIR).filter((f) => !f.startsWith('.')).join(' ') + : 'none found'; + die(`Unknown version: ${version}\n\nAvailable versions: ${available}`); + } + return versionDir; +} + +async function cmdDeploy(version: string, args: string[]): Promise { + const versionDir = getVersionDir(version); + const authArgs = parseAuthArgs(args); + + const rpcUrl = requireEnv('PARENT_CHAIN_RPC'); + + const deployScript = findScript(versionDir, /^Deploy.*\.s\.sol$/); + if (!deployScript) { + die(`No deploy script found in ${versionDir}`); + } + + log(`Running: ${path.basename(deployScript)}`); + + await runForgeScript({ + script: deployScript, + rpcUrl, + authArgs, + broadcast: !!authArgs, + slow: true, + skipSimulation: true, + }); +} + +async function cmdExecute(version: string, args: string[]): Promise { + const versionDir = getVersionDir(version); + const authArgs = parseAuthArgs(args); + + const rpcUrl = requireEnv('PARENT_CHAIN_RPC'); + requireEnv('UPGRADE_ACTION_ADDRESS'); + + const executeScript = findScript(versionDir, /^Execute.*\.s\.sol$/); + if (!executeScript) { + die(`No execute script found in ${versionDir}`); + } + + log(`Running: ${path.basename(executeScript)}`); + + await runForgeScript({ + script: executeScript, + rpcUrl, + authArgs, + broadcast: !!authArgs, + }); +} + +async function cmdVerify(version: string): Promise { + const versionDir = getVersionDir(version); + + const rpcUrl = requireEnv('PARENT_CHAIN_RPC'); + + const verifyScript = findScript(versionDir, /^Verify.*\.s\.sol$/); + if (!verifyScript) { + die(`No verify script found in ${versionDir} - check README for manual verification`); + } + + log(`Running: ${path.basename(verifyScript)}`); + + await runForgeScript({ + script: verifyScript, + rpcUrl, + }); +} + +async function cmdDeployExecuteVerify( + version: string, + options: { + deployKey?: string; + deployAccount?: string; + deployLedger?: boolean; + deployInteractive?: boolean; + executeKey?: string; + executeAccount?: string; + executeLedger?: boolean; + executeInteractive?: boolean; + dryRun?: boolean; + skipExecute?: boolean; + verify?: boolean; + } +): Promise { + const versionDir = getVersionDir(version); + const auth = createDeployExecuteAuth(options); + + const deployScript = findScript(versionDir, /^Deploy.*\.s\.sol$/); + const executeScript = findScript(versionDir, /^Execute.*\.s\.sol$/); + + if (!deployScript) { + die(`No deploy script found in ${versionDir}`); + } + if (!executeScript) { + die(`No execute script found in ${versionDir}`); + } + + log(`Version: ${version}`); + log(`Deploy script: ${path.basename(deployScript)}`); + log(`Execute script: ${path.basename(executeScript)}`); + + // Auto-detect skip_deploy if UPGRADE_ACTION_ADDRESS is set + let skipDeploy = !!getEnv('UPGRADE_ACTION_ADDRESS'); + let upgradeActionAddress = getEnv('UPGRADE_ACTION_ADDRESS') || ''; + + if (skipDeploy) { + log(`Using existing action from .env: ${upgradeActionAddress}`); + } + + // Validate required env vars + const rpcUrl = requireEnv('PARENT_CHAIN_RPC'); + requireEnv('INBOX_ADDRESS'); + requireEnv('PROXY_ADMIN_ADDRESS'); + requireEnv('PARENT_UPGRADE_EXECUTOR_ADDRESS'); + + // Validate auth + const deployAuth = getDeployAuth(auth); + const executeAuth = getExecuteAuth(auth); + + if (!skipDeploy && !auth.dryRun && !deployAuth) { + die('Deploy auth required. Use --deploy-key, --deploy-account, --deploy-ledger, or --deploy-interactive'); + } + if (!auth.skipExecute && !auth.dryRun && !executeAuth) { + die('Execute auth required. Use --execute-key, --execute-account, --execute-ledger, or --execute-interactive'); + } + + const chainId = await getChainId(rpcUrl); + log(`Target chain ID: ${chainId}`); + + // Step 1: Deploy + if (!skipDeploy) { + log('Step 1: Deploying upgrade action...'); + + await runForgeScript({ + script: deployScript, + rpcUrl, + authArgs: deployAuth, + broadcast: !auth.dryRun, + verify: auth.verifyContracts, + slow: true, + skipSimulation: true, + }); + + if (!auth.dryRun) { + upgradeActionAddress = parseActionAddress(deployScript, chainId); + log(`Deployed action at: ${upgradeActionAddress}`); + } else { + log('Dry run - no action deployed'); + if (!auth.skipExecute) { + log('Note: Set UPGRADE_ACTION_ADDRESS in .env to run execute step'); + return; + } + } + } else { + log('Step 1: Skipped deploy'); + } + + // Export for execute script + process.env.UPGRADE_ACTION_ADDRESS = upgradeActionAddress; + + // Step 2: Execute + if (!auth.skipExecute) { + log('Step 2: Executing upgrade...'); + + await runForgeScript({ + script: executeScript, + rpcUrl, + authArgs: executeAuth, + broadcast: !auth.dryRun, + }); + + if (auth.dryRun) { + log('Dry run - upgrade not executed'); + } else { + log('Upgrade executed successfully'); + } + } else { + log('Step 2: Skipped execute'); + } + + // Step 3: Verify + if (!auth.dryRun && !auth.skipExecute) { + log('Step 3: Verifying upgrade...'); + + const verifyScript = findScript(versionDir, /^Verify.*\.s\.sol$/); + if (verifyScript) { + await runForgeScript({ + script: verifyScript, + rpcUrl, + }); + } else { + log('No Verify script found - check README for manual verification'); + } + } + + log('Done'); +} + +export function createContractUpgradeCommand(): Command { + const cmd = new Command('contract-upgrade') + .description('Contract upgrade operations') + .argument('', 'Contract version (e.g., 1.2.1)') + .argument('', 'Command: deploy, execute, verify, deploy-execute-verify') + .option('--private-key ', 'Private key (for deploy/execute)') + .option('--account ', 'Keystore account (for deploy/execute)') + .option('--ledger', 'Use Ledger (for deploy/execute)') + .option('--interactive', 'Prompt for key (for deploy/execute)') + .option('--deploy-key ', 'Private key for deploy step') + .option('--deploy-account ', 'Keystore account for deploy') + .option('--deploy-ledger', 'Use Ledger for deploy') + .option('--deploy-interactive', 'Prompt for key for deploy') + .option('--execute-key ', 'Private key for execute step') + .option('--execute-account ', 'Keystore account for execute') + .option('--execute-ledger', 'Use Ledger for execute') + .option('--execute-interactive', 'Prompt for key for execute') + .option('-n, --dry-run', 'Simulate without broadcasting') + .option('--skip-execute', 'Deploy only') + .option('-v, --verify', 'Verify on block explorer') + .action(async (version: string, command: string, options) => { + // Build args array from remaining options for simple commands + const args: string[] = []; + if (options.privateKey) args.push('--private-key', options.privateKey); + if (options.account) args.push('--account', options.account); + if (options.ledger) args.push('--ledger'); + if (options.interactive) args.push('--interactive'); + + switch (command) { + case 'deploy': + await cmdDeploy(version, args); + break; + case 'execute': + await cmdExecute(version, args); + break; + case 'verify': + await cmdVerify(version); + break; + case 'deploy-execute-verify': + await cmdDeployExecuteVerify(version, options); + break; + default: + die(`Unknown command: ${command}\n\nCommands: deploy, execute, verify, deploy-execute-verify`); + } + }); + + return cmd; +} + +// Export individual functions for use by router +export { cmdDeploy, cmdExecute, cmdVerify, cmdDeployExecuteVerify, getVersionDir }; diff --git a/src/cli/index.ts b/src/cli/index.ts new file mode 100644 index 00000000..33b02d1a --- /dev/null +++ b/src/cli/index.ts @@ -0,0 +1,29 @@ +#!/usr/bin/env node +/** + * orbit-actions CLI entry point + */ + +import { program } from 'commander'; +import { loadEnv } from './utils/env'; +import { router } from './router'; +import { createContractUpgradeCommand } from './commands/contract-upgrade'; +import { createArbosUpgradeCommand } from './commands/arbos-upgrade'; + +// Load .env from repo root (or /app in Docker) +loadEnv(); + +program + .name('orbit-actions') + .description('CLI for Orbit chain upgrade actions') + .argument('[path]', 'Path to browse or command to run') + .argument('[args...]', 'Additional arguments') + .allowUnknownOption(true) + .action(async (pathArg?: string, args?: string[]) => { + await router(pathArg, args); + }); + +// Register subcommands for direct invocation +program.addCommand(createContractUpgradeCommand()); +program.addCommand(createArbosUpgradeCommand()); + +program.parse(); diff --git a/src/cli/router.ts b/src/cli/router.ts new file mode 100644 index 00000000..50fb2870 --- /dev/null +++ b/src/cli/router.ts @@ -0,0 +1,227 @@ +/** + * Path routing for CLI + * Handles directory listing, file viewing, and command dispatch + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import { die } from './utils/log'; +import { getScriptsDir } from './utils/env'; +import { + cmdDeploy as contractDeploy, + cmdExecute as contractExecute, + cmdVerify as contractVerify, + cmdDeployExecuteVerify as contractDeployExecuteVerify, +} from './commands/contract-upgrade'; +import { + cmdDeploy as arbosDeploy, + cmdExecute as arbosExecute, + cmdVerify as arbosVerify, + cmdDeployExecuteVerify as arbosDeployExecuteVerify, +} from './commands/arbos-upgrade'; + +const HELP_TEXT = `Usage: orbit-actions [path] [args...] + +Browse and execute scripts from the foundry scripts directory. + +Browsing: + . List top-level directories + contract-upgrades List available versions + contract-upgrades/1.2.1 List version contents + commands + contract-upgrades/1.2.1/env-templates List env templates + +Viewing files: + contract-upgrades/1.2.1/README.md View README + contract-upgrades/1.2.1/env-templates/.env.example View env template + contract-upgrades/2.1.0/.env.sample View env sample + +Running upgrade scripts: + contract-upgrades//deploy [--private-key KEY] + contract-upgrades//execute [--private-key KEY] + contract-upgrades//verify + contract-upgrades//deploy-execute-verify [--deploy-key KEY] [--execute-key KEY] + + arbos-upgrades/at-timestamp/deploy [--private-key KEY] + arbos-upgrades/at-timestamp/execute [--private-key KEY] + arbos-upgrades/at-timestamp/verify + arbos-upgrades/at-timestamp/deploy-execute-verify [--deploy-key KEY] [--execute-key KEY] + +Options for deploy-execute-verify: + --deploy-key KEY Private key for deploy step + --deploy-account NAME Keystore account for deploy + --deploy-ledger Use Ledger for deploy + --execute-key KEY Private key for execute step + --execute-account NAME Keystore account for execute + --execute-ledger Use Ledger for execute + --dry-run, -n Simulate without broadcasting + --skip-execute Deploy only + --verify, -v Verify on block explorer + +Examples: + docker run orbit-actions contract-upgrades/1.2.1 + docker run orbit-actions contract-upgrades/1.2.1/README.md + docker run -v $(pwd)/.env:/app/.env orbit-actions contract-upgrades/1.2.1/deploy-execute-verify --dry-run`; + +function listDirectory(dir: string): void { + const scriptsDir = getScriptsDir(); + let relPath = path.relative(scriptsDir, dir); + if (relPath === '.') relPath = ''; + + // List actual contents + const contents = fs.readdirSync(dir); + for (const item of contents) { + // Skip hidden files + if (!item.startsWith('.')) { + console.log(item); + } + } + + // Add virtual commands based on directory type + if (/^contract-upgrades\/[0-9]/.test(relPath)) { + console.log('---'); + console.log('deploy (run Deploy script)'); + console.log('execute (run Execute script)'); + console.log('verify (run Verify script)'); + console.log('deploy-execute-verify (full upgrade flow)'); + } else if (relPath === 'arbos-upgrades/at-timestamp') { + console.log('---'); + console.log('deploy (run Deploy script)'); + console.log('execute (execute upgrade action)'); + console.log('verify (check upgrade status)'); + console.log('deploy-execute-verify (full upgrade flow)'); + } +} + +function parseOptions(args: string[]): Record { + const options: Record = {}; + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + if (arg === '--deploy-key' || arg === '--execute-key' || arg === '--deploy-account' || arg === '--execute-account') { + options[arg.replace(/^--/, '').replace(/-([a-z])/g, (_, c) => c.toUpperCase())] = args[++i] || ''; + } else if (arg === '--deploy-ledger') { + options.deployLedger = true; + } else if (arg === '--execute-ledger') { + options.executeLedger = true; + } else if (arg === '--deploy-interactive') { + options.deployInteractive = true; + } else if (arg === '--execute-interactive') { + options.executeInteractive = true; + } else if (arg === '--dry-run' || arg === '-n') { + options.dryRun = true; + } else if (arg === '--skip-execute') { + options.skipExecute = true; + } else if (arg === '--verify' || arg === '-v') { + options.verify = true; + } else if (arg === '--private-key') { + options.privateKey = args[++i] || ''; + } else if (arg === '--account') { + options.account = args[++i] || ''; + } else if (arg === '--ledger') { + options.ledger = true; + } else if (arg === '--interactive') { + options.interactive = true; + } + } + return options; +} + +export async function router(pathArg?: string, args: string[] = []): Promise { + const scriptsDir = getScriptsDir(); + + // No args - list top level + if (!pathArg) { + const contents = fs.readdirSync(scriptsDir); + for (const item of contents) { + if (!item.startsWith('.')) { + console.log(item); + } + } + return; + } + + // Help + if (pathArg === 'help' || pathArg === '--help' || pathArg === '-h') { + console.log(HELP_TEXT); + return; + } + + const fullPath = path.join(scriptsDir, pathArg); + + // Parse path for virtual commands + const parentPath = path.dirname(fullPath); + const basename = path.basename(pathArg); + + // Check for virtual commands (deploy, execute, deploy-execute-verify) + if (!fs.existsSync(fullPath) && fs.existsSync(parentPath)) { + const relParent = path.relative(scriptsDir, parentPath); + + // Contract upgrades virtual commands + if (/^contract-upgrades\/[0-9]/.test(relParent)) { + const version = path.basename(relParent); + const options = parseOptions(args); + + switch (basename) { + case 'deploy': + await contractDeploy(version, args); + return; + case 'execute': + await contractExecute(version, args); + return; + case 'verify': + await contractVerify(version); + return; + case 'deploy-execute-verify': + await contractDeployExecuteVerify(version, options); + return; + } + } + + // ArbOS upgrades virtual commands + if (relParent === 'arbos-upgrades/at-timestamp') { + switch (basename) { + case 'deploy': + case 'deploy-execute-verify': { + // These commands need a version argument + const version = args[0]; + if (!version) { + console.error(`Error: ArbOS version required`); + console.error(`Usage: arbos-upgrades/at-timestamp/${basename} [options]`); + process.exit(1); + } + const restArgs = args.slice(1); + const restOptions = parseOptions(restArgs); + if (basename === 'deploy') { + await arbosDeploy(version, restArgs); + } else { + await arbosDeployExecuteVerify(version, restOptions); + } + return; + } + case 'execute': + await arbosExecute(args); + return; + case 'verify': + await arbosVerify(); + return; + } + } + } + + // Directory - list contents + if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) { + listDirectory(fullPath); + return; + } + + // Regular file - cat it + if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) { + const content = fs.readFileSync(fullPath, 'utf-8'); + console.log(content); + return; + } + + // Not found + die(`Not found: ${pathArg} + +Use 'help' to see available commands.`); +} diff --git a/src/cli/utils/auth.ts b/src/cli/utils/auth.ts new file mode 100644 index 00000000..496d0478 --- /dev/null +++ b/src/cli/utils/auth.ts @@ -0,0 +1,110 @@ +/** + * Authentication argument parsing utilities + */ + +export interface AuthArgs { + authArgs: string; +} + +export interface DeployExecuteAuth { + deployKey: string; + deployAccount: string; + deployLedger: boolean; + deployInteractive: boolean; + executeKey: string; + executeAccount: string; + executeLedger: boolean; + executeInteractive: boolean; + dryRun: boolean; + skipExecute: boolean; + verifyContracts: boolean; +} + +/** + * Parse simple auth args (--private-key, --account, --ledger, --interactive) + * Returns forge/cast compatible auth string + */ +export function parseAuthArgs(args: string[]): string { + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + if (arg === '--private-key' || arg === '--account') { + const value = args[i + 1]; + if (value) { + return `${arg} ${value}`; + } + } + if (arg === '--ledger' || arg === '--interactive') { + return arg; + } + } + return ''; +} + +/** + * Get deploy auth args from DeployExecuteAuth + */ +export function getDeployAuth(auth: DeployExecuteAuth): string { + if (auth.deployKey) { + return `--private-key ${auth.deployKey}`; + } + if (auth.deployAccount) { + return `--account ${auth.deployAccount}`; + } + if (auth.deployLedger) { + return '--ledger'; + } + if (auth.deployInteractive) { + return '--interactive'; + } + return ''; +} + +/** + * Get execute auth args from DeployExecuteAuth + */ +export function getExecuteAuth(auth: DeployExecuteAuth): string { + if (auth.executeKey) { + return `--private-key ${auth.executeKey}`; + } + if (auth.executeAccount) { + return `--account ${auth.executeAccount}`; + } + if (auth.executeLedger) { + return '--ledger'; + } + if (auth.executeInteractive) { + return '--interactive'; + } + return ''; +} + +/** + * Create default DeployExecuteAuth from commander options + */ +export function createDeployExecuteAuth(options: { + deployKey?: string; + deployAccount?: string; + deployLedger?: boolean; + deployInteractive?: boolean; + executeKey?: string; + executeAccount?: string; + executeLedger?: boolean; + executeInteractive?: boolean; + dryRun?: boolean; + skipExecute?: boolean; + verify?: boolean; +}): DeployExecuteAuth { + return { + deployKey: options.deployKey || '', + deployAccount: options.deployAccount || '', + deployLedger: options.deployLedger || false, + deployInteractive: options.deployInteractive || false, + executeKey: options.executeKey || '', + executeAccount: options.executeAccount || '', + executeLedger: options.executeLedger || false, + executeInteractive: options.executeInteractive || false, + dryRun: options.dryRun || false, + skipExecute: options.skipExecute || false, + verifyContracts: options.verify || false, + }; +} diff --git a/src/cli/utils/env.ts b/src/cli/utils/env.ts new file mode 100644 index 00000000..65cd06c5 --- /dev/null +++ b/src/cli/utils/env.ts @@ -0,0 +1,84 @@ +/** + * Environment variable utilities + */ + +import * as dotenv from 'dotenv'; +import * as fs from 'fs'; +import * as path from 'path'; +import { die } from './log'; + +/** + * Find the repository root by looking for package.json + */ +function findRepoRoot(): string | null { + let dir = __dirname; + // Walk up from current file location + for (let i = 0; i < 10; i++) { + if (fs.existsSync(path.join(dir, 'package.json'))) { + return dir; + } + const parent = path.dirname(dir); + if (parent === dir) break; + dir = parent; + } + return null; +} + +/** + * Load .env file from: + * 1. Current working directory + * 2. Repository root + * 3. /app/.env (Docker) + */ +export function loadEnv(): void { + const candidates = [ + path.join(process.cwd(), '.env'), + findRepoRoot() ? path.join(findRepoRoot()!, '.env') : null, + '/app/.env', + ].filter((p): p is string => p !== null); + + for (const envPath of candidates) { + if (fs.existsSync(envPath)) { + dotenv.config({ path: envPath }); + return; + } + } +} + +/** + * Require an environment variable to be set + * @throws Exits process if variable is not set + */ +export function requireEnv(name: string): string { + const value = process.env[name]; + if (!value) { + die(`Required env var not set: ${name} (check your .env file)`); + } + return value; +} + +/** + * Get an optional environment variable + */ +export function getEnv(name: string): string | undefined { + return process.env[name]; +} + +/** + * Get the scripts directory path + */ +export function getScriptsDir(): string { + const repoRoot = findRepoRoot(); + if (repoRoot) { + return path.join(repoRoot, 'scripts', 'foundry'); + } + // Fallback for Docker + return '/app/scripts/foundry'; +} + +/** + * Get the repository root path + */ +export function getRepoRoot(): string { + return findRepoRoot() || '/app'; +} diff --git a/src/cli/utils/forge.ts b/src/cli/utils/forge.ts new file mode 100644 index 00000000..c3cb8a15 --- /dev/null +++ b/src/cli/utils/forge.ts @@ -0,0 +1,167 @@ +/** + * Forge/Cast command execution utilities + */ + +import execa from 'execa'; +import * as fs from 'fs'; +import * as path from 'path'; +import { die, log } from './log'; +import { getRepoRoot } from './env'; + +export interface ForgeScriptOptions { + script: string; + rpcUrl: string; + authArgs?: string; + broadcast?: boolean; + verify?: boolean; + slow?: boolean; + skipSimulation?: boolean; + verbosity?: number; +} + +/** + * Run a forge script command + */ +export async function runForgeScript(options: ForgeScriptOptions): Promise { + const args = ['script', options.script, '--rpc-url', options.rpcUrl]; + + if (options.slow) { + args.push('--slow'); + } + + if (options.skipSimulation) { + args.push('--skip-simulation'); + } + + const verbosity = options.verbosity ?? 3; + args.push('-' + 'v'.repeat(verbosity)); + + if (options.broadcast && options.authArgs) { + args.push('--broadcast'); + args.push(...options.authArgs.split(' ').filter(Boolean)); + } + + if (options.verify) { + args.push('--verify'); + } + + log(`Running: forge ${args.slice(0, 2).join(' ')}...`); + + const result = await execa('forge', args, { + stdio: 'inherit', + env: process.env, + }); + + if (result.exitCode !== 0) { + die(`Forge script failed with exit code ${result.exitCode}`); + } +} + +export interface CastSendOptions { + to: string; + sig: string; + args: string[]; + rpcUrl: string; + authArgs?: string; +} + +/** + * Run a cast send command + */ +export async function runCastSend(options: CastSendOptions): Promise { + const args = ['send', options.to, options.sig, ...options.args, '--rpc-url', options.rpcUrl]; + + if (options.authArgs) { + args.push(...options.authArgs.split(' ').filter(Boolean)); + } + + const result = await execa('cast', args, { + stdio: 'inherit', + env: process.env, + }); + + if (result.exitCode !== 0) { + die(`Cast send failed with exit code ${result.exitCode}`); + } +} + +export interface CastCallOptions { + to: string; + sig: string; + rpcUrl: string; +} + +/** + * Run a cast call command and return the result + */ +export async function runCastCall(options: CastCallOptions): Promise { + try { + const result = await execa('cast', ['call', '--rpc-url', options.rpcUrl, options.to, options.sig]); + return result.stdout; + } catch { + return 'N/A'; + } +} + +/** + * Generate calldata using cast + */ +export async function castCalldata(sig: string, ...args: string[]): Promise { + const result = await execa('cast', ['calldata', sig, ...args]); + return result.stdout; +} + +/** + * Get chain ID from RPC URL + */ +export async function getChainId(rpcUrl: string): Promise { + const result = await execa('cast', ['chain-id', '--rpc-url', rpcUrl]); + return result.stdout.trim(); +} + +/** + * Parse the deployed action address from forge broadcast file + */ +export function parseActionAddress(scriptPath: string, chainId: string): string { + const scriptName = path.basename(scriptPath); + const repoRoot = getRepoRoot(); + const broadcastFile = path.join(repoRoot, 'broadcast', scriptName, chainId, 'run-latest.json'); + + if (!fs.existsSync(broadcastFile)) { + die(`Broadcast file not found: ${broadcastFile}`); + } + + const content = JSON.parse(fs.readFileSync(broadcastFile, 'utf-8')); + const createTxs = content.transactions?.filter( + (tx: { transactionType: string }) => tx.transactionType === 'CREATE' + ); + + if (!createTxs || createTxs.length === 0) { + die('Could not parse action address from broadcast file'); + } + + const address = createTxs[createTxs.length - 1]?.contractAddress; + if (!address) { + die('Could not parse action address from broadcast file'); + } + + return address; +} + +/** + * Find a script file by pattern in a directory + */ +export function findScript(dir: string, pattern: RegExp): string | null { + if (!fs.existsSync(dir)) { + return null; + } + + const files = fs.readdirSync(dir); + for (const file of files) { + if (pattern.test(file) && file.endsWith('.s.sol')) { + return path.join(dir, file); + } + } + return null; +} + diff --git a/src/cli/utils/log.ts b/src/cli/utils/log.ts new file mode 100644 index 00000000..1286c19a --- /dev/null +++ b/src/cli/utils/log.ts @@ -0,0 +1,20 @@ +/** + * Logging utilities for CLI + */ + +const PREFIX = '[orbit-actions]'; + +/** + * Log an informational message to stdout + */ +export function log(message: string): void { + console.log(`${PREFIX} ${message}`); +} + +/** + * Log an error message and exit with code 1 + */ +export function die(message: string): never { + console.error(`Error: ${message}`); + process.exit(1); +} diff --git a/tsconfig.cli.json b/tsconfig.cli.json new file mode 100644 index 00000000..8429e32d --- /dev/null +++ b/tsconfig.cli.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "declaration": true + }, + "include": ["src/**/*"] +} diff --git a/yarn.lock b/yarn.lock index 1cd2598d..15c2575d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2082,6 +2082,11 @@ commander@3.0.2: resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.2.tgz#6837c3fb677ad9933d1cfba42dd14d5117d6b39e" integrity sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow== +commander@^12.0.0: + version "12.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3" + integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA== + compare-versions@^6.0.0: version "6.1.1" resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-6.1.1.tgz#7af3cc1099ba37d244b3145a9af5201b629148a9" @@ -2681,7 +2686,43 @@ ethereumjs-util@^7.0.3, ethereumjs-util@^7.1.4: ethereum-cryptography "^0.1.3" rlp "^2.2.4" -"ethers-v5@npm:ethers@^5.7.2", ethers@^5.7.1, ethers@^5.7.2: +"ethers-v5@npm:ethers@^5.7.2": + version "5.7.2" + resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.7.2.tgz#3a7deeabbb8c030d4126b24f84e525466145872e" + integrity sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg== + dependencies: + "@ethersproject/abi" "5.7.0" + "@ethersproject/abstract-provider" "5.7.0" + "@ethersproject/abstract-signer" "5.7.0" + "@ethersproject/address" "5.7.0" + "@ethersproject/base64" "5.7.0" + "@ethersproject/basex" "5.7.0" + "@ethersproject/bignumber" "5.7.0" + "@ethersproject/bytes" "5.7.0" + "@ethersproject/constants" "5.7.0" + "@ethersproject/contracts" "5.7.0" + "@ethersproject/hash" "5.7.0" + "@ethersproject/hdnode" "5.7.0" + "@ethersproject/json-wallets" "5.7.0" + "@ethersproject/keccak256" "5.7.0" + "@ethersproject/logger" "5.7.0" + "@ethersproject/networks" "5.7.1" + "@ethersproject/pbkdf2" "5.7.0" + "@ethersproject/properties" "5.7.0" + "@ethersproject/providers" "5.7.2" + "@ethersproject/random" "5.7.0" + "@ethersproject/rlp" "5.7.0" + "@ethersproject/sha2" "5.7.0" + "@ethersproject/signing-key" "5.7.0" + "@ethersproject/solidity" "5.7.0" + "@ethersproject/strings" "5.7.0" + "@ethersproject/transactions" "5.7.0" + "@ethersproject/units" "5.7.0" + "@ethersproject/wallet" "5.7.0" + "@ethersproject/web" "5.7.1" + "@ethersproject/wordlists" "5.7.0" + +ethers@^5.7.1, ethers@^5.7.2: version "5.7.2" resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.7.2.tgz#3a7deeabbb8c030d4126b24f84e525466145872e" integrity sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg== @@ -2767,6 +2808,21 @@ evp_bytestokey@^1.0.3: md5.js "^1.3.4" safe-buffer "^5.1.1" +execa@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -3017,6 +3073,11 @@ get-port@^3.1.0: resolved "https://registry.yarnpkg.com/get-port/-/get-port-3.2.0.tgz#dd7ce7de187c06c8bf353796ac71e099f0980ebc" integrity sha512-x5UJKlgeUiNT8nyo/AcnwLnZuZNcSjSw0kogRB+Whd1fjjFq4B1hySFxSFWWSn4mIBzg3sRNUDFYc4g5gjPoLg== +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + get-symbol-description@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" @@ -3391,6 +3452,11 @@ https-proxy-agent@^5.0.0: agent-base "6" debug "4" +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -3612,6 +3678,11 @@ is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3: dependencies: call-bind "^1.0.7" +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + is-string@^1.0.5, is-string@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" @@ -3945,6 +4016,11 @@ memorystream@^0.3.1: resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2" integrity sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw== +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + merge2@^1.2.3, merge2@^1.3.0, merge2@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" @@ -3975,6 +4051,11 @@ mime-types@^2.1.12: dependencies: mime-db "1.52.0" +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" @@ -4132,6 +4213,13 @@ normalize-path@^3.0.0, normalize-path@~3.0.0: resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + number-to-bn@1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/number-to-bn/-/number-to-bn-1.7.0.tgz#bb3623592f7e5f9e0030b1977bd41a0c53fe1ea0" @@ -4177,6 +4265,13 @@ once@1.x, once@^1.3.0: dependencies: wrappy "1" +onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + open@^7.4.2: version "7.4.2" resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321" @@ -4311,7 +4406,7 @@ path-key@^2.0.1: resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" integrity sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw== -path-key@^3.1.0: +path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== @@ -4810,7 +4905,7 @@ side-channel@^1.0.4: get-intrinsic "^1.2.4" object-inspect "^1.13.1" -signal-exit@^3.0.2: +signal-exit@^3.0.2, signal-exit@^3.0.3: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== @@ -5016,6 +5111,11 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1: dependencies: ansi-regex "^5.0.1" +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + strip-hex-prefix@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/strip-hex-prefix/-/strip-hex-prefix-1.0.0.tgz#0c5f155fef1151373377de9dbb588da05500e36f" From c5520f74cfe2e115403979f447e2c47130308e95 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Mon, 9 Feb 2026 13:19:12 +0000 Subject: [PATCH 11/44] refactor: remove obvious comments and add named constants - Remove redundant comments that state the obvious - Add named constants for ArbOS precompiles (ARB_OWNER_PUBLIC, ARB_SYS) - Add ARBOS_VERSION_OFFSET constant with explanation - Remove unused AuthArgs interface - Remove unused getRepoRoot import - Fix double findRepoRoot() call in loadEnv() --- src/cli/commands/arbos-upgrade.ts | 36 +++++++++++++--------------- src/cli/commands/contract-upgrade.ts | 15 ++---------- src/cli/index.ts | 6 +---- src/cli/router.ts | 19 --------------- src/cli/utils/auth.ts | 21 ---------------- src/cli/utils/env.ts | 31 ++---------------------- src/cli/utils/forge.ts | 25 ------------------- src/cli/utils/log.ts | 10 -------- 8 files changed, 21 insertions(+), 142 deletions(-) diff --git a/src/cli/commands/arbos-upgrade.ts b/src/cli/commands/arbos-upgrade.ts index 6724cacf..04e41444 100644 --- a/src/cli/commands/arbos-upgrade.ts +++ b/src/cli/commands/arbos-upgrade.ts @@ -1,7 +1,3 @@ -/** - * ArbOS upgrade commands - */ - import { Command } from 'commander'; import * as path from 'path'; import * as fs from 'fs'; @@ -20,6 +16,13 @@ import { const ARBOS_DIR = path.join(getScriptsDir(), 'arbos-upgrades', 'at-timestamp'); const DEPLOY_SCRIPT = path.join(ARBOS_DIR, 'DeployUpgradeArbOSVersionAtTimestampAction.s.sol'); +// ArbOS precompile addresses +const ARB_OWNER_PUBLIC = '0x000000000000000000000000000000000000006b'; +const ARB_SYS = '0x0000000000000000000000000000000000000064'; + +// Nitro ArbOS versions are offset by 55 to avoid collision with classic (pre-Nitro) versions +const ARBOS_VERSION_OFFSET = 55; + async function cmdDeploy(version: string, args: string[]): Promise { const authArgs = parseAuthArgs(args); @@ -29,7 +32,7 @@ async function cmdDeploy(version: string, args: string[]): Promise { die(`Deploy script not found: ${DEPLOY_SCRIPT}`); } - // Export version for the script + // Forge script reads this from env process.env.ARBOS_VERSION = version; log(`Running: ${path.basename(DEPLOY_SCRIPT)} for ArbOS ${version}`); @@ -83,14 +86,14 @@ async function cmdVerify(): Promise { log('Checking ArbOS upgrade status...'); const scheduled = await runCastCall({ - to: '0x000000000000000000000000000000000000006b', + to: ARB_OWNER_PUBLIC, sig: 'getScheduledUpgrade()(uint64,uint64)', rpcUrl, }); log(`Scheduled upgrade (version, timestamp): ${scheduled}`); const currentRaw = await runCastCall({ - to: '0x0000000000000000000000000000000000000064', + to: ARB_SYS, sig: 'arbOSVersion()(uint64)', rpcUrl, }); @@ -100,7 +103,7 @@ async function cmdVerify(): Promise { currentVersion = 0; } else { const rawNum = parseInt(currentRaw, 10); - currentVersion = rawNum - 55; + currentVersion = rawNum - ARBOS_VERSION_OFFSET; } log(`Current ArbOS version: ${currentVersion}`); @@ -138,15 +141,13 @@ async function cmdDeployExecuteVerify( log(`Using existing action from .env: ${upgradeActionAddress}`); } - // Validate required env vars const rpcUrl = requireEnv('CHILD_CHAIN_RPC'); const upgradeExecutor = requireEnv('CHILD_UPGRADE_EXECUTOR_ADDRESS'); requireEnv('SCHEDULE_TIMESTAMP'); - // Export version for the script + // Forge script reads this from env process.env.ARBOS_VERSION = version; - // Validate auth const deployAuth = getDeployAuth(auth); const executeAuth = getExecuteAuth(auth); @@ -159,7 +160,6 @@ async function cmdDeployExecuteVerify( log(`Scheduled timestamp: ${process.env.SCHEDULE_TIMESTAMP}`); - // Step 1: Deploy let chainId = ''; if (!skipDeploy) { chainId = await getChainId(rpcUrl); @@ -189,11 +189,10 @@ async function cmdDeployExecuteVerify( log('Step 1: Skipped deploy'); } - // Step 2: Execute via cast send if (!auth.skipExecute) { log('Step 2: Executing ArbOS upgrade...'); - const performCalldata = '0xb0a75d36'; + const performCalldata = '0xb0a75d36'; // perform() selector if (auth.dryRun) { const executeCalldata = await castCalldata('execute(address,bytes)', upgradeActionAddress, performCalldata); @@ -219,19 +218,18 @@ async function cmdDeployExecuteVerify( log('Step 2: Skipped execute'); } - // Step 3: Verify if (!auth.dryRun && !auth.skipExecute) { log('Step 3: Verifying scheduled upgrade...'); const scheduled = await runCastCall({ - to: '0x000000000000000000000000000000000000006b', + to: ARB_OWNER_PUBLIC, sig: 'getScheduledUpgrade()(uint64,uint64)', rpcUrl, }); log(`Scheduled upgrade (version, timestamp): ${scheduled}`); const currentRaw = await runCastCall({ - to: '0x0000000000000000000000000000000000000064', + to: ARB_SYS, sig: 'arbOSVersion()(uint64)', rpcUrl, }); @@ -241,7 +239,7 @@ async function cmdDeployExecuteVerify( currentVersion = 0; } else { const rawNum = parseInt(currentRaw, 10); - currentVersion = rawNum - 55; + currentVersion = rawNum - ARBOS_VERSION_OFFSET; } log(`Current ArbOS version: ${currentVersion}`); @@ -271,7 +269,6 @@ export function createArbosUpgradeCommand(): Command { .option('--skip-execute', 'Deploy only') .option('-v, --verify', 'Verify on block explorer') .action(async (version: string, command: string, options) => { - // Build args array from remaining options for simple commands const args: string[] = []; if (options.privateKey) args.push('--private-key', options.privateKey); if (options.account) args.push('--account', options.account); @@ -299,5 +296,4 @@ export function createArbosUpgradeCommand(): Command { return cmd; } -// Export individual functions for use by router export { cmdDeploy, cmdExecute, cmdVerify, cmdDeployExecuteVerify }; diff --git a/src/cli/commands/contract-upgrade.ts b/src/cli/commands/contract-upgrade.ts index 7aa66d17..fc88618d 100644 --- a/src/cli/commands/contract-upgrade.ts +++ b/src/cli/commands/contract-upgrade.ts @@ -1,12 +1,8 @@ -/** - * Contract upgrade commands - */ - import { Command } from 'commander'; import * as path from 'path'; import * as fs from 'fs'; import { log, die } from '../utils/log'; -import { requireEnv, getEnv, getScriptsDir, getRepoRoot } from '../utils/env'; +import { requireEnv, getEnv, getScriptsDir } from '../utils/env'; import { parseAuthArgs, createDeployExecuteAuth, getDeployAuth, getExecuteAuth } from '../utils/auth'; import { runForgeScript, getChainId, parseActionAddress, findScript } from '../utils/forge'; @@ -127,13 +123,11 @@ async function cmdDeployExecuteVerify( log(`Using existing action from .env: ${upgradeActionAddress}`); } - // Validate required env vars const rpcUrl = requireEnv('PARENT_CHAIN_RPC'); requireEnv('INBOX_ADDRESS'); requireEnv('PROXY_ADMIN_ADDRESS'); requireEnv('PARENT_UPGRADE_EXECUTOR_ADDRESS'); - // Validate auth const deployAuth = getDeployAuth(auth); const executeAuth = getExecuteAuth(auth); @@ -147,7 +141,6 @@ async function cmdDeployExecuteVerify( const chainId = await getChainId(rpcUrl); log(`Target chain ID: ${chainId}`); - // Step 1: Deploy if (!skipDeploy) { log('Step 1: Deploying upgrade action...'); @@ -175,10 +168,9 @@ async function cmdDeployExecuteVerify( log('Step 1: Skipped deploy'); } - // Export for execute script + // Forge script reads this from env process.env.UPGRADE_ACTION_ADDRESS = upgradeActionAddress; - // Step 2: Execute if (!auth.skipExecute) { log('Step 2: Executing upgrade...'); @@ -198,7 +190,6 @@ async function cmdDeployExecuteVerify( log('Step 2: Skipped execute'); } - // Step 3: Verify if (!auth.dryRun && !auth.skipExecute) { log('Step 3: Verifying upgrade...'); @@ -237,7 +228,6 @@ export function createContractUpgradeCommand(): Command { .option('--skip-execute', 'Deploy only') .option('-v, --verify', 'Verify on block explorer') .action(async (version: string, command: string, options) => { - // Build args array from remaining options for simple commands const args: string[] = []; if (options.privateKey) args.push('--private-key', options.privateKey); if (options.account) args.push('--account', options.account); @@ -265,5 +255,4 @@ export function createContractUpgradeCommand(): Command { return cmd; } -// Export individual functions for use by router export { cmdDeploy, cmdExecute, cmdVerify, cmdDeployExecuteVerify, getVersionDir }; diff --git a/src/cli/index.ts b/src/cli/index.ts index 33b02d1a..5f97048f 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -1,7 +1,4 @@ #!/usr/bin/env node -/** - * orbit-actions CLI entry point - */ import { program } from 'commander'; import { loadEnv } from './utils/env'; @@ -9,7 +6,6 @@ import { router } from './router'; import { createContractUpgradeCommand } from './commands/contract-upgrade'; import { createArbosUpgradeCommand } from './commands/arbos-upgrade'; -// Load .env from repo root (or /app in Docker) loadEnv(); program @@ -22,8 +18,8 @@ program await router(pathArg, args); }); -// Register subcommands for direct invocation program.addCommand(createContractUpgradeCommand()); program.addCommand(createArbosUpgradeCommand()); program.parse(); + diff --git a/src/cli/router.ts b/src/cli/router.ts index 50fb2870..d8a9617a 100644 --- a/src/cli/router.ts +++ b/src/cli/router.ts @@ -1,8 +1,3 @@ -/** - * Path routing for CLI - * Handles directory listing, file viewing, and command dispatch - */ - import * as fs from 'fs'; import * as path from 'path'; import { die } from './utils/log'; @@ -67,16 +62,13 @@ function listDirectory(dir: string): void { let relPath = path.relative(scriptsDir, dir); if (relPath === '.') relPath = ''; - // List actual contents const contents = fs.readdirSync(dir); for (const item of contents) { - // Skip hidden files if (!item.startsWith('.')) { console.log(item); } } - // Add virtual commands based on directory type if (/^contract-upgrades\/[0-9]/.test(relPath)) { console.log('---'); console.log('deploy (run Deploy script)'); @@ -128,7 +120,6 @@ function parseOptions(args: string[]): Record { export async function router(pathArg?: string, args: string[] = []): Promise { const scriptsDir = getScriptsDir(); - // No args - list top level if (!pathArg) { const contents = fs.readdirSync(scriptsDir); for (const item of contents) { @@ -139,23 +130,18 @@ export async function router(pathArg?: string, args: string[] = []): Promise p !== null); @@ -45,10 +32,6 @@ export function loadEnv(): void { } } -/** - * Require an environment variable to be set - * @throws Exits process if variable is not set - */ export function requireEnv(name: string): string { const value = process.env[name]; if (!value) { @@ -57,28 +40,18 @@ export function requireEnv(name: string): string { return value; } -/** - * Get an optional environment variable - */ export function getEnv(name: string): string | undefined { return process.env[name]; } -/** - * Get the scripts directory path - */ export function getScriptsDir(): string { const repoRoot = findRepoRoot(); if (repoRoot) { return path.join(repoRoot, 'scripts', 'foundry'); } - // Fallback for Docker return '/app/scripts/foundry'; } -/** - * Get the repository root path - */ export function getRepoRoot(): string { return findRepoRoot() || '/app'; } diff --git a/src/cli/utils/forge.ts b/src/cli/utils/forge.ts index c3cb8a15..eaac011b 100644 --- a/src/cli/utils/forge.ts +++ b/src/cli/utils/forge.ts @@ -1,7 +1,3 @@ -/** - * Forge/Cast command execution utilities - */ - import execa from 'execa'; import * as fs from 'fs'; import * as path from 'path'; @@ -19,9 +15,6 @@ export interface ForgeScriptOptions { verbosity?: number; } -/** - * Run a forge script command - */ export async function runForgeScript(options: ForgeScriptOptions): Promise { const args = ['script', options.script, '--rpc-url', options.rpcUrl]; @@ -65,9 +58,6 @@ export interface CastSendOptions { authArgs?: string; } -/** - * Run a cast send command - */ export async function runCastSend(options: CastSendOptions): Promise { const args = ['send', options.to, options.sig, ...options.args, '--rpc-url', options.rpcUrl]; @@ -91,9 +81,6 @@ export interface CastCallOptions { rpcUrl: string; } -/** - * Run a cast call command and return the result - */ export async function runCastCall(options: CastCallOptions): Promise { try { const result = await execa('cast', ['call', '--rpc-url', options.rpcUrl, options.to, options.sig]); @@ -103,25 +90,16 @@ export async function runCastCall(options: CastCallOptions): Promise { } } -/** - * Generate calldata using cast - */ export async function castCalldata(sig: string, ...args: string[]): Promise { const result = await execa('cast', ['calldata', sig, ...args]); return result.stdout; } -/** - * Get chain ID from RPC URL - */ export async function getChainId(rpcUrl: string): Promise { const result = await execa('cast', ['chain-id', '--rpc-url', rpcUrl]); return result.stdout.trim(); } -/** - * Parse the deployed action address from forge broadcast file - */ export function parseActionAddress(scriptPath: string, chainId: string): string { const scriptName = path.basename(scriptPath); const repoRoot = getRepoRoot(); @@ -148,9 +126,6 @@ export function parseActionAddress(scriptPath: string, chainId: string): string return address; } -/** - * Find a script file by pattern in a directory - */ export function findScript(dir: string, pattern: RegExp): string | null { if (!fs.existsSync(dir)) { return null; diff --git a/src/cli/utils/log.ts b/src/cli/utils/log.ts index 1286c19a..9df2a6c6 100644 --- a/src/cli/utils/log.ts +++ b/src/cli/utils/log.ts @@ -1,19 +1,9 @@ -/** - * Logging utilities for CLI - */ - const PREFIX = '[orbit-actions]'; -/** - * Log an informational message to stdout - */ export function log(message: string): void { console.log(`${PREFIX} ${message}`); } -/** - * Log an error message and exit with code 1 - */ export function die(message: string): never { console.error(`Error: ${message}`); process.exit(1); From c93b86f050eed02588c3ad5f0c57015d118aea7f Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Mon, 9 Feb 2026 15:02:11 +0000 Subject: [PATCH 12/44] fix: update docker tests and eslint config for TS CLI - Remove deprecated @typescript-eslint/tslint plugin from eslint config - Update docker tests to use --entrypoint for tool access - Apply prettier formatting to src/cli files - Fix no-implicit-coercion lint errors (!! -> Boolean()) --- .eslintrc.js | 8 +- .../creator-upgrades/1.2.1/output/1.json | 2 +- .../creator-upgrades/1.2.1/output/1337.json | 2 +- .../creator-upgrades/1.2.1/output/42161.json | 2 +- .../creator-upgrades/1.2.2/output/1.json | 2 +- .../1.2.2/output/11155111.json | 2 +- .../creator-upgrades/1.2.2/output/42161.json | 2 +- .../creator-upgrades/1.2.2/output/421614.json | 2 +- .../creator-upgrades/1.2.2/output/42170.json | 2 +- .../creator-upgrades/1.2.2/output/8453.json | 2 +- src/cli/commands/arbos-upgrade.ts | 272 ++++++++++-------- src/cli/commands/contract-upgrade.ts | 244 +++++++++------- src/cli/index.ts | 23 +- src/cli/router.ts | 168 ++++++----- src/cli/utils/auth.ts | 76 ++--- src/cli/utils/env.ts | 44 +-- src/cli/utils/forge.ts | 148 ++++++---- src/cli/utils/log.ts | 8 +- test/docker/test-docker.bash | 22 +- 19 files changed, 559 insertions(+), 472 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 23d5ab1a..cd3e7f3d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -60,7 +60,7 @@ module.exports = { 'plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended', ], - plugins: ['@typescript-eslint', 'prettier', '@typescript-eslint/tslint'], + plugins: ['@typescript-eslint', 'prettier'], rules: { 'no-empty-pattern': 'warn', 'prettier/prettier': ['error', { singleQuote: true }], @@ -77,12 +77,6 @@ module.exports = { caughtErrorsIgnorePattern: '^_', }, ], - '@typescript-eslint/tslint/config': [ - 'error', - { - rules: { 'strict-comparisons': true }, - }, - ], 'no-implicit-coercion': 'error', '@typescript-eslint/no-shadow': ['error'], }, diff --git a/scripts/foundry/creator-upgrades/1.2.1/output/1.json b/scripts/foundry/creator-upgrades/1.2.1/output/1.json index d5cfb46c..bf86fbb5 100644 --- a/scripts/foundry/creator-upgrades/1.2.1/output/1.json +++ b/scripts/foundry/creator-upgrades/1.2.1/output/1.json @@ -5,4 +5,4 @@ "updateBridgeErc20TemplatesCalldata": "0x1bb7c6cc0000000000000000000000007efcb76d0e2e776a298aaa603d433336e5f8b6ab000000000000000000000000383f16fb2809a56fc639c1ee2c93ad2aa7ee130a00000000000000000000000031faaab44e74eb408d1fc69a14806b4b9ca09da2000000000000000000000000302275067251f5fcdb9359bda735fd8f7a4a54c000000000000000000000000019431dc37098877486532250fb3158140717c00c", "updateBridgeEthTemplatesCalldata": "0xd94d6e0a0000000000000000000000001c6accd9d66f3b993928e7439c9a2d67b94a445f000000000000000000000000958985cf2c54f99ba4a599221a8090c1f9cee9a50000000000000000000000001162084c3c6575121146582db5be43189e8cee6b00000000000000000000000013be515e44eefaf3ebefad684f1fbb574ac0a4940000000000000000000000002a6dd4433ffa96dc1755814fc0d9cc83a5f68dec", "updateRollupCreatorTemplatesCalldata": "0xac9a97b40000000000000000000000001135265fe014d3fa32b3507e325642b92affeaeb00000000000000000000000057ea090ac0554d174ae0e2855b460e84a1a7c2210000000000000000000000001d901dd7a5efe421c3c437b147040e5af22e6a430000000000000000000000000ae4dd666748bf0f6db5c149eab1d8ad27820a6a000000000000000000000000660ea1675f7323dc3ba0c8ddfb593225eb01e3c10000000000000000000000006c21303f5986180b1394d2c89f3e883890e2867b0000000000000000000000002b0e04dc90e3fa58165cb41e2834b44a56e766af0000000000000000000000009cad81628ab7d8e239f1a5b497313341578c5f710000000000000000000000002e31291fa573db3dfeae00c9bd1806b73c7185c8" -} \ No newline at end of file +} diff --git a/scripts/foundry/creator-upgrades/1.2.1/output/1337.json b/scripts/foundry/creator-upgrades/1.2.1/output/1337.json index 785a6d5d..22ad429a 100644 --- a/scripts/foundry/creator-upgrades/1.2.1/output/1337.json +++ b/scripts/foundry/creator-upgrades/1.2.1/output/1337.json @@ -5,4 +5,4 @@ "updateBridgeErc20TemplatesCalldata": "0x1bb7c6cc0000000000000000000000004e5b65fb12d4165e22f5861d97a33ba45c006114000000000000000000000000457f2a773d9ebd5eadd5d014db162749a1ea92eb0000000000000000000000009df23e34ac13a7145eba1164660e701839197b1b0000000000000000000000009f1ece352ce8d540738ccb38aa3fa3d44d00a2590000000000000000000000000bdad990640a488400565fe6fb1d879ffe12da37", "updateBridgeEthTemplatesCalldata": "0xd94d6e0a000000000000000000000000217788c286797d56cd59af5e493f3699c39cbbe80000000000000000000000006ca66235758bccd08a4d1612662482f08fab93470000000000000000000000000f1f89aaf1c6fdb7ff9d361e4388f5f3997f12a800000000000000000000000060571c8f4b52954a24a5e7306d435e951528d963000000000000000000000000b075b82c7a23e0994df4793422a1f03dbcf9136f", "updateRollupCreatorTemplatesCalldata": "0xac9a97b40000000000000000000000005e36aa9caaf5f708fca5c04d2d4c776a62b2b2580000000000000000000000002766e96f90f9f027835e0c00c04c8119c635ce02000000000000000000000000037b11bb930dbb7c875ce459eeff69fc2e9fd40d0000000000000000000000009c2ed9f57d053fdfaecbf1b6dfd7c97e2e340b84000000000000000000000000f7ec0b16a45dc99ae21bfa8b4b737d1d61ca9fa4000000000000000000000000dfb681cc1f2c180c2131bb4deb46642d6258b0ff000000000000000000000000bd4cc2f69ffd94b5f62dcc5a27c2eb805093fc0d000000000000000000000000a80482dddb7f8b9dcc24a1cd13488e3379a1456800000000000000000000000092f58045ffb1c00a7b9486b9d2a55d316380cb45" -} \ No newline at end of file +} diff --git a/scripts/foundry/creator-upgrades/1.2.1/output/42161.json b/scripts/foundry/creator-upgrades/1.2.1/output/42161.json index 507add51..a9e3e652 100644 --- a/scripts/foundry/creator-upgrades/1.2.1/output/42161.json +++ b/scripts/foundry/creator-upgrades/1.2.1/output/42161.json @@ -5,4 +5,4 @@ "updateBridgeErc20TemplatesCalldata": "0x1bb7c6cc0000000000000000000000002a6dd4433ffa96dc1755814fc0d9cc83a5f68dec0000000000000000000000007a299ad29499736994aa3a9afa3f476445faeb2c0000000000000000000000007efcb76d0e2e776a298aaa603d433336e5f8b6ab00000000000000000000000018fd37a4fb9e1f06d9383958afd236771f15a8cb000000000000000000000000302275067251f5fcdb9359bda735fd8f7a4a54c0", "updateBridgeEthTemplatesCalldata": "0xd94d6e0a000000000000000000000000b23214f241bdeb275f7dcbfbb1ea79349101d4b000000000000000000000000018ed2d5bf7c5943bfd20a2995b9879e30c9e8dda0000000000000000000000008f6406781cc955398c45a48dcefeebdb2c8e2caa000000000000000000000000f40c24ba346aa459ed28e196d4a46cf17174bd6c00000000000000000000000013be515e44eefaf3ebefad684f1fbb574ac0a494", "updateRollupCreatorTemplatesCalldata": "0xac9a97b400000000000000000000000019431dc37098877486532250fb3158140717c00c000000000000000000000000b20107bfb36d3b5aca534acafbd8857b10b402a80000000000000000000000005ca988f213efbcb86ed7e2aacb0c15c91e648f8d000000000000000000000000ee9e5546a11cb5b4a86e92da05f2ef75c26e47540000000000000000000000000ae4dd666748bf0f6db5c149eab1d8ad27820a6a000000000000000000000000660ea1675f7323dc3ba0c8ddfb593225eb01e3c10000000000000000000000006c21303f5986180b1394d2c89f3e883890e2867b0000000000000000000000002b0e04dc90e3fa58165cb41e2834b44a56e766af00000000000000000000000090d68b056c411015eae3ec0b98ad94e2c91419f1" -} \ No newline at end of file +} diff --git a/scripts/foundry/creator-upgrades/1.2.2/output/1.json b/scripts/foundry/creator-upgrades/1.2.2/output/1.json index d5570972..3011dac7 100644 --- a/scripts/foundry/creator-upgrades/1.2.2/output/1.json +++ b/scripts/foundry/creator-upgrades/1.2.2/output/1.json @@ -3,4 +3,4 @@ "creatorCalldata": "0x99a88ec400000000000000000000000060d9a46f24d5a35b95a78dd3e793e55d94ee0660000000000000000000000000f39a8a43cffa0513cc057d290fa3e7a57dcd8d46", "retryableSenderCalldata": "0x99a88ec4000000000000000000000000a96b7f9e20a1f6e11815d4af08d911b21cb380ec0000000000000000000000008de0fb2651fdd10975088ae61a71cac6d372063d", "to": "0xE60081476E505F14C231a7efa47e607ff50dAEB5" -} \ No newline at end of file +} diff --git a/scripts/foundry/creator-upgrades/1.2.2/output/11155111.json b/scripts/foundry/creator-upgrades/1.2.2/output/11155111.json index d2a92dd6..34103571 100644 --- a/scripts/foundry/creator-upgrades/1.2.2/output/11155111.json +++ b/scripts/foundry/creator-upgrades/1.2.2/output/11155111.json @@ -3,4 +3,4 @@ "creatorCalldata": "0x99a88ec40000000000000000000000007edb2dfbeef9417e0454a80c51ee0c034e45a570000000000000000000000000757143a7ed0dc76499607c7e5b0771965ae1fe06", "retryableSenderCalldata": "0x99a88ec40000000000000000000000002e9da5298ce57818caf96735bdcf900215c25d060000000000000000000000005d0e7fd5fca46aca13e475c070aa3e2f8eb01925", "to": "0xE58B76B21A98334CFD7FD6757102efe029E62Ed0" -} \ No newline at end of file +} diff --git a/scripts/foundry/creator-upgrades/1.2.2/output/42161.json b/scripts/foundry/creator-upgrades/1.2.2/output/42161.json index f213f5ec..d6db95b3 100644 --- a/scripts/foundry/creator-upgrades/1.2.2/output/42161.json +++ b/scripts/foundry/creator-upgrades/1.2.2/output/42161.json @@ -3,4 +3,4 @@ "creatorCalldata": "0x99a88ec40000000000000000000000002f5624dc8800dfa0a82ac03509ef8bb8e7ac000e00000000000000000000000052d5181dd67ac17176127e670e5baee4d47c6c9e", "retryableSenderCalldata": "0x99a88ec4000000000000000000000000f9fbfc857d51ff51fedd4ea88efc29039871dccf0000000000000000000000009ea06b8753bca071a5c57002ab84598577fb08c1", "to": "0xBE95d0EE267f3E90606537b1C8A6Fb36d2DC1Ce6" -} \ No newline at end of file +} diff --git a/scripts/foundry/creator-upgrades/1.2.2/output/421614.json b/scripts/foundry/creator-upgrades/1.2.2/output/421614.json index 4e5695c1..3418ce2e 100644 --- a/scripts/foundry/creator-upgrades/1.2.2/output/421614.json +++ b/scripts/foundry/creator-upgrades/1.2.2/output/421614.json @@ -3,4 +3,4 @@ "creatorCalldata": "0x99a88ec400000000000000000000000056c486d3786fa26cc61473c499a36eb9cc1fbd8e000000000000000000000000ec43416728f656ac5b7d860236bb102e2abe0f88", "retryableSenderCalldata": "0x99a88ec4000000000000000000000000a665705700774a40b73b4e3509ae01c7ef05ba0f0000000000000000000000009308a1264ab831002821971ac5fa342c4f775637", "to": "0x8E112dd87E71Ac9061caA2ccC2513027C3cF5D90" -} \ No newline at end of file +} diff --git a/scripts/foundry/creator-upgrades/1.2.2/output/42170.json b/scripts/foundry/creator-upgrades/1.2.2/output/42170.json index 7cee8514..b4895a0e 100644 --- a/scripts/foundry/creator-upgrades/1.2.2/output/42170.json +++ b/scripts/foundry/creator-upgrades/1.2.2/output/42170.json @@ -3,4 +3,4 @@ "creatorCalldata": "0x99a88ec40000000000000000000000008b9d9490a68b1f16ac8a21ddae5fd7ab9d708c140000000000000000000000004998b99dd376a0cfff0e4b7f1ee0056f79910e64", "retryableSenderCalldata": "0x99a88ec4000000000000000000000000841a23c7c4e20515eaf03debd8ab60f12b5cc13e000000000000000000000000e60081476e505f14c231a7efa47e607ff50daeb5", "to": "0x211A5579D21e1938b2B5ff87a3F7896933543E97" -} \ No newline at end of file +} diff --git a/scripts/foundry/creator-upgrades/1.2.2/output/8453.json b/scripts/foundry/creator-upgrades/1.2.2/output/8453.json index 97838476..e74451f7 100644 --- a/scripts/foundry/creator-upgrades/1.2.2/output/8453.json +++ b/scripts/foundry/creator-upgrades/1.2.2/output/8453.json @@ -3,4 +3,4 @@ "creatorCalldata": "0x99a88ec40000000000000000000000004c240987d6fe4fa8c7a0004986e3db563150ca550000000000000000000000004f45074375ac881094003fecebac1f12169ffd96", "retryableSenderCalldata": "0x99a88ec4000000000000000000000000d106ec93d2c1adaa65c4b17ffc7bb166ce30ddae00000000000000000000000052767940bdbac734116fb2a2effeba01bfa82124", "to": "0x413Aa082995f0D7672C4d564624DdeBD221C8D0D" -} \ No newline at end of file +} diff --git a/src/cli/commands/arbos-upgrade.ts b/src/cli/commands/arbos-upgrade.ts index 04e41444..4fa9e229 100644 --- a/src/cli/commands/arbos-upgrade.ts +++ b/src/cli/commands/arbos-upgrade.ts @@ -1,9 +1,14 @@ -import { Command } from 'commander'; -import * as path from 'path'; -import * as fs from 'fs'; -import { log, die } from '../utils/log'; -import { requireEnv, getEnv, getScriptsDir } from '../utils/env'; -import { parseAuthArgs, createDeployExecuteAuth, getDeployAuth, getExecuteAuth } from '../utils/auth'; +import { Command } from 'commander' +import * as path from 'path' +import * as fs from 'fs' +import { log, die } from '../utils/log' +import { requireEnv, getEnv, getScriptsDir } from '../utils/env' +import { + parseAuthArgs, + createDeployExecuteAuth, + getDeployAuth, + getExecuteAuth, +} from '../utils/auth' import { runForgeScript, runCastSend, @@ -11,62 +16,69 @@ import { castCalldata, getChainId, parseActionAddress, -} from '../utils/forge'; +} from '../utils/forge' -const ARBOS_DIR = path.join(getScriptsDir(), 'arbos-upgrades', 'at-timestamp'); -const DEPLOY_SCRIPT = path.join(ARBOS_DIR, 'DeployUpgradeArbOSVersionAtTimestampAction.s.sol'); +const ARBOS_DIR = path.join(getScriptsDir(), 'arbos-upgrades', 'at-timestamp') +const DEPLOY_SCRIPT = path.join( + ARBOS_DIR, + 'DeployUpgradeArbOSVersionAtTimestampAction.s.sol' +) // ArbOS precompile addresses -const ARB_OWNER_PUBLIC = '0x000000000000000000000000000000000000006b'; -const ARB_SYS = '0x0000000000000000000000000000000000000064'; +const ARB_OWNER_PUBLIC = '0x000000000000000000000000000000000000006b' +const ARB_SYS = '0x0000000000000000000000000000000000000064' // Nitro ArbOS versions are offset by 55 to avoid collision with classic (pre-Nitro) versions -const ARBOS_VERSION_OFFSET = 55; +const ARBOS_VERSION_OFFSET = 55 async function cmdDeploy(version: string, args: string[]): Promise { - const authArgs = parseAuthArgs(args); + const authArgs = parseAuthArgs(args) - const rpcUrl = requireEnv('CHILD_CHAIN_RPC'); + const rpcUrl = requireEnv('CHILD_CHAIN_RPC') if (!fs.existsSync(DEPLOY_SCRIPT)) { - die(`Deploy script not found: ${DEPLOY_SCRIPT}`); + die(`Deploy script not found: ${DEPLOY_SCRIPT}`) } // Forge script reads this from env - process.env.ARBOS_VERSION = version; + process.env.ARBOS_VERSION = version - log(`Running: ${path.basename(DEPLOY_SCRIPT)} for ArbOS ${version}`); + log(`Running: ${path.basename(DEPLOY_SCRIPT)} for ArbOS ${version}`) await runForgeScript({ script: DEPLOY_SCRIPT, rpcUrl, authArgs, - broadcast: !!authArgs, + broadcast: Boolean(authArgs), slow: true, - }); + }) } async function cmdExecute(args: string[]): Promise { - const authArgs = parseAuthArgs(args); + const authArgs = parseAuthArgs(args) - const rpcUrl = requireEnv('CHILD_CHAIN_RPC'); - const upgradeExecutor = requireEnv('CHILD_UPGRADE_EXECUTOR_ADDRESS'); - const actionAddress = requireEnv('UPGRADE_ACTION_ADDRESS'); + const rpcUrl = requireEnv('CHILD_CHAIN_RPC') + const upgradeExecutor = requireEnv('CHILD_UPGRADE_EXECUTOR_ADDRESS') + const actionAddress = requireEnv('UPGRADE_ACTION_ADDRESS') - log(`Executing ArbOS upgrade action: ${actionAddress}`); + log(`Executing ArbOS upgrade action: ${actionAddress}`) - const performCalldata = '0xb0a75d36'; // perform() selector + const performCalldata = '0xb0a75d36' // perform() selector if (!authArgs) { // No auth - output calldata for multisig - const executeCalldata = await castCalldata('execute(address,bytes)', actionAddress, performCalldata); - - log('Calldata for UpgradeExecutor.execute():'); - console.log(''); - console.log(`To: ${upgradeExecutor}`); - console.log(`Calldata: ${executeCalldata}`); - console.log(''); - log('Submit this to your multisig/Safe to execute the upgrade'); + const executeCalldata = await castCalldata( + 'execute(address,bytes)', + actionAddress, + performCalldata + ) + + log('Calldata for UpgradeExecutor.execute():') + console.log('') + console.log(`To: ${upgradeExecutor}`) + console.log(`Calldata: ${executeCalldata}`) + console.log('') + log('Submit this to your multisig/Safe to execute the upgrade') } else { await runCastSend({ to: upgradeExecutor, @@ -74,97 +86,101 @@ async function cmdExecute(args: string[]): Promise { args: [actionAddress, performCalldata], rpcUrl, authArgs, - }); + }) - log('ArbOS upgrade scheduled successfully'); + log('ArbOS upgrade scheduled successfully') } } async function cmdVerify(): Promise { - const rpcUrl = requireEnv('CHILD_CHAIN_RPC'); + const rpcUrl = requireEnv('CHILD_CHAIN_RPC') - log('Checking ArbOS upgrade status...'); + log('Checking ArbOS upgrade status...') const scheduled = await runCastCall({ to: ARB_OWNER_PUBLIC, sig: 'getScheduledUpgrade()(uint64,uint64)', rpcUrl, - }); - log(`Scheduled upgrade (version, timestamp): ${scheduled}`); + }) + log(`Scheduled upgrade (version, timestamp): ${scheduled}`) const currentRaw = await runCastCall({ to: ARB_SYS, sig: 'arbOSVersion()(uint64)', rpcUrl, - }); + }) - let currentVersion: number; + let currentVersion: number if (currentRaw === 'N/A') { - currentVersion = 0; + currentVersion = 0 } else { - const rawNum = parseInt(currentRaw, 10); - currentVersion = rawNum - ARBOS_VERSION_OFFSET; + const rawNum = parseInt(currentRaw, 10) + currentVersion = rawNum - ARBOS_VERSION_OFFSET } - log(`Current ArbOS version: ${currentVersion}`); + log(`Current ArbOS version: ${currentVersion}`) } async function cmdDeployExecuteVerify( version: string, options: { - deployKey?: string; - deployAccount?: string; - deployLedger?: boolean; - deployInteractive?: boolean; - executeKey?: string; - executeAccount?: string; - executeLedger?: boolean; - executeInteractive?: boolean; - dryRun?: boolean; - skipExecute?: boolean; - verify?: boolean; + deployKey?: string + deployAccount?: string + deployLedger?: boolean + deployInteractive?: boolean + executeKey?: string + executeAccount?: string + executeLedger?: boolean + executeInteractive?: boolean + dryRun?: boolean + skipExecute?: boolean + verify?: boolean } ): Promise { - const auth = createDeployExecuteAuth(options); + const auth = createDeployExecuteAuth(options) - log(`ArbOS version: ${version}`); + log(`ArbOS version: ${version}`) if (!fs.existsSync(DEPLOY_SCRIPT)) { - die(`Deploy script not found: ${DEPLOY_SCRIPT}`); + die(`Deploy script not found: ${DEPLOY_SCRIPT}`) } // Auto-detect skip_deploy if UPGRADE_ACTION_ADDRESS is set - let skipDeploy = !!getEnv('UPGRADE_ACTION_ADDRESS'); - let upgradeActionAddress = getEnv('UPGRADE_ACTION_ADDRESS') || ''; + const skipDeploy = Boolean(getEnv('UPGRADE_ACTION_ADDRESS')) + let upgradeActionAddress = getEnv('UPGRADE_ACTION_ADDRESS') || '' if (skipDeploy) { - log(`Using existing action from .env: ${upgradeActionAddress}`); + log(`Using existing action from .env: ${upgradeActionAddress}`) } - const rpcUrl = requireEnv('CHILD_CHAIN_RPC'); - const upgradeExecutor = requireEnv('CHILD_UPGRADE_EXECUTOR_ADDRESS'); - requireEnv('SCHEDULE_TIMESTAMP'); + const rpcUrl = requireEnv('CHILD_CHAIN_RPC') + const upgradeExecutor = requireEnv('CHILD_UPGRADE_EXECUTOR_ADDRESS') + requireEnv('SCHEDULE_TIMESTAMP') // Forge script reads this from env - process.env.ARBOS_VERSION = version; + process.env.ARBOS_VERSION = version - const deployAuth = getDeployAuth(auth); - const executeAuth = getExecuteAuth(auth); + const deployAuth = getDeployAuth(auth) + const executeAuth = getExecuteAuth(auth) if (!skipDeploy && !auth.dryRun && !deployAuth) { - die('Deploy auth required. Use --deploy-key, --deploy-account, --deploy-ledger, or --deploy-interactive'); + die( + 'Deploy auth required. Use --deploy-key, --deploy-account, --deploy-ledger, or --deploy-interactive' + ) } if (!auth.skipExecute && !auth.dryRun && !executeAuth) { - die('Execute auth required. Use --execute-key, --execute-account, --execute-ledger, or --execute-interactive'); + die( + 'Execute auth required. Use --execute-key, --execute-account, --execute-ledger, or --execute-interactive' + ) } - log(`Scheduled timestamp: ${process.env.SCHEDULE_TIMESTAMP}`); + log(`Scheduled timestamp: ${process.env.SCHEDULE_TIMESTAMP}`) - let chainId = ''; + let chainId = '' if (!skipDeploy) { - chainId = await getChainId(rpcUrl); - log(`Target chain ID: ${chainId}`); - log('Step 1: Deploying ArbOS upgrade action...'); + chainId = await getChainId(rpcUrl) + log(`Target chain ID: ${chainId}`) + log('Step 1: Deploying ArbOS upgrade action...') await runForgeScript({ script: DEPLOY_SCRIPT, @@ -173,36 +189,40 @@ async function cmdDeployExecuteVerify( broadcast: !auth.dryRun, verify: auth.verifyContracts, slow: true, - }); + }) if (!auth.dryRun) { - upgradeActionAddress = parseActionAddress(DEPLOY_SCRIPT, chainId); - log(`Deployed action at: ${upgradeActionAddress}`); + upgradeActionAddress = parseActionAddress(DEPLOY_SCRIPT, chainId) + log(`Deployed action at: ${upgradeActionAddress}`) } else { - log('Dry run - no action deployed'); + log('Dry run - no action deployed') if (!auth.skipExecute) { - log('Note: Set UPGRADE_ACTION_ADDRESS in .env to run execute step'); - return; + log('Note: Set UPGRADE_ACTION_ADDRESS in .env to run execute step') + return } } } else { - log('Step 1: Skipped deploy'); + log('Step 1: Skipped deploy') } if (!auth.skipExecute) { - log('Step 2: Executing ArbOS upgrade...'); + log('Step 2: Executing ArbOS upgrade...') - const performCalldata = '0xb0a75d36'; // perform() selector + const performCalldata = '0xb0a75d36' // perform() selector if (auth.dryRun) { - const executeCalldata = await castCalldata('execute(address,bytes)', upgradeActionAddress, performCalldata); - - log('Dry run - calldata for UpgradeExecutor.execute():'); - console.log(''); - console.log(`To: ${upgradeExecutor}`); - console.log(`Calldata: ${executeCalldata}`); - console.log(''); - log('Submit this to your multisig/Safe to execute the upgrade'); + const executeCalldata = await castCalldata( + 'execute(address,bytes)', + upgradeActionAddress, + performCalldata + ) + + log('Dry run - calldata for UpgradeExecutor.execute():') + console.log('') + console.log(`To: ${upgradeExecutor}`) + console.log(`Calldata: ${executeCalldata}`) + console.log('') + log('Submit this to your multisig/Safe to execute the upgrade') } else { await runCastSend({ to: upgradeExecutor, @@ -210,49 +230,53 @@ async function cmdDeployExecuteVerify( args: [upgradeActionAddress, performCalldata], rpcUrl, authArgs: executeAuth, - }); + }) - log('ArbOS upgrade scheduled successfully'); + log('ArbOS upgrade scheduled successfully') } } else { - log('Step 2: Skipped execute'); + log('Step 2: Skipped execute') } if (!auth.dryRun && !auth.skipExecute) { - log('Step 3: Verifying scheduled upgrade...'); + log('Step 3: Verifying scheduled upgrade...') const scheduled = await runCastCall({ to: ARB_OWNER_PUBLIC, sig: 'getScheduledUpgrade()(uint64,uint64)', rpcUrl, - }); - log(`Scheduled upgrade (version, timestamp): ${scheduled}`); + }) + log(`Scheduled upgrade (version, timestamp): ${scheduled}`) const currentRaw = await runCastCall({ to: ARB_SYS, sig: 'arbOSVersion()(uint64)', rpcUrl, - }); + }) - let currentVersion: number; + let currentVersion: number if (currentRaw === 'N/A') { - currentVersion = 0; + currentVersion = 0 } else { - const rawNum = parseInt(currentRaw, 10); - currentVersion = rawNum - ARBOS_VERSION_OFFSET; + const rawNum = parseInt(currentRaw, 10) + currentVersion = rawNum - ARBOS_VERSION_OFFSET } - log(`Current ArbOS version: ${currentVersion}`); + log(`Current ArbOS version: ${currentVersion}`) } - log('Done'); + log('Done') } export function createArbosUpgradeCommand(): Command { const cmd = new Command('arbos-upgrade') .description('ArbOS upgrade operations') .argument('', 'ArbOS version number') - .argument('[command]', 'Command: deploy, execute, verify, deploy-execute-verify', 'deploy-execute-verify') + .argument( + '[command]', + 'Command: deploy, execute, verify, deploy-execute-verify', + 'deploy-execute-verify' + ) .option('--private-key ', 'Private key (for deploy/execute)') .option('--account ', 'Keystore account (for deploy/execute)') .option('--ledger', 'Use Ledger (for deploy/execute)') @@ -269,31 +293,33 @@ export function createArbosUpgradeCommand(): Command { .option('--skip-execute', 'Deploy only') .option('-v, --verify', 'Verify on block explorer') .action(async (version: string, command: string, options) => { - const args: string[] = []; - if (options.privateKey) args.push('--private-key', options.privateKey); - if (options.account) args.push('--account', options.account); - if (options.ledger) args.push('--ledger'); - if (options.interactive) args.push('--interactive'); + const args: string[] = [] + if (options.privateKey) args.push('--private-key', options.privateKey) + if (options.account) args.push('--account', options.account) + if (options.ledger) args.push('--ledger') + if (options.interactive) args.push('--interactive') switch (command) { case 'deploy': - await cmdDeploy(version, args); - break; + await cmdDeploy(version, args) + break case 'execute': - await cmdExecute(args); - break; + await cmdExecute(args) + break case 'verify': - await cmdVerify(); - break; + await cmdVerify() + break case 'deploy-execute-verify': - await cmdDeployExecuteVerify(version, options); - break; + await cmdDeployExecuteVerify(version, options) + break default: - die(`Unknown command: ${command}\n\nCommands: deploy, execute, verify, deploy-execute-verify`); + die( + `Unknown command: ${command}\n\nCommands: deploy, execute, verify, deploy-execute-verify` + ) } - }); + }) - return cmd; + return cmd } -export { cmdDeploy, cmdExecute, cmdVerify, cmdDeployExecuteVerify }; +export { cmdDeploy, cmdExecute, cmdVerify, cmdDeployExecuteVerify } diff --git a/src/cli/commands/contract-upgrade.ts b/src/cli/commands/contract-upgrade.ts index fc88618d..3d416573 100644 --- a/src/cli/commands/contract-upgrade.ts +++ b/src/cli/commands/contract-upgrade.ts @@ -1,148 +1,167 @@ -import { Command } from 'commander'; -import * as path from 'path'; -import * as fs from 'fs'; -import { log, die } from '../utils/log'; -import { requireEnv, getEnv, getScriptsDir } from '../utils/env'; -import { parseAuthArgs, createDeployExecuteAuth, getDeployAuth, getExecuteAuth } from '../utils/auth'; -import { runForgeScript, getChainId, parseActionAddress, findScript } from '../utils/forge'; - -const CONTRACTS_DIR = path.join(getScriptsDir(), 'contract-upgrades'); +import { Command } from 'commander' +import * as path from 'path' +import * as fs from 'fs' +import { log, die } from '../utils/log' +import { requireEnv, getEnv, getScriptsDir } from '../utils/env' +import { + parseAuthArgs, + createDeployExecuteAuth, + getDeployAuth, + getExecuteAuth, +} from '../utils/auth' +import { + runForgeScript, + getChainId, + parseActionAddress, + findScript, +} from '../utils/forge' + +const CONTRACTS_DIR = path.join(getScriptsDir(), 'contract-upgrades') function getVersionDir(version: string): string { - const versionDir = path.join(CONTRACTS_DIR, version); + const versionDir = path.join(CONTRACTS_DIR, version) if (!fs.existsSync(versionDir)) { const available = fs.existsSync(CONTRACTS_DIR) - ? fs.readdirSync(CONTRACTS_DIR).filter((f) => !f.startsWith('.')).join(' ') - : 'none found'; - die(`Unknown version: ${version}\n\nAvailable versions: ${available}`); + ? fs + .readdirSync(CONTRACTS_DIR) + .filter(f => !f.startsWith('.')) + .join(' ') + : 'none found' + die(`Unknown version: ${version}\n\nAvailable versions: ${available}`) } - return versionDir; + return versionDir } async function cmdDeploy(version: string, args: string[]): Promise { - const versionDir = getVersionDir(version); - const authArgs = parseAuthArgs(args); + const versionDir = getVersionDir(version) + const authArgs = parseAuthArgs(args) - const rpcUrl = requireEnv('PARENT_CHAIN_RPC'); + const rpcUrl = requireEnv('PARENT_CHAIN_RPC') - const deployScript = findScript(versionDir, /^Deploy.*\.s\.sol$/); + const deployScript = findScript(versionDir, /^Deploy.*\.s\.sol$/) if (!deployScript) { - die(`No deploy script found in ${versionDir}`); + die(`No deploy script found in ${versionDir}`) } - log(`Running: ${path.basename(deployScript)}`); + log(`Running: ${path.basename(deployScript)}`) await runForgeScript({ script: deployScript, rpcUrl, authArgs, - broadcast: !!authArgs, + broadcast: Boolean(authArgs), slow: true, skipSimulation: true, - }); + }) } async function cmdExecute(version: string, args: string[]): Promise { - const versionDir = getVersionDir(version); - const authArgs = parseAuthArgs(args); + const versionDir = getVersionDir(version) + const authArgs = parseAuthArgs(args) - const rpcUrl = requireEnv('PARENT_CHAIN_RPC'); - requireEnv('UPGRADE_ACTION_ADDRESS'); + const rpcUrl = requireEnv('PARENT_CHAIN_RPC') + requireEnv('UPGRADE_ACTION_ADDRESS') - const executeScript = findScript(versionDir, /^Execute.*\.s\.sol$/); + const executeScript = findScript(versionDir, /^Execute.*\.s\.sol$/) if (!executeScript) { - die(`No execute script found in ${versionDir}`); + die(`No execute script found in ${versionDir}`) } - log(`Running: ${path.basename(executeScript)}`); + log(`Running: ${path.basename(executeScript)}`) await runForgeScript({ script: executeScript, rpcUrl, authArgs, - broadcast: !!authArgs, - }); + broadcast: Boolean(authArgs), + }) } async function cmdVerify(version: string): Promise { - const versionDir = getVersionDir(version); + const versionDir = getVersionDir(version) - const rpcUrl = requireEnv('PARENT_CHAIN_RPC'); + const rpcUrl = requireEnv('PARENT_CHAIN_RPC') - const verifyScript = findScript(versionDir, /^Verify.*\.s\.sol$/); + const verifyScript = findScript(versionDir, /^Verify.*\.s\.sol$/) if (!verifyScript) { - die(`No verify script found in ${versionDir} - check README for manual verification`); + die( + `No verify script found in ${versionDir} - check README for manual verification` + ) } - log(`Running: ${path.basename(verifyScript)}`); + log(`Running: ${path.basename(verifyScript)}`) await runForgeScript({ script: verifyScript, rpcUrl, - }); + }) } async function cmdDeployExecuteVerify( version: string, options: { - deployKey?: string; - deployAccount?: string; - deployLedger?: boolean; - deployInteractive?: boolean; - executeKey?: string; - executeAccount?: string; - executeLedger?: boolean; - executeInteractive?: boolean; - dryRun?: boolean; - skipExecute?: boolean; - verify?: boolean; + deployKey?: string + deployAccount?: string + deployLedger?: boolean + deployInteractive?: boolean + executeKey?: string + executeAccount?: string + executeLedger?: boolean + executeInteractive?: boolean + dryRun?: boolean + skipExecute?: boolean + verify?: boolean } ): Promise { - const versionDir = getVersionDir(version); - const auth = createDeployExecuteAuth(options); + const versionDir = getVersionDir(version) + const auth = createDeployExecuteAuth(options) - const deployScript = findScript(versionDir, /^Deploy.*\.s\.sol$/); - const executeScript = findScript(versionDir, /^Execute.*\.s\.sol$/); + const deployScript = findScript(versionDir, /^Deploy.*\.s\.sol$/) + const executeScript = findScript(versionDir, /^Execute.*\.s\.sol$/) if (!deployScript) { - die(`No deploy script found in ${versionDir}`); + die(`No deploy script found in ${versionDir}`) } if (!executeScript) { - die(`No execute script found in ${versionDir}`); + die(`No execute script found in ${versionDir}`) } - log(`Version: ${version}`); - log(`Deploy script: ${path.basename(deployScript)}`); - log(`Execute script: ${path.basename(executeScript)}`); + log(`Version: ${version}`) + log(`Deploy script: ${path.basename(deployScript)}`) + log(`Execute script: ${path.basename(executeScript)}`) // Auto-detect skip_deploy if UPGRADE_ACTION_ADDRESS is set - let skipDeploy = !!getEnv('UPGRADE_ACTION_ADDRESS'); - let upgradeActionAddress = getEnv('UPGRADE_ACTION_ADDRESS') || ''; + const skipDeploy = Boolean(getEnv('UPGRADE_ACTION_ADDRESS')) + let upgradeActionAddress = getEnv('UPGRADE_ACTION_ADDRESS') || '' if (skipDeploy) { - log(`Using existing action from .env: ${upgradeActionAddress}`); + log(`Using existing action from .env: ${upgradeActionAddress}`) } - const rpcUrl = requireEnv('PARENT_CHAIN_RPC'); - requireEnv('INBOX_ADDRESS'); - requireEnv('PROXY_ADMIN_ADDRESS'); - requireEnv('PARENT_UPGRADE_EXECUTOR_ADDRESS'); + const rpcUrl = requireEnv('PARENT_CHAIN_RPC') + requireEnv('INBOX_ADDRESS') + requireEnv('PROXY_ADMIN_ADDRESS') + requireEnv('PARENT_UPGRADE_EXECUTOR_ADDRESS') - const deployAuth = getDeployAuth(auth); - const executeAuth = getExecuteAuth(auth); + const deployAuth = getDeployAuth(auth) + const executeAuth = getExecuteAuth(auth) if (!skipDeploy && !auth.dryRun && !deployAuth) { - die('Deploy auth required. Use --deploy-key, --deploy-account, --deploy-ledger, or --deploy-interactive'); + die( + 'Deploy auth required. Use --deploy-key, --deploy-account, --deploy-ledger, or --deploy-interactive' + ) } if (!auth.skipExecute && !auth.dryRun && !executeAuth) { - die('Execute auth required. Use --execute-key, --execute-account, --execute-ledger, or --execute-interactive'); + die( + 'Execute auth required. Use --execute-key, --execute-account, --execute-ledger, or --execute-interactive' + ) } - const chainId = await getChainId(rpcUrl); - log(`Target chain ID: ${chainId}`); + const chainId = await getChainId(rpcUrl) + log(`Target chain ID: ${chainId}`) if (!skipDeploy) { - log('Step 1: Deploying upgrade action...'); + log('Step 1: Deploying upgrade action...') await runForgeScript({ script: deployScript, @@ -152,66 +171,69 @@ async function cmdDeployExecuteVerify( verify: auth.verifyContracts, slow: true, skipSimulation: true, - }); + }) if (!auth.dryRun) { - upgradeActionAddress = parseActionAddress(deployScript, chainId); - log(`Deployed action at: ${upgradeActionAddress}`); + upgradeActionAddress = parseActionAddress(deployScript, chainId) + log(`Deployed action at: ${upgradeActionAddress}`) } else { - log('Dry run - no action deployed'); + log('Dry run - no action deployed') if (!auth.skipExecute) { - log('Note: Set UPGRADE_ACTION_ADDRESS in .env to run execute step'); - return; + log('Note: Set UPGRADE_ACTION_ADDRESS in .env to run execute step') + return } } } else { - log('Step 1: Skipped deploy'); + log('Step 1: Skipped deploy') } // Forge script reads this from env - process.env.UPGRADE_ACTION_ADDRESS = upgradeActionAddress; + process.env.UPGRADE_ACTION_ADDRESS = upgradeActionAddress if (!auth.skipExecute) { - log('Step 2: Executing upgrade...'); + log('Step 2: Executing upgrade...') await runForgeScript({ script: executeScript, rpcUrl, authArgs: executeAuth, broadcast: !auth.dryRun, - }); + }) if (auth.dryRun) { - log('Dry run - upgrade not executed'); + log('Dry run - upgrade not executed') } else { - log('Upgrade executed successfully'); + log('Upgrade executed successfully') } } else { - log('Step 2: Skipped execute'); + log('Step 2: Skipped execute') } if (!auth.dryRun && !auth.skipExecute) { - log('Step 3: Verifying upgrade...'); + log('Step 3: Verifying upgrade...') - const verifyScript = findScript(versionDir, /^Verify.*\.s\.sol$/); + const verifyScript = findScript(versionDir, /^Verify.*\.s\.sol$/) if (verifyScript) { await runForgeScript({ script: verifyScript, rpcUrl, - }); + }) } else { - log('No Verify script found - check README for manual verification'); + log('No Verify script found - check README for manual verification') } } - log('Done'); + log('Done') } export function createContractUpgradeCommand(): Command { const cmd = new Command('contract-upgrade') .description('Contract upgrade operations') .argument('', 'Contract version (e.g., 1.2.1)') - .argument('', 'Command: deploy, execute, verify, deploy-execute-verify') + .argument( + '', + 'Command: deploy, execute, verify, deploy-execute-verify' + ) .option('--private-key ', 'Private key (for deploy/execute)') .option('--account ', 'Keystore account (for deploy/execute)') .option('--ledger', 'Use Ledger (for deploy/execute)') @@ -228,31 +250,39 @@ export function createContractUpgradeCommand(): Command { .option('--skip-execute', 'Deploy only') .option('-v, --verify', 'Verify on block explorer') .action(async (version: string, command: string, options) => { - const args: string[] = []; - if (options.privateKey) args.push('--private-key', options.privateKey); - if (options.account) args.push('--account', options.account); - if (options.ledger) args.push('--ledger'); - if (options.interactive) args.push('--interactive'); + const args: string[] = [] + if (options.privateKey) args.push('--private-key', options.privateKey) + if (options.account) args.push('--account', options.account) + if (options.ledger) args.push('--ledger') + if (options.interactive) args.push('--interactive') switch (command) { case 'deploy': - await cmdDeploy(version, args); - break; + await cmdDeploy(version, args) + break case 'execute': - await cmdExecute(version, args); - break; + await cmdExecute(version, args) + break case 'verify': - await cmdVerify(version); - break; + await cmdVerify(version) + break case 'deploy-execute-verify': - await cmdDeployExecuteVerify(version, options); - break; + await cmdDeployExecuteVerify(version, options) + break default: - die(`Unknown command: ${command}\n\nCommands: deploy, execute, verify, deploy-execute-verify`); + die( + `Unknown command: ${command}\n\nCommands: deploy, execute, verify, deploy-execute-verify` + ) } - }); + }) - return cmd; + return cmd } -export { cmdDeploy, cmdExecute, cmdVerify, cmdDeployExecuteVerify, getVersionDir }; +export { + cmdDeploy, + cmdExecute, + cmdVerify, + cmdDeployExecuteVerify, + getVersionDir, +} diff --git a/src/cli/index.ts b/src/cli/index.ts index 5f97048f..0b009dc6 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -1,12 +1,12 @@ #!/usr/bin/env node -import { program } from 'commander'; -import { loadEnv } from './utils/env'; -import { router } from './router'; -import { createContractUpgradeCommand } from './commands/contract-upgrade'; -import { createArbosUpgradeCommand } from './commands/arbos-upgrade'; +import { program } from 'commander' +import { loadEnv } from './utils/env' +import { router } from './router' +import { createContractUpgradeCommand } from './commands/contract-upgrade' +import { createArbosUpgradeCommand } from './commands/arbos-upgrade' -loadEnv(); +loadEnv() program .name('orbit-actions') @@ -15,11 +15,10 @@ program .argument('[args...]', 'Additional arguments') .allowUnknownOption(true) .action(async (pathArg?: string, args?: string[]) => { - await router(pathArg, args); - }); + await router(pathArg, args) + }) -program.addCommand(createContractUpgradeCommand()); -program.addCommand(createArbosUpgradeCommand()); - -program.parse(); +program.addCommand(createContractUpgradeCommand()) +program.addCommand(createArbosUpgradeCommand()) +program.parse() diff --git a/src/cli/router.ts b/src/cli/router.ts index d8a9617a..606e9821 100644 --- a/src/cli/router.ts +++ b/src/cli/router.ts @@ -1,19 +1,19 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import { die } from './utils/log'; -import { getScriptsDir } from './utils/env'; +import * as fs from 'fs' +import * as path from 'path' +import { die } from './utils/log' +import { getScriptsDir } from './utils/env' import { cmdDeploy as contractDeploy, cmdExecute as contractExecute, cmdVerify as contractVerify, cmdDeployExecuteVerify as contractDeployExecuteVerify, -} from './commands/contract-upgrade'; +} from './commands/contract-upgrade' import { cmdDeploy as arbosDeploy, cmdExecute as arbosExecute, cmdVerify as arbosVerify, cmdDeployExecuteVerify as arbosDeployExecuteVerify, -} from './commands/arbos-upgrade'; +} from './commands/arbos-upgrade' const HELP_TEXT = `Usage: orbit-actions [path] [args...] @@ -55,110 +55,120 @@ Options for deploy-execute-verify: Examples: docker run orbit-actions contract-upgrades/1.2.1 docker run orbit-actions contract-upgrades/1.2.1/README.md - docker run -v $(pwd)/.env:/app/.env orbit-actions contract-upgrades/1.2.1/deploy-execute-verify --dry-run`; + docker run -v $(pwd)/.env:/app/.env orbit-actions contract-upgrades/1.2.1/deploy-execute-verify --dry-run` function listDirectory(dir: string): void { - const scriptsDir = getScriptsDir(); - let relPath = path.relative(scriptsDir, dir); - if (relPath === '.') relPath = ''; + const scriptsDir = getScriptsDir() + let relPath = path.relative(scriptsDir, dir) + if (relPath === '.') relPath = '' - const contents = fs.readdirSync(dir); + const contents = fs.readdirSync(dir) for (const item of contents) { if (!item.startsWith('.')) { - console.log(item); + console.log(item) } } if (/^contract-upgrades\/[0-9]/.test(relPath)) { - console.log('---'); - console.log('deploy (run Deploy script)'); - console.log('execute (run Execute script)'); - console.log('verify (run Verify script)'); - console.log('deploy-execute-verify (full upgrade flow)'); + console.log('---') + console.log('deploy (run Deploy script)') + console.log('execute (run Execute script)') + console.log('verify (run Verify script)') + console.log('deploy-execute-verify (full upgrade flow)') } else if (relPath === 'arbos-upgrades/at-timestamp') { - console.log('---'); - console.log('deploy (run Deploy script)'); - console.log('execute (execute upgrade action)'); - console.log('verify (check upgrade status)'); - console.log('deploy-execute-verify (full upgrade flow)'); + console.log('---') + console.log('deploy (run Deploy script)') + console.log('execute (execute upgrade action)') + console.log('verify (check upgrade status)') + console.log('deploy-execute-verify (full upgrade flow)') } } function parseOptions(args: string[]): Record { - const options: Record = {}; + const options: Record = {} for (let i = 0; i < args.length; i++) { - const arg = args[i]; - if (arg === '--deploy-key' || arg === '--execute-key' || arg === '--deploy-account' || arg === '--execute-account') { - options[arg.replace(/^--/, '').replace(/-([a-z])/g, (_, c) => c.toUpperCase())] = args[++i] || ''; + const arg = args[i] + if ( + arg === '--deploy-key' || + arg === '--execute-key' || + arg === '--deploy-account' || + arg === '--execute-account' + ) { + options[ + arg.replace(/^--/, '').replace(/-([a-z])/g, (_, c) => c.toUpperCase()) + ] = args[++i] || '' } else if (arg === '--deploy-ledger') { - options.deployLedger = true; + options.deployLedger = true } else if (arg === '--execute-ledger') { - options.executeLedger = true; + options.executeLedger = true } else if (arg === '--deploy-interactive') { - options.deployInteractive = true; + options.deployInteractive = true } else if (arg === '--execute-interactive') { - options.executeInteractive = true; + options.executeInteractive = true } else if (arg === '--dry-run' || arg === '-n') { - options.dryRun = true; + options.dryRun = true } else if (arg === '--skip-execute') { - options.skipExecute = true; + options.skipExecute = true } else if (arg === '--verify' || arg === '-v') { - options.verify = true; + options.verify = true } else if (arg === '--private-key') { - options.privateKey = args[++i] || ''; + options.privateKey = args[++i] || '' } else if (arg === '--account') { - options.account = args[++i] || ''; + options.account = args[++i] || '' } else if (arg === '--ledger') { - options.ledger = true; + options.ledger = true } else if (arg === '--interactive') { - options.interactive = true; + options.interactive = true } } - return options; + return options } -export async function router(pathArg?: string, args: string[] = []): Promise { - const scriptsDir = getScriptsDir(); +export async function router( + pathArg?: string, + args: string[] = [] +): Promise { + const scriptsDir = getScriptsDir() if (!pathArg) { - const contents = fs.readdirSync(scriptsDir); + const contents = fs.readdirSync(scriptsDir) for (const item of contents) { if (!item.startsWith('.')) { - console.log(item); + console.log(item) } } - return; + return } if (pathArg === 'help' || pathArg === '--help' || pathArg === '-h') { - console.log(HELP_TEXT); - return; + console.log(HELP_TEXT) + return } - const fullPath = path.join(scriptsDir, pathArg); - const parentPath = path.dirname(fullPath); - const basename = path.basename(pathArg); + const fullPath = path.join(scriptsDir, pathArg) + const parentPath = path.dirname(fullPath) + const basename = path.basename(pathArg) if (!fs.existsSync(fullPath) && fs.existsSync(parentPath)) { - const relParent = path.relative(scriptsDir, parentPath); + const relParent = path.relative(scriptsDir, parentPath) if (/^contract-upgrades\/[0-9]/.test(relParent)) { - const version = path.basename(relParent); - const options = parseOptions(args); + const version = path.basename(relParent) + const options = parseOptions(args) switch (basename) { case 'deploy': - await contractDeploy(version, args); - return; + await contractDeploy(version, args) + return case 'execute': - await contractExecute(version, args); - return; + await contractExecute(version, args) + return case 'verify': - await contractVerify(version); - return; + await contractVerify(version) + return case 'deploy-execute-verify': - await contractDeployExecuteVerify(version, options); - return; + await contractDeployExecuteVerify(version, options) + return } } @@ -166,43 +176,45 @@ export async function router(pathArg?: string, args: string[] = []): Promise [options]`); - process.exit(1); + console.error(`Error: ArbOS version required`) + console.error( + `Usage: arbos-upgrades/at-timestamp/${basename} [options]` + ) + process.exit(1) } - const restArgs = args.slice(1); - const restOptions = parseOptions(restArgs); + const restArgs = args.slice(1) + const restOptions = parseOptions(restArgs) if (basename === 'deploy') { - await arbosDeploy(version, restArgs); + await arbosDeploy(version, restArgs) } else { - await arbosDeployExecuteVerify(version, restOptions); + await arbosDeployExecuteVerify(version, restOptions) } - return; + return } case 'execute': - await arbosExecute(args); - return; + await arbosExecute(args) + return case 'verify': - await arbosVerify(); - return; + await arbosVerify() + return } } } if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) { - listDirectory(fullPath); - return; + listDirectory(fullPath) + return } if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) { - const content = fs.readFileSync(fullPath, 'utf-8'); - console.log(content); - return; + const content = fs.readFileSync(fullPath, 'utf-8') + console.log(content) + return } die(`Not found: ${pathArg} -Use 'help' to see available commands.`); +Use 'help' to see available commands.`) } diff --git a/src/cli/utils/auth.ts b/src/cli/utils/auth.ts index dca9a629..7e719674 100644 --- a/src/cli/utils/auth.ts +++ b/src/cli/utils/auth.ts @@ -1,77 +1,77 @@ export interface DeployExecuteAuth { - deployKey: string; - deployAccount: string; - deployLedger: boolean; - deployInteractive: boolean; - executeKey: string; - executeAccount: string; - executeLedger: boolean; - executeInteractive: boolean; - dryRun: boolean; - skipExecute: boolean; - verifyContracts: boolean; + deployKey: string + deployAccount: string + deployLedger: boolean + deployInteractive: boolean + executeKey: string + executeAccount: string + executeLedger: boolean + executeInteractive: boolean + dryRun: boolean + skipExecute: boolean + verifyContracts: boolean } export function parseAuthArgs(args: string[]): string { for (let i = 0; i < args.length; i++) { - const arg = args[i]; + const arg = args[i] if (arg === '--private-key' || arg === '--account') { - const value = args[i + 1]; + const value = args[i + 1] if (value) { - return `${arg} ${value}`; + return `${arg} ${value}` } } if (arg === '--ledger' || arg === '--interactive') { - return arg; + return arg } } - return ''; + return '' } export function getDeployAuth(auth: DeployExecuteAuth): string { if (auth.deployKey) { - return `--private-key ${auth.deployKey}`; + return `--private-key ${auth.deployKey}` } if (auth.deployAccount) { - return `--account ${auth.deployAccount}`; + return `--account ${auth.deployAccount}` } if (auth.deployLedger) { - return '--ledger'; + return '--ledger' } if (auth.deployInteractive) { - return '--interactive'; + return '--interactive' } - return ''; + return '' } export function getExecuteAuth(auth: DeployExecuteAuth): string { if (auth.executeKey) { - return `--private-key ${auth.executeKey}`; + return `--private-key ${auth.executeKey}` } if (auth.executeAccount) { - return `--account ${auth.executeAccount}`; + return `--account ${auth.executeAccount}` } if (auth.executeLedger) { - return '--ledger'; + return '--ledger' } if (auth.executeInteractive) { - return '--interactive'; + return '--interactive' } - return ''; + return '' } export function createDeployExecuteAuth(options: { - deployKey?: string; - deployAccount?: string; - deployLedger?: boolean; - deployInteractive?: boolean; - executeKey?: string; - executeAccount?: string; - executeLedger?: boolean; - executeInteractive?: boolean; - dryRun?: boolean; - skipExecute?: boolean; - verify?: boolean; + deployKey?: string + deployAccount?: string + deployLedger?: boolean + deployInteractive?: boolean + executeKey?: string + executeAccount?: string + executeLedger?: boolean + executeInteractive?: boolean + dryRun?: boolean + skipExecute?: boolean + verify?: boolean }): DeployExecuteAuth { return { deployKey: options.deployKey || '', @@ -85,5 +85,5 @@ export function createDeployExecuteAuth(options: { dryRun: options.dryRun || false, skipExecute: options.skipExecute || false, verifyContracts: options.verify || false, - }; + } } diff --git a/src/cli/utils/env.ts b/src/cli/utils/env.ts index 44e61636..4bc8e22b 100644 --- a/src/cli/utils/env.ts +++ b/src/cli/utils/env.ts @@ -1,57 +1,57 @@ -import * as dotenv from 'dotenv'; -import * as fs from 'fs'; -import * as path from 'path'; -import { die } from './log'; +import * as dotenv from 'dotenv' +import * as fs from 'fs' +import * as path from 'path' +import { die } from './log' function findRepoRoot(): string | null { - let dir = __dirname; + let dir = __dirname for (let i = 0; i < 10; i++) { if (fs.existsSync(path.join(dir, 'package.json'))) { - return dir; + return dir } - const parent = path.dirname(dir); - if (parent === dir) break; - dir = parent; + const parent = path.dirname(dir) + if (parent === dir) break + dir = parent } - return null; + return null } export function loadEnv(): void { - const repoRoot = findRepoRoot(); + const repoRoot = findRepoRoot() const candidates = [ path.join(process.cwd(), '.env'), repoRoot ? path.join(repoRoot, '.env') : null, '/app/.env', - ].filter((p): p is string => p !== null); + ].filter((p): p is string => p !== null) for (const envPath of candidates) { if (fs.existsSync(envPath)) { - dotenv.config({ path: envPath }); - return; + dotenv.config({ path: envPath }) + return } } } export function requireEnv(name: string): string { - const value = process.env[name]; + const value = process.env[name] if (!value) { - die(`Required env var not set: ${name} (check your .env file)`); + die(`Required env var not set: ${name} (check your .env file)`) } - return value; + return value } export function getEnv(name: string): string | undefined { - return process.env[name]; + return process.env[name] } export function getScriptsDir(): string { - const repoRoot = findRepoRoot(); + const repoRoot = findRepoRoot() if (repoRoot) { - return path.join(repoRoot, 'scripts', 'foundry'); + return path.join(repoRoot, 'scripts', 'foundry') } - return '/app/scripts/foundry'; + return '/app/scripts/foundry' } export function getRepoRoot(): string { - return findRepoRoot() || '/app'; + return findRepoRoot() || '/app' } diff --git a/src/cli/utils/forge.ts b/src/cli/utils/forge.ts index eaac011b..b4e93473 100644 --- a/src/cli/utils/forge.ts +++ b/src/cli/utils/forge.ts @@ -1,142 +1,168 @@ -import execa from 'execa'; -import * as fs from 'fs'; -import * as path from 'path'; -import { die, log } from './log'; -import { getRepoRoot } from './env'; +import execa from 'execa' +import * as fs from 'fs' +import * as path from 'path' +import { die, log } from './log' +import { getRepoRoot } from './env' export interface ForgeScriptOptions { - script: string; - rpcUrl: string; - authArgs?: string; - broadcast?: boolean; - verify?: boolean; - slow?: boolean; - skipSimulation?: boolean; - verbosity?: number; + script: string + rpcUrl: string + authArgs?: string + broadcast?: boolean + verify?: boolean + slow?: boolean + skipSimulation?: boolean + verbosity?: number } -export async function runForgeScript(options: ForgeScriptOptions): Promise { - const args = ['script', options.script, '--rpc-url', options.rpcUrl]; +export async function runForgeScript( + options: ForgeScriptOptions +): Promise { + const args = ['script', options.script, '--rpc-url', options.rpcUrl] if (options.slow) { - args.push('--slow'); + args.push('--slow') } if (options.skipSimulation) { - args.push('--skip-simulation'); + args.push('--skip-simulation') } - const verbosity = options.verbosity ?? 3; - args.push('-' + 'v'.repeat(verbosity)); + const verbosity = options.verbosity ?? 3 + args.push('-' + 'v'.repeat(verbosity)) if (options.broadcast && options.authArgs) { - args.push('--broadcast'); - args.push(...options.authArgs.split(' ').filter(Boolean)); + args.push('--broadcast') + args.push(...options.authArgs.split(' ').filter(Boolean)) } if (options.verify) { - args.push('--verify'); + args.push('--verify') } - log(`Running: forge ${args.slice(0, 2).join(' ')}...`); + log(`Running: forge ${args.slice(0, 2).join(' ')}...`) const result = await execa('forge', args, { stdio: 'inherit', env: process.env, - }); + }) if (result.exitCode !== 0) { - die(`Forge script failed with exit code ${result.exitCode}`); + die(`Forge script failed with exit code ${result.exitCode}`) } } export interface CastSendOptions { - to: string; - sig: string; - args: string[]; - rpcUrl: string; - authArgs?: string; + to: string + sig: string + args: string[] + rpcUrl: string + authArgs?: string } export async function runCastSend(options: CastSendOptions): Promise { - const args = ['send', options.to, options.sig, ...options.args, '--rpc-url', options.rpcUrl]; + const args = [ + 'send', + options.to, + options.sig, + ...options.args, + '--rpc-url', + options.rpcUrl, + ] if (options.authArgs) { - args.push(...options.authArgs.split(' ').filter(Boolean)); + args.push(...options.authArgs.split(' ').filter(Boolean)) } const result = await execa('cast', args, { stdio: 'inherit', env: process.env, - }); + }) if (result.exitCode !== 0) { - die(`Cast send failed with exit code ${result.exitCode}`); + die(`Cast send failed with exit code ${result.exitCode}`) } } export interface CastCallOptions { - to: string; - sig: string; - rpcUrl: string; + to: string + sig: string + rpcUrl: string } export async function runCastCall(options: CastCallOptions): Promise { try { - const result = await execa('cast', ['call', '--rpc-url', options.rpcUrl, options.to, options.sig]); - return result.stdout; + const result = await execa('cast', [ + 'call', + '--rpc-url', + options.rpcUrl, + options.to, + options.sig, + ]) + return result.stdout } catch { - return 'N/A'; + return 'N/A' } } -export async function castCalldata(sig: string, ...args: string[]): Promise { - const result = await execa('cast', ['calldata', sig, ...args]); - return result.stdout; +export async function castCalldata( + sig: string, + ...args: string[] +): Promise { + const result = await execa('cast', ['calldata', sig, ...args]) + return result.stdout } export async function getChainId(rpcUrl: string): Promise { - const result = await execa('cast', ['chain-id', '--rpc-url', rpcUrl]); - return result.stdout.trim(); + const result = await execa('cast', ['chain-id', '--rpc-url', rpcUrl]) + return result.stdout.trim() } -export function parseActionAddress(scriptPath: string, chainId: string): string { - const scriptName = path.basename(scriptPath); - const repoRoot = getRepoRoot(); - const broadcastFile = path.join(repoRoot, 'broadcast', scriptName, chainId, 'run-latest.json'); +export function parseActionAddress( + scriptPath: string, + chainId: string +): string { + const scriptName = path.basename(scriptPath) + const repoRoot = getRepoRoot() + const broadcastFile = path.join( + repoRoot, + 'broadcast', + scriptName, + chainId, + 'run-latest.json' + ) if (!fs.existsSync(broadcastFile)) { - die(`Broadcast file not found: ${broadcastFile}`); + die(`Broadcast file not found: ${broadcastFile}`) } - const content = JSON.parse(fs.readFileSync(broadcastFile, 'utf-8')); + const content = JSON.parse(fs.readFileSync(broadcastFile, 'utf-8')) const createTxs = content.transactions?.filter( (tx: { transactionType: string }) => tx.transactionType === 'CREATE' - ); + ) if (!createTxs || createTxs.length === 0) { - die('Could not parse action address from broadcast file'); + die('Could not parse action address from broadcast file') } - const address = createTxs[createTxs.length - 1]?.contractAddress; + const address = createTxs[createTxs.length - 1]?.contractAddress if (!address) { - die('Could not parse action address from broadcast file'); + die('Could not parse action address from broadcast file') } - return address; + return address } export function findScript(dir: string, pattern: RegExp): string | null { if (!fs.existsSync(dir)) { - return null; + return null } - const files = fs.readdirSync(dir); + const files = fs.readdirSync(dir) for (const file of files) { if (pattern.test(file) && file.endsWith('.s.sol')) { - return path.join(dir, file); + return path.join(dir, file) } } - return null; + return null } - diff --git a/src/cli/utils/log.ts b/src/cli/utils/log.ts index 9df2a6c6..355c1699 100644 --- a/src/cli/utils/log.ts +++ b/src/cli/utils/log.ts @@ -1,10 +1,10 @@ -const PREFIX = '[orbit-actions]'; +const PREFIX = '[orbit-actions]' export function log(message: string): void { - console.log(`${PREFIX} ${message}`); + console.log(`${PREFIX} ${message}`) } export function die(message: string): never { - console.error(`Error: ${message}`); - process.exit(1); + console.error(`Error: ${message}`) + process.exit(1) } diff --git a/test/docker/test-docker.bash b/test/docker/test-docker.bash index 03167239..d24cd210 100755 --- a/test/docker/test-docker.bash +++ b/test/docker/test-docker.bash @@ -25,23 +25,23 @@ run_test() { fi } -# Test 1: Tools are installed (passthrough) -echo "--- Tool Passthrough ---" -run_test "forge" docker run --rm "$IMAGE_NAME" forge --version -run_test "cast" docker run --rm "$IMAGE_NAME" cast --version -run_test "yarn" docker run --rm "$IMAGE_NAME" yarn --version -run_test "node" docker run --rm "$IMAGE_NAME" node --version +# Test 1: Tools are installed (via --entrypoint) +echo "--- Tools Installed ---" +run_test "forge" docker run --rm --entrypoint forge "$IMAGE_NAME" --version +run_test "cast" docker run --rm --entrypoint cast "$IMAGE_NAME" --version +run_test "yarn" docker run --rm --entrypoint yarn "$IMAGE_NAME" --version +run_test "node" docker run --rm --entrypoint node "$IMAGE_NAME" --version # Test 2: Dependencies are installed echo "" echo "--- Dependencies ---" -run_test "node_modules exists" docker run --rm "$IMAGE_NAME" test -d node_modules -run_test "forge dependencies" docker run --rm "$IMAGE_NAME" test -d node_modules/@arbitrum +run_test "node_modules exists" docker run --rm --entrypoint test "$IMAGE_NAME" -d node_modules +run_test "forge dependencies" docker run --rm --entrypoint test "$IMAGE_NAME" -d node_modules/@arbitrum # Test 3: Contracts compile echo "" echo "--- Contract Compilation ---" -run_test "contracts built" docker run --rm "$IMAGE_NAME" test -d out +run_test "contracts built" docker run --rm --entrypoint test "$IMAGE_NAME" -d out # Test 4: Browsing - list directories echo "" @@ -119,13 +119,13 @@ run_test "help command" docker run --rm "$IMAGE_NAME" help # Test 7: Yarn scripts work echo "" echo "--- Yarn Scripts ---" -run_test "yarn orbit:contracts:version --help" docker run --rm "$IMAGE_NAME" yarn orbit:contracts:version --help +run_test "yarn orbit:contracts:version --help" docker run --rm --entrypoint yarn "$IMAGE_NAME" orbit:contracts:version --help # Test 8: Unit tests pass echo "" echo "--- Unit Tests ---" echo "Running unit tests inside container..." -if docker run --rm "$IMAGE_NAME" yarn test:unit; then +if docker run --rm --entrypoint yarn "$IMAGE_NAME" test:unit; then echo "Unit tests: OK" else echo "Unit tests: FAILED" From 92fbc3121dafbc0aebc37c3fc1dcde06d0aad9d6 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Mon, 9 Feb 2026 15:39:06 +0000 Subject: [PATCH 13/44] test: add local smoke tests for CLI Non-Docker tests for the bin/router and related scripts. --- test/local/test-local.bash | 83 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100755 test/local/test-local.bash diff --git a/test/local/test-local.bash b/test/local/test-local.bash new file mode 100755 index 00000000..ef5358ec --- /dev/null +++ b/test/local/test-local.bash @@ -0,0 +1,83 @@ +#!/bin/bash +set -euo pipefail + +# Local (non-Docker) smoke tests for orbit-actions CLI +# Tests the bin/router and related scripts directly + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +ROUTER="$REPO_ROOT/bin/router" + +echo "=== Local Smoke Tests ===" +echo "Router: $ROUTER" +echo "" + +PASSED=0 +FAILED=0 + +check() { + local name="$1" + shift + printf "Testing %s... " "$name" + if "$@" >/dev/null 2>&1; then + echo "OK" + PASSED=$((PASSED + 1)) + else + echo "FAILED" + FAILED=$((FAILED + 1)) + fi +} + +check_output() { + local name="$1" + local expected="$2" + shift 2 + printf "Testing %s... " "$name" + # Disable pipefail for this check - it interferes with if/pipe/grep + if (set +o pipefail; "$@" 2>&1 | grep -q "$expected"); then + echo "OK" + PASSED=$((PASSED + 1)) + else + echo "FAILED (expected: $expected)" + FAILED=$((FAILED + 1)) + fi +} + +echo "--- Prerequisites ---" +check "forge installed" command -v forge +check "cast installed" command -v cast +check "jq installed" command -v jq + +echo "" +echo "--- Directory Browsing ---" +check "list top level" "$ROUTER" +check_output "list contract-upgrades" "1.2.1" "$ROUTER" contract-upgrades +check_output "list contract-upgrades/1.2.1" "deploy" "$ROUTER" contract-upgrades/1.2.1 +check_output "list arbos-upgrades" "at-timestamp" "$ROUTER" arbos-upgrades + +echo "" +echo "--- File Viewing ---" +check_output "view README" "Nitro contracts" "$ROUTER" contract-upgrades/1.2.1/README.md + +echo "" +echo "--- Help ---" +check_output "help command" "Usage:" "$ROUTER" help +check_output "contract-upgrade help" "deploy-execute-verify" "$REPO_ROOT/bin/contract-upgrade" --help +check_output "arbos-upgrade help" "deploy-execute-verify" "$REPO_ROOT/bin/arbos-upgrade" --help + +echo "" +echo "--- Passthrough ---" +check_output "forge passthrough" "forge" "$ROUTER" forge --version +check_output "cast passthrough" "cast" "$ROUTER" cast --version + +echo "" +echo "=== Summary ===" +echo "Passed: $PASSED" +echo "Failed: $FAILED" + +if [[ $FAILED -gt 0 ]]; then + echo "Some tests failed!" + exit 1 +else + echo "All tests passed!" +fi From e3addacaec53df068b777c62d5091a09594252dc Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Mon, 9 Feb 2026 16:10:22 +0000 Subject: [PATCH 14/44] docs: reorganize README with CLI and Docker sections Replace verbose Docker documentation with concise CLI section and streamlined Docker examples. Move tooling documentation to end of README to prioritize upgrade guides. --- README.md | 115 ++++++++++++++++++++++-------------------------------- 1 file changed, 47 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index 90383e73..43d0a4fb 100644 --- a/README.md +++ b/README.md @@ -25,73 +25,6 @@ For token bridge related operations, these are the additional requirements: yarn install ``` -## Using Docker - -The Orbit actions are also available via Docker. - -### Build the image - -First, ensure Foundry dependencies are installed: - -```bash -forge install -``` - -Then build the image: - -```bash -docker build -t orbit-actions . -``` - -### Run commands - -Pass the command you want to run directly to Docker: - -```bash -# Check contract versions -docker run --rm \ - -e INBOX_ADDRESS=0xYourInboxAddress \ - -e INFURA_KEY=your_infura_key \ - orbit-actions \ - yarn orbit:contracts:version --network arb1 - -# Run forge script -docker run --rm \ - --env-file orbit.env \ - -v $(pwd)/broadcast:/app/broadcast \ - orbit-actions \ - forge script --sender 0xYourAddress --rpc-url $PARENT_CHAIN_RPC --broadcast \ - scripts/foundry/contract-upgrades/2.1.3/DeployNitroContracts2Point1Point3UpgradeAction.s.sol -vvv - -# Run cast commands -docker run --rm \ - orbit-actions \ - cast call --rpc-url https://arb1.arbitrum.io/rpc 0xYourRollup "wasmModuleRoot()" -``` - -### Environment variables - -Create an `orbit.env` file with your configuration and pass it using `--env-file`: - -```bash -PARENT_CHAIN_RPC=https://arb1.arbitrum.io/rpc -INBOX_ADDRESS=0x... -PROXY_ADMIN_ADDRESS=0x... -PARENT_UPGRADE_EXECUTOR_ADDRESS=0x... -``` - -### Getting output artifacts - -Mount a volume to retrieve broadcast artifacts: - -```bash -docker run --rm \ - --env-file orbit.env \ - -v $(pwd)/broadcast:/app/broadcast \ - orbit-actions \ - forge script ... -``` - ## Check Version and Upgrade Path Run the follow command to check the version of Nitro contracts deployed on the parent chain of your Orbit chain. @@ -193,4 +126,50 @@ See [setCacheManager](scripts/foundry/stylus/setCacheManager). Currently limited to L2s; L3 support is expected in a future update. -See [Nitro contracts 3.1.0 upgrade](https://github.com/OffchainLabs/orbit-actions/tree/main/scripts/foundry/contract-upgrades/3.1.0). +See [Nitro contracts 3.1.0 upgrade](https://github.com/OffchainLabs/orbit-actions/tree/main/scripts/foundry/contract-upgrades/3.1.0). + +# CLI + +The `orbit-actions` CLI provides a guided interface for running upgrade scripts. It wraps Foundry commands and handles the deploy/execute/verify workflow. + +```bash +# Browse available scripts +yarn orbit-actions # List top-level directories +yarn orbit-actions contract-upgrades # List versions +yarn orbit-actions contract-upgrades/1.2.1 # List contents + commands + +# View files +yarn orbit-actions contract-upgrades/1.2.1/README.md + +# Run contract upgrades +yarn orbit-actions contract-upgrades/1.2.1/deploy-execute-verify --dry-run +yarn orbit-actions contract-upgrades/1.2.1/deploy --private-key $KEY + +# Run ArbOS upgrades +yarn orbit-actions arbos-upgrades/at-timestamp/deploy-execute-verify 32 --dry-run +``` + +Run `yarn orbit-actions help` for full usage details. The CLI reads configuration from a `.env` file in the working directory. + +## Docker + +The CLI is available as a Docker image at `offchainlabs/orbit-actions`: + +```bash +# Check contract versions +docker run --rm \ + -e INBOX_ADDRESS=0xaE21fDA3de92dE2FDAF606233b2863782Ba046F9 \ + -e INFURA_KEY=$INFURA_KEY \ + offchainlabs/orbit-actions:versioner \ + --network arb1 + +# Browse upgrade scripts +docker run --rm offchainlabs/orbit-actions contract-upgrades + +# Run upgrade with env file and capture broadcast output +docker run --rm \ + -v $(pwd)/.env:/app/.env \ + -v $(pwd)/broadcast:/app/broadcast \ + offchainlabs/orbit-actions \ + contract-upgrades/1.2.1/deploy-execute-verify --dry-run +``` From 596182b083fb1d857ac9756b593122e63f0a8d6c Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Tue, 10 Feb 2026 12:06:04 +0000 Subject: [PATCH 15/44] refactor: simplify env loading to current directory only Remove fallback locations for .env files. Now only loads from process.cwd(), matching Forge's behavior for transparency. --- src/cli/utils/env.ts | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/cli/utils/env.ts b/src/cli/utils/env.ts index 4bc8e22b..195305b5 100644 --- a/src/cli/utils/env.ts +++ b/src/cli/utils/env.ts @@ -17,18 +17,9 @@ function findRepoRoot(): string | null { } export function loadEnv(): void { - const repoRoot = findRepoRoot() - const candidates = [ - path.join(process.cwd(), '.env'), - repoRoot ? path.join(repoRoot, '.env') : null, - '/app/.env', - ].filter((p): p is string => p !== null) - - for (const envPath of candidates) { - if (fs.existsSync(envPath)) { - dotenv.config({ path: envPath }) - return - } + const envPath = path.join(process.cwd(), '.env') + if (fs.existsSync(envPath)) { + dotenv.config({ path: envPath }) } } From 9dc010880c3599911b335968cf0f69f78f9b8669 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Tue, 10 Feb 2026 12:33:54 +0000 Subject: [PATCH 16/44] refactor: extract duplicate execute/verify logic in arbos-upgrade Extract executeUpgrade() and verifyUpgrade() helpers to eliminate code duplication between standalone commands and deploy-execute-verify. --- src/cli/commands/arbos-upgrade.ts | 149 +++++++++++------------------- 1 file changed, 56 insertions(+), 93 deletions(-) diff --git a/src/cli/commands/arbos-upgrade.ts b/src/cli/commands/arbos-upgrade.ts index 4fa9e229..bfb4e57c 100644 --- a/src/cli/commands/arbos-upgrade.ts +++ b/src/cli/commands/arbos-upgrade.ts @@ -30,50 +30,23 @@ const ARB_SYS = '0x0000000000000000000000000000000000000064' // Nitro ArbOS versions are offset by 55 to avoid collision with classic (pre-Nitro) versions const ARBOS_VERSION_OFFSET = 55 - -async function cmdDeploy(version: string, args: string[]): Promise { - const authArgs = parseAuthArgs(args) - - const rpcUrl = requireEnv('CHILD_CHAIN_RPC') - - if (!fs.existsSync(DEPLOY_SCRIPT)) { - die(`Deploy script not found: ${DEPLOY_SCRIPT}`) - } - - // Forge script reads this from env - process.env.ARBOS_VERSION = version - - log(`Running: ${path.basename(DEPLOY_SCRIPT)} for ArbOS ${version}`) - - await runForgeScript({ - script: DEPLOY_SCRIPT, - rpcUrl, - authArgs, - broadcast: Boolean(authArgs), - slow: true, - }) -} - -async function cmdExecute(args: string[]): Promise { - const authArgs = parseAuthArgs(args) - - const rpcUrl = requireEnv('CHILD_CHAIN_RPC') - const upgradeExecutor = requireEnv('CHILD_UPGRADE_EXECUTOR_ADDRESS') - const actionAddress = requireEnv('UPGRADE_ACTION_ADDRESS') - - log(`Executing ArbOS upgrade action: ${actionAddress}`) - - const performCalldata = '0xb0a75d36' // perform() selector - - if (!authArgs) { - // No auth - output calldata for multisig +const PERFORM_SELECTOR = '0xb0a75d36' + +async function executeUpgrade( + actionAddress: string, + upgradeExecutor: string, + rpcUrl: string, + authArgs: string, + dryRun: boolean +): Promise { + if (dryRun || !authArgs) { const executeCalldata = await castCalldata( 'execute(address,bytes)', actionAddress, - performCalldata + PERFORM_SELECTOR ) - log('Calldata for UpgradeExecutor.execute():') + log(dryRun ? 'Dry run - calldata for UpgradeExecutor.execute():' : 'Calldata for UpgradeExecutor.execute():') console.log('') console.log(`To: ${upgradeExecutor}`) console.log(`Calldata: ${executeCalldata}`) @@ -83,7 +56,7 @@ async function cmdExecute(args: string[]): Promise { await runCastSend({ to: upgradeExecutor, sig: 'execute(address,bytes)', - args: [actionAddress, performCalldata], + args: [actionAddress, PERFORM_SELECTOR], rpcUrl, authArgs, }) @@ -92,9 +65,7 @@ async function cmdExecute(args: string[]): Promise { } } -async function cmdVerify(): Promise { - const rpcUrl = requireEnv('CHILD_CHAIN_RPC') - +async function verifyUpgrade(rpcUrl: string): Promise { log('Checking ArbOS upgrade status...') const scheduled = await runCastCall({ @@ -121,6 +92,46 @@ async function cmdVerify(): Promise { log(`Current ArbOS version: ${currentVersion}`) } +async function cmdDeploy(version: string, args: string[]): Promise { + const authArgs = parseAuthArgs(args) + + const rpcUrl = requireEnv('CHILD_CHAIN_RPC') + + if (!fs.existsSync(DEPLOY_SCRIPT)) { + die(`Deploy script not found: ${DEPLOY_SCRIPT}`) + } + + // Forge script reads this from env + process.env.ARBOS_VERSION = version + + log(`Running: ${path.basename(DEPLOY_SCRIPT)} for ArbOS ${version}`) + + await runForgeScript({ + script: DEPLOY_SCRIPT, + rpcUrl, + authArgs, + broadcast: Boolean(authArgs), + slow: true, + }) +} + +async function cmdExecute(args: string[]): Promise { + const authArgs = parseAuthArgs(args) + + const rpcUrl = requireEnv('CHILD_CHAIN_RPC') + const upgradeExecutor = requireEnv('CHILD_UPGRADE_EXECUTOR_ADDRESS') + const actionAddress = requireEnv('UPGRADE_ACTION_ADDRESS') + + log(`Executing ArbOS upgrade action: ${actionAddress}`) + + await executeUpgrade(actionAddress, upgradeExecutor, rpcUrl, authArgs, false) +} + +async function cmdVerify(): Promise { + const rpcUrl = requireEnv('CHILD_CHAIN_RPC') + await verifyUpgrade(rpcUrl) +} + async function cmdDeployExecuteVerify( version: string, options: { @@ -207,62 +218,14 @@ async function cmdDeployExecuteVerify( if (!auth.skipExecute) { log('Step 2: Executing ArbOS upgrade...') - - const performCalldata = '0xb0a75d36' // perform() selector - - if (auth.dryRun) { - const executeCalldata = await castCalldata( - 'execute(address,bytes)', - upgradeActionAddress, - performCalldata - ) - - log('Dry run - calldata for UpgradeExecutor.execute():') - console.log('') - console.log(`To: ${upgradeExecutor}`) - console.log(`Calldata: ${executeCalldata}`) - console.log('') - log('Submit this to your multisig/Safe to execute the upgrade') - } else { - await runCastSend({ - to: upgradeExecutor, - sig: 'execute(address,bytes)', - args: [upgradeActionAddress, performCalldata], - rpcUrl, - authArgs: executeAuth, - }) - - log('ArbOS upgrade scheduled successfully') - } + await executeUpgrade(upgradeActionAddress, upgradeExecutor, rpcUrl, executeAuth, auth.dryRun) } else { log('Step 2: Skipped execute') } if (!auth.dryRun && !auth.skipExecute) { log('Step 3: Verifying scheduled upgrade...') - - const scheduled = await runCastCall({ - to: ARB_OWNER_PUBLIC, - sig: 'getScheduledUpgrade()(uint64,uint64)', - rpcUrl, - }) - log(`Scheduled upgrade (version, timestamp): ${scheduled}`) - - const currentRaw = await runCastCall({ - to: ARB_SYS, - sig: 'arbOSVersion()(uint64)', - rpcUrl, - }) - - let currentVersion: number - if (currentRaw === 'N/A') { - currentVersion = 0 - } else { - const rawNum = parseInt(currentRaw, 10) - currentVersion = rawNum - ARBOS_VERSION_OFFSET - } - - log(`Current ArbOS version: ${currentVersion}`) + await verifyUpgrade(rpcUrl) } log('Done') From 6dd231fa2c18b8d63d28cc9eb4dd24a00abf051e Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Tue, 10 Feb 2026 13:03:11 +0000 Subject: [PATCH 17/44] refactor: remove unused Commander subcommands Remove redundant subcommand interface - all functionality is accessed via the router's path-based syntax. Also extract deployAction helper to reduce duplication in arbos-upgrade. --- src/cli/commands/arbos-upgrade.ts | 113 +++++++-------------------- src/cli/commands/contract-upgrade.ts | 54 ------------- src/cli/index.ts | 5 -- 3 files changed, 28 insertions(+), 144 deletions(-) diff --git a/src/cli/commands/arbos-upgrade.ts b/src/cli/commands/arbos-upgrade.ts index bfb4e57c..02e84315 100644 --- a/src/cli/commands/arbos-upgrade.ts +++ b/src/cli/commands/arbos-upgrade.ts @@ -1,4 +1,3 @@ -import { Command } from 'commander' import * as path from 'path' import * as fs from 'fs' import { log, die } from '../utils/log' @@ -32,6 +31,31 @@ const ARB_SYS = '0x0000000000000000000000000000000000000064' const ARBOS_VERSION_OFFSET = 55 const PERFORM_SELECTOR = '0xb0a75d36' +function checkDeployScript(): void { + if (!fs.existsSync(DEPLOY_SCRIPT)) { + die(`Deploy script not found: ${DEPLOY_SCRIPT}`) + } +} + +async function deployAction( + version: string, + rpcUrl: string, + authArgs: string, + options: { broadcast: boolean; verify?: boolean } +): Promise { + checkDeployScript() + process.env.ARBOS_VERSION = version + + await runForgeScript({ + script: DEPLOY_SCRIPT, + rpcUrl, + authArgs, + broadcast: options.broadcast, + verify: options.verify, + slow: true, + }) +} + async function executeUpgrade( actionAddress: string, upgradeExecutor: string, @@ -94,25 +118,9 @@ async function verifyUpgrade(rpcUrl: string): Promise { async function cmdDeploy(version: string, args: string[]): Promise { const authArgs = parseAuthArgs(args) - const rpcUrl = requireEnv('CHILD_CHAIN_RPC') - - if (!fs.existsSync(DEPLOY_SCRIPT)) { - die(`Deploy script not found: ${DEPLOY_SCRIPT}`) - } - - // Forge script reads this from env - process.env.ARBOS_VERSION = version - log(`Running: ${path.basename(DEPLOY_SCRIPT)} for ArbOS ${version}`) - - await runForgeScript({ - script: DEPLOY_SCRIPT, - rpcUrl, - authArgs, - broadcast: Boolean(authArgs), - slow: true, - }) + await deployAction(version, rpcUrl, authArgs, { broadcast: Boolean(authArgs) }) } async function cmdExecute(args: string[]): Promise { @@ -151,12 +159,8 @@ async function cmdDeployExecuteVerify( const auth = createDeployExecuteAuth(options) log(`ArbOS version: ${version}`) + checkDeployScript() - if (!fs.existsSync(DEPLOY_SCRIPT)) { - die(`Deploy script not found: ${DEPLOY_SCRIPT}`) - } - - // Auto-detect skip_deploy if UPGRADE_ACTION_ADDRESS is set const skipDeploy = Boolean(getEnv('UPGRADE_ACTION_ADDRESS')) let upgradeActionAddress = getEnv('UPGRADE_ACTION_ADDRESS') || '' @@ -168,9 +172,6 @@ async function cmdDeployExecuteVerify( const upgradeExecutor = requireEnv('CHILD_UPGRADE_EXECUTOR_ADDRESS') requireEnv('SCHEDULE_TIMESTAMP') - // Forge script reads this from env - process.env.ARBOS_VERSION = version - const deployAuth = getDeployAuth(auth) const executeAuth = getExecuteAuth(auth) @@ -193,13 +194,9 @@ async function cmdDeployExecuteVerify( log(`Target chain ID: ${chainId}`) log('Step 1: Deploying ArbOS upgrade action...') - await runForgeScript({ - script: DEPLOY_SCRIPT, - rpcUrl, - authArgs: deployAuth, + await deployAction(version, rpcUrl, deployAuth, { broadcast: !auth.dryRun, verify: auth.verifyContracts, - slow: true, }) if (!auth.dryRun) { @@ -231,58 +228,4 @@ async function cmdDeployExecuteVerify( log('Done') } -export function createArbosUpgradeCommand(): Command { - const cmd = new Command('arbos-upgrade') - .description('ArbOS upgrade operations') - .argument('', 'ArbOS version number') - .argument( - '[command]', - 'Command: deploy, execute, verify, deploy-execute-verify', - 'deploy-execute-verify' - ) - .option('--private-key ', 'Private key (for deploy/execute)') - .option('--account ', 'Keystore account (for deploy/execute)') - .option('--ledger', 'Use Ledger (for deploy/execute)') - .option('--interactive', 'Prompt for key (for deploy/execute)') - .option('--deploy-key ', 'Private key for deploy step') - .option('--deploy-account ', 'Keystore account for deploy') - .option('--deploy-ledger', 'Use Ledger for deploy') - .option('--deploy-interactive', 'Prompt for key for deploy') - .option('--execute-key ', 'Private key for execute step') - .option('--execute-account ', 'Keystore account for execute') - .option('--execute-ledger', 'Use Ledger for execute') - .option('--execute-interactive', 'Prompt for key for execute') - .option('-n, --dry-run', 'Simulate without broadcasting') - .option('--skip-execute', 'Deploy only') - .option('-v, --verify', 'Verify on block explorer') - .action(async (version: string, command: string, options) => { - const args: string[] = [] - if (options.privateKey) args.push('--private-key', options.privateKey) - if (options.account) args.push('--account', options.account) - if (options.ledger) args.push('--ledger') - if (options.interactive) args.push('--interactive') - - switch (command) { - case 'deploy': - await cmdDeploy(version, args) - break - case 'execute': - await cmdExecute(args) - break - case 'verify': - await cmdVerify() - break - case 'deploy-execute-verify': - await cmdDeployExecuteVerify(version, options) - break - default: - die( - `Unknown command: ${command}\n\nCommands: deploy, execute, verify, deploy-execute-verify` - ) - } - }) - - return cmd -} - export { cmdDeploy, cmdExecute, cmdVerify, cmdDeployExecuteVerify } diff --git a/src/cli/commands/contract-upgrade.ts b/src/cli/commands/contract-upgrade.ts index 3d416573..48444311 100644 --- a/src/cli/commands/contract-upgrade.ts +++ b/src/cli/commands/contract-upgrade.ts @@ -1,4 +1,3 @@ -import { Command } from 'commander' import * as path from 'path' import * as fs from 'fs' import { log, die } from '../utils/log' @@ -226,59 +225,6 @@ async function cmdDeployExecuteVerify( log('Done') } -export function createContractUpgradeCommand(): Command { - const cmd = new Command('contract-upgrade') - .description('Contract upgrade operations') - .argument('', 'Contract version (e.g., 1.2.1)') - .argument( - '', - 'Command: deploy, execute, verify, deploy-execute-verify' - ) - .option('--private-key ', 'Private key (for deploy/execute)') - .option('--account ', 'Keystore account (for deploy/execute)') - .option('--ledger', 'Use Ledger (for deploy/execute)') - .option('--interactive', 'Prompt for key (for deploy/execute)') - .option('--deploy-key ', 'Private key for deploy step') - .option('--deploy-account ', 'Keystore account for deploy') - .option('--deploy-ledger', 'Use Ledger for deploy') - .option('--deploy-interactive', 'Prompt for key for deploy') - .option('--execute-key ', 'Private key for execute step') - .option('--execute-account ', 'Keystore account for execute') - .option('--execute-ledger', 'Use Ledger for execute') - .option('--execute-interactive', 'Prompt for key for execute') - .option('-n, --dry-run', 'Simulate without broadcasting') - .option('--skip-execute', 'Deploy only') - .option('-v, --verify', 'Verify on block explorer') - .action(async (version: string, command: string, options) => { - const args: string[] = [] - if (options.privateKey) args.push('--private-key', options.privateKey) - if (options.account) args.push('--account', options.account) - if (options.ledger) args.push('--ledger') - if (options.interactive) args.push('--interactive') - - switch (command) { - case 'deploy': - await cmdDeploy(version, args) - break - case 'execute': - await cmdExecute(version, args) - break - case 'verify': - await cmdVerify(version) - break - case 'deploy-execute-verify': - await cmdDeployExecuteVerify(version, options) - break - default: - die( - `Unknown command: ${command}\n\nCommands: deploy, execute, verify, deploy-execute-verify` - ) - } - }) - - return cmd -} - export { cmdDeploy, cmdExecute, diff --git a/src/cli/index.ts b/src/cli/index.ts index 0b009dc6..6d5ee588 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -3,8 +3,6 @@ import { program } from 'commander' import { loadEnv } from './utils/env' import { router } from './router' -import { createContractUpgradeCommand } from './commands/contract-upgrade' -import { createArbosUpgradeCommand } from './commands/arbos-upgrade' loadEnv() @@ -18,7 +16,4 @@ program await router(pathArg, args) }) -program.addCommand(createContractUpgradeCommand()) -program.addCommand(createArbosUpgradeCommand()) - program.parse() From aaa6eea34ca7e6762d295a9caf5613a7b560a45a Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Tue, 10 Feb 2026 13:06:52 +0000 Subject: [PATCH 18/44] build: pin Foundry to specific nightly version Pin foundryup to nightly-2026-02-09 for reproducible Docker builds. --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index b8fe17f2..e2003a34 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,10 +7,10 @@ RUN apt-get update && apt-get install -y \ jq \ && rm -rf /var/lib/apt/lists/* -# Install Foundry +# Install Foundry (pinned for reproducible builds) RUN curl -L https://foundry.paradigm.xyz | bash ENV PATH="/root/.foundry/bin:${PATH}" -RUN foundryup +RUN foundryup --version nightly-2026-02-09 # Install Yarn Classic (v1) - matches the repo's yarn.lock format RUN npm install -g --force yarn@1.22.22 From ead08146e36a1f6747a750e9ab1c5a50f6b5496c Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Tue, 10 Feb 2026 13:08:38 +0000 Subject: [PATCH 19/44] chore: remove obvious Dockerfile comments --- Dockerfile | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index e2003a34..1e8fe26a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,23 +15,16 @@ RUN foundryup --version nightly-2026-02-09 # Install Yarn Classic (v1) - matches the repo's yarn.lock format RUN npm install -g --force yarn@1.22.22 -# Set working directory WORKDIR /app -# Copy package files first for better caching +# Copy package files first for better layer caching COPY package.json yarn.lock ./ -# Install dependencies (using --ignore-scripts like CI does, then forge install separately) +# --ignore-scripts: forge install runs separately after full copy RUN yarn install --frozen-lockfile --ignore-scripts -# Copy the rest of the repository COPY . . - -# Build contracts RUN forge build - -# Build CLI RUN yarn build:cli -# Direct node entrypoint (no shell wrapper) ENTRYPOINT ["node", "/app/dist/cli/index.js"] From 7977435038e717d6f95a4b88c99bb7ddf6076c9e Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Tue, 10 Feb 2026 13:31:11 +0000 Subject: [PATCH 20/44] fix: correct Foundry version pin and lint issues - Pin to actual nightly release (2026-02-10) using commit hash - Add dist/ to eslintignore - Fix prettier formatting in arbos-upgrade.ts --- .eslintignore | 1 + Dockerfile | 4 ++-- src/cli/commands/arbos-upgrade.ts | 18 +++++++++++++++--- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/.eslintignore b/.eslintignore index c3af8579..a89c6a71 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ lib/ +dist/ diff --git a/Dockerfile b/Dockerfile index 1e8fe26a..9a7c5637 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,10 +7,10 @@ RUN apt-get update && apt-get install -y \ jq \ && rm -rf /var/lib/apt/lists/* -# Install Foundry (pinned for reproducible builds) +# Install Foundry (pinned to 2026-02-10 nightly for reproducible builds) RUN curl -L https://foundry.paradigm.xyz | bash ENV PATH="/root/.foundry/bin:${PATH}" -RUN foundryup --version nightly-2026-02-09 +RUN foundryup --version nightly-e788798a511a32e896b127560e2269fb2c43eddd # Install Yarn Classic (v1) - matches the repo's yarn.lock format RUN npm install -g --force yarn@1.22.22 diff --git a/src/cli/commands/arbos-upgrade.ts b/src/cli/commands/arbos-upgrade.ts index 02e84315..0a598485 100644 --- a/src/cli/commands/arbos-upgrade.ts +++ b/src/cli/commands/arbos-upgrade.ts @@ -70,7 +70,11 @@ async function executeUpgrade( PERFORM_SELECTOR ) - log(dryRun ? 'Dry run - calldata for UpgradeExecutor.execute():' : 'Calldata for UpgradeExecutor.execute():') + log( + dryRun + ? 'Dry run - calldata for UpgradeExecutor.execute():' + : 'Calldata for UpgradeExecutor.execute():' + ) console.log('') console.log(`To: ${upgradeExecutor}`) console.log(`Calldata: ${executeCalldata}`) @@ -120,7 +124,9 @@ async function cmdDeploy(version: string, args: string[]): Promise { const authArgs = parseAuthArgs(args) const rpcUrl = requireEnv('CHILD_CHAIN_RPC') log(`Running: ${path.basename(DEPLOY_SCRIPT)} for ArbOS ${version}`) - await deployAction(version, rpcUrl, authArgs, { broadcast: Boolean(authArgs) }) + await deployAction(version, rpcUrl, authArgs, { + broadcast: Boolean(authArgs), + }) } async function cmdExecute(args: string[]): Promise { @@ -215,7 +221,13 @@ async function cmdDeployExecuteVerify( if (!auth.skipExecute) { log('Step 2: Executing ArbOS upgrade...') - await executeUpgrade(upgradeActionAddress, upgradeExecutor, rpcUrl, executeAuth, auth.dryRun) + await executeUpgrade( + upgradeActionAddress, + upgradeExecutor, + rpcUrl, + executeAuth, + auth.dryRun + ) } else { log('Step 2: Skipped execute') } From 2f30ddefd2ef236a8020ce02e45c7bf258799aaf Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Tue, 10 Feb 2026 14:23:52 +0000 Subject: [PATCH 21/44] revert: remove Foundry version pin that fails in Docker The foundryup --version flag doesn't work correctly in Docker when bootstrapping from the install script. Revert to unpinned foundryup which installs the latest stable release. --- Dockerfile | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9a7c5637..864e3ada 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,10 +7,9 @@ RUN apt-get update && apt-get install -y \ jq \ && rm -rf /var/lib/apt/lists/* -# Install Foundry (pinned to 2026-02-10 nightly for reproducible builds) -RUN curl -L https://foundry.paradigm.xyz | bash +# Install Foundry ENV PATH="/root/.foundry/bin:${PATH}" -RUN foundryup --version nightly-e788798a511a32e896b127560e2269fb2c43eddd +RUN curl -L https://foundry.paradigm.xyz | bash && foundryup # Install Yarn Classic (v1) - matches the repo's yarn.lock format RUN npm install -g --force yarn@1.22.22 From 80ab399f6e4136a39ce1862e9aca31caab6fc708 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Wed, 4 Mar 2026 11:01:53 +0000 Subject: [PATCH 22/44] Update src/cli/commands/arbos-upgrade.ts Co-authored-by: Henry <11198460+godzillaba@users.noreply.github.com> --- src/cli/commands/arbos-upgrade.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/commands/arbos-upgrade.ts b/src/cli/commands/arbos-upgrade.ts index 0a598485..c13a8bf0 100644 --- a/src/cli/commands/arbos-upgrade.ts +++ b/src/cli/commands/arbos-upgrade.ts @@ -29,7 +29,7 @@ const ARB_SYS = '0x0000000000000000000000000000000000000064' // Nitro ArbOS versions are offset by 55 to avoid collision with classic (pre-Nitro) versions const ARBOS_VERSION_OFFSET = 55 -const PERFORM_SELECTOR = '0xb0a75d36' +const PERFORM_SELECTOR = new Interface(['function perform()']).getFunction('perform')!.selector function checkDeployScript(): void { if (!fs.existsSync(DEPLOY_SCRIPT)) { From f083a68819c73ca0f6672c60160f5ae9a35997ff Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Wed, 4 Mar 2026 11:15:07 +0000 Subject: [PATCH 23/44] fix: add missing Interface import from ethers --- src/cli/commands/arbos-upgrade.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cli/commands/arbos-upgrade.ts b/src/cli/commands/arbos-upgrade.ts index c13a8bf0..08d0230e 100644 --- a/src/cli/commands/arbos-upgrade.ts +++ b/src/cli/commands/arbos-upgrade.ts @@ -1,5 +1,6 @@ import * as path from 'path' import * as fs from 'fs' +import { Interface } from 'ethers' import { log, die } from '../utils/log' import { requireEnv, getEnv, getScriptsDir } from '../utils/env' import { From 5a4ceca2d477352ca5490cb1cf87702e02aaa616 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Wed, 4 Mar 2026 11:20:12 +0000 Subject: [PATCH 24/44] refactor: pass env vars to runForgeScript instead of mutating process.env --- src/cli/commands/arbos-upgrade.ts | 2 +- src/cli/commands/contract-upgrade.ts | 5 +++-- src/cli/utils/forge.ts | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/cli/commands/arbos-upgrade.ts b/src/cli/commands/arbos-upgrade.ts index 08d0230e..998fc3a2 100644 --- a/src/cli/commands/arbos-upgrade.ts +++ b/src/cli/commands/arbos-upgrade.ts @@ -45,7 +45,6 @@ async function deployAction( options: { broadcast: boolean; verify?: boolean } ): Promise { checkDeployScript() - process.env.ARBOS_VERSION = version await runForgeScript({ script: DEPLOY_SCRIPT, @@ -54,6 +53,7 @@ async function deployAction( broadcast: options.broadcast, verify: options.verify, slow: true, + env: { ARBOS_VERSION: version }, }) } diff --git a/src/cli/commands/contract-upgrade.ts b/src/cli/commands/contract-upgrade.ts index 48444311..2d9cb3b6 100644 --- a/src/cli/commands/contract-upgrade.ts +++ b/src/cli/commands/contract-upgrade.ts @@ -186,8 +186,7 @@ async function cmdDeployExecuteVerify( log('Step 1: Skipped deploy') } - // Forge script reads this from env - process.env.UPGRADE_ACTION_ADDRESS = upgradeActionAddress + const forgeEnv = { UPGRADE_ACTION_ADDRESS: upgradeActionAddress } if (!auth.skipExecute) { log('Step 2: Executing upgrade...') @@ -197,6 +196,7 @@ async function cmdDeployExecuteVerify( rpcUrl, authArgs: executeAuth, broadcast: !auth.dryRun, + env: forgeEnv, }) if (auth.dryRun) { @@ -216,6 +216,7 @@ async function cmdDeployExecuteVerify( await runForgeScript({ script: verifyScript, rpcUrl, + env: forgeEnv, }) } else { log('No Verify script found - check README for manual verification') diff --git a/src/cli/utils/forge.ts b/src/cli/utils/forge.ts index b4e93473..db2cc44f 100644 --- a/src/cli/utils/forge.ts +++ b/src/cli/utils/forge.ts @@ -13,6 +13,7 @@ export interface ForgeScriptOptions { slow?: boolean skipSimulation?: boolean verbosity?: number + env?: Record } export async function runForgeScript( @@ -44,7 +45,7 @@ export async function runForgeScript( const result = await execa('forge', args, { stdio: 'inherit', - env: process.env, + env: { ...process.env, ...options.env }, }) if (result.exitCode !== 0) { From 2acea4ee91d81a76788a95ac29449042975dc93c Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Wed, 4 Mar 2026 11:29:00 +0000 Subject: [PATCH 25/44] fix: use try/catch for execa error handling instead of dead exitCode checks --- src/cli/utils/forge.ts | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/cli/utils/forge.ts b/src/cli/utils/forge.ts index db2cc44f..1975dcae 100644 --- a/src/cli/utils/forge.ts +++ b/src/cli/utils/forge.ts @@ -43,13 +43,13 @@ export async function runForgeScript( log(`Running: forge ${args.slice(0, 2).join(' ')}...`) - const result = await execa('forge', args, { - stdio: 'inherit', - env: { ...process.env, ...options.env }, - }) - - if (result.exitCode !== 0) { - die(`Forge script failed with exit code ${result.exitCode}`) + try { + await execa('forge', args, { + stdio: 'inherit', + env: { ...process.env, ...options.env }, + }) + } catch { + die('Forge script failed') } } @@ -75,13 +75,12 @@ export async function runCastSend(options: CastSendOptions): Promise { args.push(...options.authArgs.split(' ').filter(Boolean)) } - const result = await execa('cast', args, { - stdio: 'inherit', - env: process.env, - }) - - if (result.exitCode !== 0) { - die(`Cast send failed with exit code ${result.exitCode}`) + try { + await execa('cast', args, { + stdio: 'inherit', + }) + } catch { + die('Cast send failed') } } From a863cdb2011ab53cc390cfbdd7f0677510acf728 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Wed, 4 Mar 2026 11:33:52 +0000 Subject: [PATCH 26/44] refactor: constrain verbosity to valid forge range via union type --- src/cli/utils/forge.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/utils/forge.ts b/src/cli/utils/forge.ts index 1975dcae..5dac75ee 100644 --- a/src/cli/utils/forge.ts +++ b/src/cli/utils/forge.ts @@ -12,7 +12,7 @@ export interface ForgeScriptOptions { verify?: boolean slow?: boolean skipSimulation?: boolean - verbosity?: number + verbosity?: 1 | 2 | 3 | 4 | 5 env?: Record } From 4b4712e88174040f2cb5785e1531fa75b2c9c1ec Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Mon, 9 Mar 2026 15:53:24 +0000 Subject: [PATCH 27/44] fix: minor cleanups to Dockerfile, workflow, router, and arbos-upgrade Add comments to Dockerfile and workflow, format PERFORM_SELECTOR expression, use die() instead of manual console.error/process.exit in router. --- .github/workflows/publish-docker.yml | 1 + Dockerfile | 2 ++ src/cli/commands/arbos-upgrade.ts | 4 +++- src/cli/router.ts | 6 ++---- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml index d5e2d674..b22fe35c 100644 --- a/.github/workflows/publish-docker.yml +++ b/.github/workflows/publish-docker.yml @@ -35,6 +35,7 @@ jobs: id: meta uses: docker/metadata-action@v5 with: + # "orbit" is a deprecated term; the published image uses "chain-actions" images: offchainlabs/chain-actions tags: | type=raw,value=latest,enable={{is_default_branch}} diff --git a/Dockerfile b/Dockerfile index 864e3ada..28512779 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,6 +23,8 @@ COPY package.json yarn.lock ./ RUN yarn install --frozen-lockfile --ignore-scripts COPY . . +# forge install can't run here: it clones git submodules, but .dockerignore +# excludes .git/. CI runs forge install on the host so lib/ is copied in above. RUN forge build RUN yarn build:cli diff --git a/src/cli/commands/arbos-upgrade.ts b/src/cli/commands/arbos-upgrade.ts index 998fc3a2..635c74a5 100644 --- a/src/cli/commands/arbos-upgrade.ts +++ b/src/cli/commands/arbos-upgrade.ts @@ -30,7 +30,9 @@ const ARB_SYS = '0x0000000000000000000000000000000000000064' // Nitro ArbOS versions are offset by 55 to avoid collision with classic (pre-Nitro) versions const ARBOS_VERSION_OFFSET = 55 -const PERFORM_SELECTOR = new Interface(['function perform()']).getFunction('perform')!.selector +const PERFORM_SELECTOR = new Interface(['function perform()']).getFunction( + 'perform' +)!.selector function checkDeployScript(): void { if (!fs.existsSync(DEPLOY_SCRIPT)) { diff --git a/src/cli/router.ts b/src/cli/router.ts index 606e9821..e98fc6f2 100644 --- a/src/cli/router.ts +++ b/src/cli/router.ts @@ -178,11 +178,9 @@ export async function router( case 'deploy-execute-verify': { const version = args[0] if (!version) { - console.error(`Error: ArbOS version required`) - console.error( - `Usage: arbos-upgrades/at-timestamp/${basename} [options]` + die( + `ArbOS version required\nUsage: arbos-upgrades/at-timestamp/${basename} [options]` ) - process.exit(1) } const restArgs = args.slice(1) const restOptions = parseOptions(restArgs) From 94bef2813bdd9d8ace9708c40a5eeabc472eaa1b Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Tue, 10 Mar 2026 11:39:46 +0000 Subject: [PATCH 28/44] refactor: remove CLI auth args and combined command, use FOUNDRY_* env vars Remove custom auth flag parsing (--private-key, --account, --ledger, etc.) and the deploy-execute-verify combined command. Forge/cast auth and behavior is now configured entirely via FOUNDRY_*/ETH_* env vars passed through process.env. - Delete src/cli/utils/auth.ts - Simplify ForgeScriptOptions to {script, rpcUrl, env?} - Remove authArgs from CastSendOptions - Gate arbos cast send on FOUNDRY_BROADCAST env var - Remove parseOptions and combined command routing - Update env templates with FOUNDRY_* vars - Update README with env var docs and separate-step workflow --- README.md | 65 +++++-- .../env-templates/.env.local-upgrade.example | 8 + .../.env.sepolia-upgrade.example | 8 + .../.env.arbsepolia-upgrade.example | 9 + .../env-templates/.env.local-upgrade.example | 9 + .../.env.sepolia-upgrade.example | 9 + .../contract-upgrades/2.1.0/.env.sample | 9 +- .../contract-upgrades/2.1.2/.env.sample | 9 +- .../contract-upgrades/2.1.3/.env.sample | 10 +- src/cli/commands/arbos-upgrade.ts | 170 ++++-------------- src/cli/commands/contract-upgrade.ts | 165 ++--------------- src/cli/index.ts | 1 - src/cli/router.ts | 106 +++-------- src/cli/utils/auth.ts | 89 --------- src/cli/utils/forge.ts | 44 +---- 15 files changed, 187 insertions(+), 524 deletions(-) delete mode 100644 src/cli/utils/auth.ts diff --git a/README.md b/README.md index 384fad55..458dc22f 100644 --- a/README.md +++ b/README.md @@ -140,22 +140,57 @@ The `orbit-actions` CLI provides a guided interface for running upgrade scripts. ```bash # Browse available scripts -yarn orbit-actions # List top-level directories -yarn orbit-actions contract-upgrades # List versions -yarn orbit-actions contract-upgrades/1.2.1 # List contents + commands +yarn cli # List top-level directories +yarn cli -- contract-upgrades # List versions +yarn cli -- contract-upgrades/1.2.1 # List contents + commands # View files -yarn orbit-actions contract-upgrades/1.2.1/README.md +yarn cli -- contract-upgrades/1.2.1/README.md -# Run contract upgrades -yarn orbit-actions contract-upgrades/1.2.1/deploy-execute-verify --dry-run -yarn orbit-actions contract-upgrades/1.2.1/deploy --private-key $KEY +# Run contract upgrade steps individually +yarn cli -- contract-upgrades/1.2.1/deploy +yarn cli -- contract-upgrades/1.2.1/execute +yarn cli -- contract-upgrades/1.2.1/verify -# Run ArbOS upgrades -yarn orbit-actions arbos-upgrades/at-timestamp/deploy-execute-verify 32 --dry-run +# Run ArbOS upgrade steps individually +yarn cli -- arbos-upgrades/at-timestamp/deploy 32 +yarn cli -- arbos-upgrades/at-timestamp/execute +yarn cli -- arbos-upgrades/at-timestamp/verify ``` -Run `yarn orbit-actions help` for full usage details. The CLI reads configuration from a `.env` file in the working directory. +Run `yarn cli -- help` for full usage details. + +### Configuration + +The CLI reads chain-specific configuration (RPC URLs, contract addresses) from a `.env` file in the project root. See env templates in each version directory for examples. + +Forge behavior -- broadcasting, authentication, verbosity, verification -- is controlled via standard `FOUNDRY_*` / `ETH_*` env vars in the same `.env` file. The CLI passes `process.env` through to forge, so any env var forge recognizes will work. + +Common forge env vars: + +| Variable | Effect | +|----------|--------| +| `FOUNDRY_BROADCAST=true` | Broadcast transactions (without this, scripts run in simulation) | +| `FOUNDRY_SLOW=true` | Wait for each tx to be confirmed before sending the next | +| `FOUNDRY_VERBOSITY=3` | Equivalent to `-vvv` | +| `FOUNDRY_VERIFY=true` | Verify contracts on block explorer after deploy | +| `ETH_PRIVATE_KEY=0x...` | Private key for signing transactions | + +### Full upgrade flow + +The deploy, execute, and verify steps are run separately. This allows multisig users to submit Safe approvals between steps, and EOA users can chain the commands: + +```bash +# 1. Deploy the upgrade action contract +yarn cli -- contract-upgrades/2.1.3/deploy +# Note the deployed action address printed at the end + +# 2. Set UPGRADE_ACTION_ADDRESS in .env, then execute +yarn cli -- contract-upgrades/2.1.3/execute + +# 3. Verify the upgrade +yarn cli -- contract-upgrades/2.1.3/verify +``` ## Docker @@ -172,10 +207,16 @@ docker run --rm \ # Browse upgrade scripts docker run --rm offchainlabs/orbit-actions contract-upgrades -# Run upgrade with env file and capture broadcast output +# Deploy with env file (simulation mode -- no FOUNDRY_BROADCAST) docker run --rm \ -v $(pwd)/.env:/app/.env \ -v $(pwd)/broadcast:/app/broadcast \ offchainlabs/orbit-actions \ - contract-upgrades/1.2.1/deploy-execute-verify --dry-run + contract-upgrades/2.1.3/deploy + +# Execute with broadcasting enabled +docker run --rm \ + -v $(pwd)/.env:/app/.env \ + offchainlabs/orbit-actions \ + contract-upgrades/2.1.3/execute ``` diff --git a/scripts/foundry/arbos-upgrades/at-timestamp/env-templates/.env.local-upgrade.example b/scripts/foundry/arbos-upgrades/at-timestamp/env-templates/.env.local-upgrade.example index 2ceb824e..b9b9535b 100644 --- a/scripts/foundry/arbos-upgrades/at-timestamp/env-templates/.env.local-upgrade.example +++ b/scripts/foundry/arbos-upgrades/at-timestamp/env-templates/.env.local-upgrade.example @@ -1,3 +1,11 @@ +## Forge configuration (uncomment/adjust as needed) +# FOUNDRY_BROADCAST=true +# FOUNDRY_SLOW=true +# FOUNDRY_VERBOSITY=3 +# ETH_PRIVATE_KEY=0x... + +## Chain and contract addresses +CHILD_CHAIN_RPC= UPGRADE_ACTION_ADDRESS="0x217788c286797D56Cd59aF5e493f3699C39cbbe8" CHILD_UPGRADE_EXECUTOR_ADDRESS="0x6A17B0D4EA37c4F519caCf776450cb42199Df0A4" ARBOS_VERSION=20 diff --git a/scripts/foundry/arbos-upgrades/at-timestamp/env-templates/.env.sepolia-upgrade.example b/scripts/foundry/arbos-upgrades/at-timestamp/env-templates/.env.sepolia-upgrade.example index 821f2393..ce300d14 100644 --- a/scripts/foundry/arbos-upgrades/at-timestamp/env-templates/.env.sepolia-upgrade.example +++ b/scripts/foundry/arbos-upgrades/at-timestamp/env-templates/.env.sepolia-upgrade.example @@ -1,3 +1,11 @@ +## Forge configuration (uncomment/adjust as needed) +# FOUNDRY_BROADCAST=true +# FOUNDRY_SLOW=true +# FOUNDRY_VERBOSITY=3 +# ETH_PRIVATE_KEY=0x... + +## Chain and contract addresses +CHILD_CHAIN_RPC= UPGRADE_ACTION_ADDRESS="0x7A132A8130a0C2dCeABd1FDb42ed01FCf6B9494a" CHILD_UPGRADE_EXECUTOR_ADDRESS="0x059EF6e2CaA4d779e087273646EfF49ef45dBD81" ARBOS_VERSION=20 diff --git a/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.arbsepolia-upgrade.example b/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.arbsepolia-upgrade.example index e315f603..72eb4da9 100644 --- a/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.arbsepolia-upgrade.example +++ b/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.arbsepolia-upgrade.example @@ -1,3 +1,12 @@ +## Forge configuration (uncomment/adjust as needed) +# FOUNDRY_BROADCAST=true +# FOUNDRY_SLOW=true +# FOUNDRY_VERBOSITY=3 +# ETH_PRIVATE_KEY=0x... +# FOUNDRY_VERIFY=true + +## Chain and contract addresses +PARENT_CHAIN_RPC= WASM_MODULE_ROOT="0x8b104a2e80ac6165dc58b9048de12f301d70b02a0ab51396c22b4b4b802a16a4" PARENT_CHAIN_IS_ARBITRUM=true IS_FEE_TOKEN_CHAIN=false diff --git a/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.local-upgrade.example b/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.local-upgrade.example index 2556ed03..2b55d480 100644 --- a/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.local-upgrade.example +++ b/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.local-upgrade.example @@ -1,3 +1,12 @@ +## Forge configuration (uncomment/adjust as needed) +# FOUNDRY_BROADCAST=true +# FOUNDRY_SLOW=true +# FOUNDRY_VERBOSITY=3 +# ETH_PRIVATE_KEY=0x... +# FOUNDRY_VERIFY=true + +## Chain and contract addresses +PARENT_CHAIN_RPC= WASM_MODULE_ROOT="0x8b104a2e80ac6165dc58b9048de12f301d70b02a0ab51396c22b4b4b802a16a4" PARENT_CHAIN_IS_ARBITRUM=true IS_FEE_TOKEN_CHAIN=false diff --git a/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.sepolia-upgrade.example b/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.sepolia-upgrade.example index c5999a05..cb093990 100644 --- a/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.sepolia-upgrade.example +++ b/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.sepolia-upgrade.example @@ -1,3 +1,12 @@ +## Forge configuration (uncomment/adjust as needed) +# FOUNDRY_BROADCAST=true +# FOUNDRY_SLOW=true +# FOUNDRY_VERBOSITY=3 +# ETH_PRIVATE_KEY=0x... +# FOUNDRY_VERIFY=true + +## Chain and contract addresses +PARENT_CHAIN_RPC= WASM_MODULE_ROOT="0x8b104a2e80ac6165dc58b9048de12f301d70b02a0ab51396c22b4b4b802a16a4" PARENT_CHAIN_IS_ARBITRUM=false IS_FEE_TOKEN_CHAIN=false diff --git a/scripts/foundry/contract-upgrades/2.1.0/.env.sample b/scripts/foundry/contract-upgrades/2.1.0/.env.sample index 9521eef3..aabadbca 100644 --- a/scripts/foundry/contract-upgrades/2.1.0/.env.sample +++ b/scripts/foundry/contract-upgrades/2.1.0/.env.sample @@ -1,5 +1,12 @@ -## These env vars are used for ExecuteNitroContracts2Point1Point0UpgradeScript +## Forge configuration (uncomment/adjust as needed) +# FOUNDRY_BROADCAST=true +# FOUNDRY_SLOW=true +# FOUNDRY_VERBOSITY=3 +# ETH_PRIVATE_KEY=0x... +# FOUNDRY_VERIFY=true +## Chain and contract addresses +PARENT_CHAIN_RPC= UPGRADE_ACTION_ADDRESS= INBOX_ADDRESS= PROXY_ADMIN_ADDRESS= diff --git a/scripts/foundry/contract-upgrades/2.1.2/.env.sample b/scripts/foundry/contract-upgrades/2.1.2/.env.sample index eba03a66..cfb1757d 100644 --- a/scripts/foundry/contract-upgrades/2.1.2/.env.sample +++ b/scripts/foundry/contract-upgrades/2.1.2/.env.sample @@ -1,5 +1,12 @@ -## These env vars are used for ExecuteNitroContracts2Point1Point2UpgradeScript +## Forge configuration (uncomment/adjust as needed) +# FOUNDRY_BROADCAST=true +# FOUNDRY_SLOW=true +# FOUNDRY_VERBOSITY=3 +# ETH_PRIVATE_KEY=0x... +# FOUNDRY_VERIFY=true +## Chain and contract addresses +PARENT_CHAIN_RPC= UPGRADE_ACTION_ADDRESS= INBOX_ADDRESS= PROXY_ADMIN_ADDRESS= diff --git a/scripts/foundry/contract-upgrades/2.1.3/.env.sample b/scripts/foundry/contract-upgrades/2.1.3/.env.sample index c189ee2f..3969f14e 100644 --- a/scripts/foundry/contract-upgrades/2.1.3/.env.sample +++ b/scripts/foundry/contract-upgrades/2.1.3/.env.sample @@ -1,4 +1,12 @@ -## These env vars are used for ExecuteNitroContracts2Point1Point2UpgradeScript +## Forge configuration (uncomment/adjust as needed) +# FOUNDRY_BROADCAST=true +# FOUNDRY_SLOW=true +# FOUNDRY_VERBOSITY=3 +# ETH_PRIVATE_KEY=0x... +# FOUNDRY_VERIFY=true + +## Chain and contract addresses +PARENT_CHAIN_RPC= PARENT_CHAIN_IS_ARBITRUM=true MAX_DATA_SIZE=104857 UPGRADE_ACTION_ADDRESS= diff --git a/src/cli/commands/arbos-upgrade.ts b/src/cli/commands/arbos-upgrade.ts index 635c74a5..41285aea 100644 --- a/src/cli/commands/arbos-upgrade.ts +++ b/src/cli/commands/arbos-upgrade.ts @@ -2,13 +2,7 @@ import * as path from 'path' import * as fs from 'fs' import { Interface } from 'ethers' import { log, die } from '../utils/log' -import { requireEnv, getEnv, getScriptsDir } from '../utils/env' -import { - parseAuthArgs, - createDeployExecuteAuth, - getDeployAuth, - getExecuteAuth, -} from '../utils/auth' +import { requireEnv, getScriptsDir } from '../utils/env' import { runForgeScript, runCastSend, @@ -42,19 +36,13 @@ function checkDeployScript(): void { async function deployAction( version: string, - rpcUrl: string, - authArgs: string, - options: { broadcast: boolean; verify?: boolean } + rpcUrl: string ): Promise { checkDeployScript() await runForgeScript({ script: DEPLOY_SCRIPT, rpcUrl, - authArgs, - broadcast: options.broadcast, - verify: options.verify, - slow: true, env: { ARBOS_VERSION: version }, }) } @@ -62,34 +50,28 @@ async function deployAction( async function executeUpgrade( actionAddress: string, upgradeExecutor: string, - rpcUrl: string, - authArgs: string, - dryRun: boolean + rpcUrl: string ): Promise { - if (dryRun || !authArgs) { - const executeCalldata = await castCalldata( - 'execute(address,bytes)', - actionAddress, - PERFORM_SELECTOR - ) - - log( - dryRun - ? 'Dry run - calldata for UpgradeExecutor.execute():' - : 'Calldata for UpgradeExecutor.execute():' - ) - console.log('') - console.log(`To: ${upgradeExecutor}`) - console.log(`Calldata: ${executeCalldata}`) - console.log('') - log('Submit this to your multisig/Safe to execute the upgrade') - } else { + const executeCalldata = await castCalldata( + 'execute(address,bytes)', + actionAddress, + PERFORM_SELECTOR + ) + + log('Calldata for UpgradeExecutor.execute():') + console.log('') + console.log(`To: ${upgradeExecutor}`) + console.log(`Calldata: ${executeCalldata}`) + console.log('') + log('Submit this to your multisig/Safe to execute the upgrade') + + if (process.env.FOUNDRY_BROADCAST) { + log('Broadcasting transaction...') await runCastSend({ to: upgradeExecutor, sig: 'execute(address,bytes)', args: [actionAddress, PERFORM_SELECTOR], rpcUrl, - authArgs, }) log('ArbOS upgrade scheduled successfully') @@ -123,25 +105,27 @@ async function verifyUpgrade(rpcUrl: string): Promise { log(`Current ArbOS version: ${currentVersion}`) } -async function cmdDeploy(version: string, args: string[]): Promise { - const authArgs = parseAuthArgs(args) +async function cmdDeploy(version: string): Promise { const rpcUrl = requireEnv('CHILD_CHAIN_RPC') log(`Running: ${path.basename(DEPLOY_SCRIPT)} for ArbOS ${version}`) - await deployAction(version, rpcUrl, authArgs, { - broadcast: Boolean(authArgs), - }) -} + await deployAction(version, rpcUrl) -async function cmdExecute(args: string[]): Promise { - const authArgs = parseAuthArgs(args) + const chainId = await getChainId(rpcUrl) + const address = parseActionAddress(DEPLOY_SCRIPT, chainId) + if (address) { + log(`Deployed action address: ${address}`) + log('Set UPGRADE_ACTION_ADDRESS in .env for the execute step') + } +} +async function cmdExecute(): Promise { const rpcUrl = requireEnv('CHILD_CHAIN_RPC') const upgradeExecutor = requireEnv('CHILD_UPGRADE_EXECUTOR_ADDRESS') const actionAddress = requireEnv('UPGRADE_ACTION_ADDRESS') log(`Executing ArbOS upgrade action: ${actionAddress}`) - await executeUpgrade(actionAddress, upgradeExecutor, rpcUrl, authArgs, false) + await executeUpgrade(actionAddress, upgradeExecutor, rpcUrl) } async function cmdVerify(): Promise { @@ -149,98 +133,4 @@ async function cmdVerify(): Promise { await verifyUpgrade(rpcUrl) } -async function cmdDeployExecuteVerify( - version: string, - options: { - deployKey?: string - deployAccount?: string - deployLedger?: boolean - deployInteractive?: boolean - executeKey?: string - executeAccount?: string - executeLedger?: boolean - executeInteractive?: boolean - dryRun?: boolean - skipExecute?: boolean - verify?: boolean - } -): Promise { - const auth = createDeployExecuteAuth(options) - - log(`ArbOS version: ${version}`) - checkDeployScript() - - const skipDeploy = Boolean(getEnv('UPGRADE_ACTION_ADDRESS')) - let upgradeActionAddress = getEnv('UPGRADE_ACTION_ADDRESS') || '' - - if (skipDeploy) { - log(`Using existing action from .env: ${upgradeActionAddress}`) - } - - const rpcUrl = requireEnv('CHILD_CHAIN_RPC') - const upgradeExecutor = requireEnv('CHILD_UPGRADE_EXECUTOR_ADDRESS') - requireEnv('SCHEDULE_TIMESTAMP') - - const deployAuth = getDeployAuth(auth) - const executeAuth = getExecuteAuth(auth) - - if (!skipDeploy && !auth.dryRun && !deployAuth) { - die( - 'Deploy auth required. Use --deploy-key, --deploy-account, --deploy-ledger, or --deploy-interactive' - ) - } - if (!auth.skipExecute && !auth.dryRun && !executeAuth) { - die( - 'Execute auth required. Use --execute-key, --execute-account, --execute-ledger, or --execute-interactive' - ) - } - - log(`Scheduled timestamp: ${process.env.SCHEDULE_TIMESTAMP}`) - - let chainId = '' - if (!skipDeploy) { - chainId = await getChainId(rpcUrl) - log(`Target chain ID: ${chainId}`) - log('Step 1: Deploying ArbOS upgrade action...') - - await deployAction(version, rpcUrl, deployAuth, { - broadcast: !auth.dryRun, - verify: auth.verifyContracts, - }) - - if (!auth.dryRun) { - upgradeActionAddress = parseActionAddress(DEPLOY_SCRIPT, chainId) - log(`Deployed action at: ${upgradeActionAddress}`) - } else { - log('Dry run - no action deployed') - if (!auth.skipExecute) { - log('Note: Set UPGRADE_ACTION_ADDRESS in .env to run execute step') - return - } - } - } else { - log('Step 1: Skipped deploy') - } - - if (!auth.skipExecute) { - log('Step 2: Executing ArbOS upgrade...') - await executeUpgrade( - upgradeActionAddress, - upgradeExecutor, - rpcUrl, - executeAuth, - auth.dryRun - ) - } else { - log('Step 2: Skipped execute') - } - - if (!auth.dryRun && !auth.skipExecute) { - log('Step 3: Verifying scheduled upgrade...') - await verifyUpgrade(rpcUrl) - } - - log('Done') -} - -export { cmdDeploy, cmdExecute, cmdVerify, cmdDeployExecuteVerify } +export { cmdDeploy, cmdExecute, cmdVerify } diff --git a/src/cli/commands/contract-upgrade.ts b/src/cli/commands/contract-upgrade.ts index 2d9cb3b6..33066665 100644 --- a/src/cli/commands/contract-upgrade.ts +++ b/src/cli/commands/contract-upgrade.ts @@ -1,13 +1,7 @@ import * as path from 'path' import * as fs from 'fs' import { log, die } from '../utils/log' -import { requireEnv, getEnv, getScriptsDir } from '../utils/env' -import { - parseAuthArgs, - createDeployExecuteAuth, - getDeployAuth, - getExecuteAuth, -} from '../utils/auth' +import { requireEnv, getScriptsDir } from '../utils/env' import { runForgeScript, getChainId, @@ -31,9 +25,8 @@ function getVersionDir(version: string): string { return versionDir } -async function cmdDeploy(version: string, args: string[]): Promise { +async function cmdDeploy(version: string): Promise { const versionDir = getVersionDir(version) - const authArgs = parseAuthArgs(args) const rpcUrl = requireEnv('PARENT_CHAIN_RPC') @@ -47,16 +40,18 @@ async function cmdDeploy(version: string, args: string[]): Promise { await runForgeScript({ script: deployScript, rpcUrl, - authArgs, - broadcast: Boolean(authArgs), - slow: true, - skipSimulation: true, }) + + const chainId = await getChainId(rpcUrl) + const address = parseActionAddress(deployScript, chainId) + if (address) { + log(`Deployed action address: ${address}`) + log('Set UPGRADE_ACTION_ADDRESS in .env for the execute step') + } } -async function cmdExecute(version: string, args: string[]): Promise { +async function cmdExecute(version: string): Promise { const versionDir = getVersionDir(version) - const authArgs = parseAuthArgs(args) const rpcUrl = requireEnv('PARENT_CHAIN_RPC') requireEnv('UPGRADE_ACTION_ADDRESS') @@ -71,8 +66,6 @@ async function cmdExecute(version: string, args: string[]): Promise { await runForgeScript({ script: executeScript, rpcUrl, - authArgs, - broadcast: Boolean(authArgs), }) } @@ -96,140 +89,4 @@ async function cmdVerify(version: string): Promise { }) } -async function cmdDeployExecuteVerify( - version: string, - options: { - deployKey?: string - deployAccount?: string - deployLedger?: boolean - deployInteractive?: boolean - executeKey?: string - executeAccount?: string - executeLedger?: boolean - executeInteractive?: boolean - dryRun?: boolean - skipExecute?: boolean - verify?: boolean - } -): Promise { - const versionDir = getVersionDir(version) - const auth = createDeployExecuteAuth(options) - - const deployScript = findScript(versionDir, /^Deploy.*\.s\.sol$/) - const executeScript = findScript(versionDir, /^Execute.*\.s\.sol$/) - - if (!deployScript) { - die(`No deploy script found in ${versionDir}`) - } - if (!executeScript) { - die(`No execute script found in ${versionDir}`) - } - - log(`Version: ${version}`) - log(`Deploy script: ${path.basename(deployScript)}`) - log(`Execute script: ${path.basename(executeScript)}`) - - // Auto-detect skip_deploy if UPGRADE_ACTION_ADDRESS is set - const skipDeploy = Boolean(getEnv('UPGRADE_ACTION_ADDRESS')) - let upgradeActionAddress = getEnv('UPGRADE_ACTION_ADDRESS') || '' - - if (skipDeploy) { - log(`Using existing action from .env: ${upgradeActionAddress}`) - } - - const rpcUrl = requireEnv('PARENT_CHAIN_RPC') - requireEnv('INBOX_ADDRESS') - requireEnv('PROXY_ADMIN_ADDRESS') - requireEnv('PARENT_UPGRADE_EXECUTOR_ADDRESS') - - const deployAuth = getDeployAuth(auth) - const executeAuth = getExecuteAuth(auth) - - if (!skipDeploy && !auth.dryRun && !deployAuth) { - die( - 'Deploy auth required. Use --deploy-key, --deploy-account, --deploy-ledger, or --deploy-interactive' - ) - } - if (!auth.skipExecute && !auth.dryRun && !executeAuth) { - die( - 'Execute auth required. Use --execute-key, --execute-account, --execute-ledger, or --execute-interactive' - ) - } - - const chainId = await getChainId(rpcUrl) - log(`Target chain ID: ${chainId}`) - - if (!skipDeploy) { - log('Step 1: Deploying upgrade action...') - - await runForgeScript({ - script: deployScript, - rpcUrl, - authArgs: deployAuth, - broadcast: !auth.dryRun, - verify: auth.verifyContracts, - slow: true, - skipSimulation: true, - }) - - if (!auth.dryRun) { - upgradeActionAddress = parseActionAddress(deployScript, chainId) - log(`Deployed action at: ${upgradeActionAddress}`) - } else { - log('Dry run - no action deployed') - if (!auth.skipExecute) { - log('Note: Set UPGRADE_ACTION_ADDRESS in .env to run execute step') - return - } - } - } else { - log('Step 1: Skipped deploy') - } - - const forgeEnv = { UPGRADE_ACTION_ADDRESS: upgradeActionAddress } - - if (!auth.skipExecute) { - log('Step 2: Executing upgrade...') - - await runForgeScript({ - script: executeScript, - rpcUrl, - authArgs: executeAuth, - broadcast: !auth.dryRun, - env: forgeEnv, - }) - - if (auth.dryRun) { - log('Dry run - upgrade not executed') - } else { - log('Upgrade executed successfully') - } - } else { - log('Step 2: Skipped execute') - } - - if (!auth.dryRun && !auth.skipExecute) { - log('Step 3: Verifying upgrade...') - - const verifyScript = findScript(versionDir, /^Verify.*\.s\.sol$/) - if (verifyScript) { - await runForgeScript({ - script: verifyScript, - rpcUrl, - env: forgeEnv, - }) - } else { - log('No Verify script found - check README for manual verification') - } - } - - log('Done') -} - -export { - cmdDeploy, - cmdExecute, - cmdVerify, - cmdDeployExecuteVerify, - getVersionDir, -} +export { cmdDeploy, cmdExecute, cmdVerify, getVersionDir } diff --git a/src/cli/index.ts b/src/cli/index.ts index 6d5ee588..93339294 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -11,7 +11,6 @@ program .description('CLI for Orbit chain upgrade actions') .argument('[path]', 'Path to browse or command to run') .argument('[args...]', 'Additional arguments') - .allowUnknownOption(true) .action(async (pathArg?: string, args?: string[]) => { await router(pathArg, args) }) diff --git a/src/cli/router.ts b/src/cli/router.ts index e98fc6f2..7709df8e 100644 --- a/src/cli/router.ts +++ b/src/cli/router.ts @@ -6,13 +6,11 @@ import { cmdDeploy as contractDeploy, cmdExecute as contractExecute, cmdVerify as contractVerify, - cmdDeployExecuteVerify as contractDeployExecuteVerify, } from './commands/contract-upgrade' import { cmdDeploy as arbosDeploy, cmdExecute as arbosExecute, cmdVerify as arbosVerify, - cmdDeployExecuteVerify as arbosDeployExecuteVerify, } from './commands/arbos-upgrade' const HELP_TEXT = `Usage: orbit-actions [path] [args...] @@ -31,31 +29,22 @@ Viewing files: contract-upgrades/2.1.0/.env.sample View env sample Running upgrade scripts: - contract-upgrades//deploy [--private-key KEY] - contract-upgrades//execute [--private-key KEY] + contract-upgrades//deploy + contract-upgrades//execute contract-upgrades//verify - contract-upgrades//deploy-execute-verify [--deploy-key KEY] [--execute-key KEY] - arbos-upgrades/at-timestamp/deploy [--private-key KEY] - arbos-upgrades/at-timestamp/execute [--private-key KEY] + arbos-upgrades/at-timestamp/deploy + arbos-upgrades/at-timestamp/execute arbos-upgrades/at-timestamp/verify - arbos-upgrades/at-timestamp/deploy-execute-verify [--deploy-key KEY] [--execute-key KEY] - -Options for deploy-execute-verify: - --deploy-key KEY Private key for deploy step - --deploy-account NAME Keystore account for deploy - --deploy-ledger Use Ledger for deploy - --execute-key KEY Private key for execute step - --execute-account NAME Keystore account for execute - --execute-ledger Use Ledger for execute - --dry-run, -n Simulate without broadcasting - --skip-execute Deploy only - --verify, -v Verify on block explorer + +Forge behavior (broadcast, auth, verbosity, etc.) is configured via +FOUNDRY_* / ETH_* env vars in your .env file. See env templates for examples. Examples: docker run orbit-actions contract-upgrades/1.2.1 docker run orbit-actions contract-upgrades/1.2.1/README.md - docker run -v $(pwd)/.env:/app/.env orbit-actions contract-upgrades/1.2.1/deploy-execute-verify --dry-run` + docker run -v $(pwd)/.env:/app/.env orbit-actions contract-upgrades/1.2.1/deploy + docker run -v $(pwd)/.env:/app/.env orbit-actions contract-upgrades/1.2.1/execute` function listDirectory(dir: string): void { const scriptsDir = getScriptsDir() @@ -71,57 +60,15 @@ function listDirectory(dir: string): void { if (/^contract-upgrades\/[0-9]/.test(relPath)) { console.log('---') - console.log('deploy (run Deploy script)') - console.log('execute (run Execute script)') - console.log('verify (run Verify script)') - console.log('deploy-execute-verify (full upgrade flow)') + console.log('deploy (run Deploy script)') + console.log('execute (run Execute script)') + console.log('verify (run Verify script)') } else if (relPath === 'arbos-upgrades/at-timestamp') { console.log('---') - console.log('deploy (run Deploy script)') - console.log('execute (execute upgrade action)') - console.log('verify (check upgrade status)') - console.log('deploy-execute-verify (full upgrade flow)') - } -} - -function parseOptions(args: string[]): Record { - const options: Record = {} - for (let i = 0; i < args.length; i++) { - const arg = args[i] - if ( - arg === '--deploy-key' || - arg === '--execute-key' || - arg === '--deploy-account' || - arg === '--execute-account' - ) { - options[ - arg.replace(/^--/, '').replace(/-([a-z])/g, (_, c) => c.toUpperCase()) - ] = args[++i] || '' - } else if (arg === '--deploy-ledger') { - options.deployLedger = true - } else if (arg === '--execute-ledger') { - options.executeLedger = true - } else if (arg === '--deploy-interactive') { - options.deployInteractive = true - } else if (arg === '--execute-interactive') { - options.executeInteractive = true - } else if (arg === '--dry-run' || arg === '-n') { - options.dryRun = true - } else if (arg === '--skip-execute') { - options.skipExecute = true - } else if (arg === '--verify' || arg === '-v') { - options.verify = true - } else if (arg === '--private-key') { - options.privateKey = args[++i] || '' - } else if (arg === '--account') { - options.account = args[++i] || '' - } else if (arg === '--ledger') { - options.ledger = true - } else if (arg === '--interactive') { - options.interactive = true - } + console.log('deploy (run Deploy script)') + console.log('execute (execute upgrade action)') + console.log('verify (check upgrade status)') } - return options } export async function router( @@ -154,45 +101,34 @@ export async function router( if (/^contract-upgrades\/[0-9]/.test(relParent)) { const version = path.basename(relParent) - const options = parseOptions(args) switch (basename) { case 'deploy': - await contractDeploy(version, args) + await contractDeploy(version) return case 'execute': - await contractExecute(version, args) + await contractExecute(version) return case 'verify': await contractVerify(version) return - case 'deploy-execute-verify': - await contractDeployExecuteVerify(version, options) - return } } if (relParent === 'arbos-upgrades/at-timestamp') { switch (basename) { - case 'deploy': - case 'deploy-execute-verify': { + case 'deploy': { const version = args[0] if (!version) { die( - `ArbOS version required\nUsage: arbos-upgrades/at-timestamp/${basename} [options]` + 'ArbOS version required\nUsage: arbos-upgrades/at-timestamp/deploy ' ) } - const restArgs = args.slice(1) - const restOptions = parseOptions(restArgs) - if (basename === 'deploy') { - await arbosDeploy(version, restArgs) - } else { - await arbosDeployExecuteVerify(version, restOptions) - } + await arbosDeploy(version) return } case 'execute': - await arbosExecute(args) + await arbosExecute() return case 'verify': await arbosVerify() diff --git a/src/cli/utils/auth.ts b/src/cli/utils/auth.ts deleted file mode 100644 index 7e719674..00000000 --- a/src/cli/utils/auth.ts +++ /dev/null @@ -1,89 +0,0 @@ -export interface DeployExecuteAuth { - deployKey: string - deployAccount: string - deployLedger: boolean - deployInteractive: boolean - executeKey: string - executeAccount: string - executeLedger: boolean - executeInteractive: boolean - dryRun: boolean - skipExecute: boolean - verifyContracts: boolean -} - -export function parseAuthArgs(args: string[]): string { - for (let i = 0; i < args.length; i++) { - const arg = args[i] - if (arg === '--private-key' || arg === '--account') { - const value = args[i + 1] - if (value) { - return `${arg} ${value}` - } - } - if (arg === '--ledger' || arg === '--interactive') { - return arg - } - } - return '' -} - -export function getDeployAuth(auth: DeployExecuteAuth): string { - if (auth.deployKey) { - return `--private-key ${auth.deployKey}` - } - if (auth.deployAccount) { - return `--account ${auth.deployAccount}` - } - if (auth.deployLedger) { - return '--ledger' - } - if (auth.deployInteractive) { - return '--interactive' - } - return '' -} - -export function getExecuteAuth(auth: DeployExecuteAuth): string { - if (auth.executeKey) { - return `--private-key ${auth.executeKey}` - } - if (auth.executeAccount) { - return `--account ${auth.executeAccount}` - } - if (auth.executeLedger) { - return '--ledger' - } - if (auth.executeInteractive) { - return '--interactive' - } - return '' -} - -export function createDeployExecuteAuth(options: { - deployKey?: string - deployAccount?: string - deployLedger?: boolean - deployInteractive?: boolean - executeKey?: string - executeAccount?: string - executeLedger?: boolean - executeInteractive?: boolean - dryRun?: boolean - skipExecute?: boolean - verify?: boolean -}): DeployExecuteAuth { - return { - deployKey: options.deployKey || '', - deployAccount: options.deployAccount || '', - deployLedger: options.deployLedger || false, - deployInteractive: options.deployInteractive || false, - executeKey: options.executeKey || '', - executeAccount: options.executeAccount || '', - executeLedger: options.executeLedger || false, - executeInteractive: options.executeInteractive || false, - dryRun: options.dryRun || false, - skipExecute: options.skipExecute || false, - verifyContracts: options.verify || false, - } -} diff --git a/src/cli/utils/forge.ts b/src/cli/utils/forge.ts index 5dac75ee..b0e0fb34 100644 --- a/src/cli/utils/forge.ts +++ b/src/cli/utils/forge.ts @@ -7,12 +7,6 @@ import { getRepoRoot } from './env' export interface ForgeScriptOptions { script: string rpcUrl: string - authArgs?: string - broadcast?: boolean - verify?: boolean - slow?: boolean - skipSimulation?: boolean - verbosity?: 1 | 2 | 3 | 4 | 5 env?: Record } @@ -21,26 +15,6 @@ export async function runForgeScript( ): Promise { const args = ['script', options.script, '--rpc-url', options.rpcUrl] - if (options.slow) { - args.push('--slow') - } - - if (options.skipSimulation) { - args.push('--skip-simulation') - } - - const verbosity = options.verbosity ?? 3 - args.push('-' + 'v'.repeat(verbosity)) - - if (options.broadcast && options.authArgs) { - args.push('--broadcast') - args.push(...options.authArgs.split(' ').filter(Boolean)) - } - - if (options.verify) { - args.push('--verify') - } - log(`Running: forge ${args.slice(0, 2).join(' ')}...`) try { @@ -58,7 +32,6 @@ export interface CastSendOptions { sig: string args: string[] rpcUrl: string - authArgs?: string } export async function runCastSend(options: CastSendOptions): Promise { @@ -71,10 +44,6 @@ export async function runCastSend(options: CastSendOptions): Promise { options.rpcUrl, ] - if (options.authArgs) { - args.push(...options.authArgs.split(' ').filter(Boolean)) - } - try { await execa('cast', args, { stdio: 'inherit', @@ -121,7 +90,7 @@ export async function getChainId(rpcUrl: string): Promise { export function parseActionAddress( scriptPath: string, chainId: string -): string { +): string | null { const scriptName = path.basename(scriptPath) const repoRoot = getRepoRoot() const broadcastFile = path.join( @@ -133,7 +102,7 @@ export function parseActionAddress( ) if (!fs.existsSync(broadcastFile)) { - die(`Broadcast file not found: ${broadcastFile}`) + return null } const content = JSON.parse(fs.readFileSync(broadcastFile, 'utf-8')) @@ -142,15 +111,10 @@ export function parseActionAddress( ) if (!createTxs || createTxs.length === 0) { - die('Could not parse action address from broadcast file') - } - - const address = createTxs[createTxs.length - 1]?.contractAddress - if (!address) { - die('Could not parse action address from broadcast file') + return null } - return address + return createTxs[createTxs.length - 1]?.contractAddress ?? null } export function findScript(dir: string, pattern: RegExp): string | null { From 376542215c8caf87b532637a90b3493384f47c8b Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Tue, 10 Mar 2026 12:57:52 +0000 Subject: [PATCH 29/44] fix: address PR review feedback on CLI UX and error handling - Log full forge args instead of truncating for easier debugging - runCastCall uses die() on failure instead of returning 'N/A' - Format scheduled upgrade output as (version, timestamp) tuple - Add contextual help text at every CLI browsing level --- src/cli/commands/arbos-upgrade.ts | 10 ++---- src/cli/router.ts | 57 ++++++++++++++++++++++++------- src/cli/utils/forge.ts | 4 +-- 3 files changed, 49 insertions(+), 22 deletions(-) diff --git a/src/cli/commands/arbos-upgrade.ts b/src/cli/commands/arbos-upgrade.ts index 41285aea..d3829d20 100644 --- a/src/cli/commands/arbos-upgrade.ts +++ b/src/cli/commands/arbos-upgrade.ts @@ -86,7 +86,7 @@ async function verifyUpgrade(rpcUrl: string): Promise { sig: 'getScheduledUpgrade()(uint64,uint64)', rpcUrl, }) - log(`Scheduled upgrade (version, timestamp): ${scheduled}`) + log(`Scheduled upgrade (version, timestamp): (${scheduled.replace('\n', ', ')})`) const currentRaw = await runCastCall({ to: ARB_SYS, @@ -94,13 +94,7 @@ async function verifyUpgrade(rpcUrl: string): Promise { rpcUrl, }) - let currentVersion: number - if (currentRaw === 'N/A') { - currentVersion = 0 - } else { - const rawNum = parseInt(currentRaw, 10) - currentVersion = rawNum - ARBOS_VERSION_OFFSET - } + const currentVersion = parseInt(currentRaw, 10) - ARBOS_VERSION_OFFSET log(`Current ArbOS version: ${currentVersion}`) } diff --git a/src/cli/router.ts b/src/cli/router.ts index 7709df8e..6277fb2c 100644 --- a/src/cli/router.ts +++ b/src/cli/router.ts @@ -51,23 +51,44 @@ function listDirectory(dir: string): void { let relPath = path.relative(scriptsDir, dir) if (relPath === '.') relPath = '' + const isVersionDir = /^contract-upgrades\/[0-9]/.test(relPath) + const isArbosDir = relPath === 'arbos-upgrades/at-timestamp' + const isCategoryDir = /^[^/]+$/.test(relPath) && relPath !== '' + + if (isCategoryDir) { + console.log('Browse deeper to see available scripts and commands.') + } else if (isVersionDir || isArbosDir) { + const hasEnvTemplates = fs.existsSync(path.join(dir, 'env-templates')) + if (hasEnvTemplates) { + console.log('Configure .env before running. See env-templates/ for examples.') + } else { + console.log('Configure .env before running. See the README for details.') + } + } + console.log('') + const contents = fs.readdirSync(dir) for (const item of contents) { if (!item.startsWith('.')) { - console.log(item) + console.log(` ${item}`) } } - if (/^contract-upgrades\/[0-9]/.test(relPath)) { - console.log('---') - console.log('deploy (run Deploy script)') - console.log('execute (run Execute script)') - console.log('verify (run Verify script)') - } else if (relPath === 'arbos-upgrades/at-timestamp') { - console.log('---') - console.log('deploy (run Deploy script)') - console.log('execute (execute upgrade action)') - console.log('verify (check upgrade status)') + if (isCategoryDir) { + console.log('') + console.log(`Example: orbit-actions ${relPath}/${contents.find(c => !c.startsWith('.')) ?? ''}`) + } else if (isVersionDir) { + console.log('') + console.log('Commands:') + console.log(` orbit-actions ${relPath}/deploy`) + console.log(` orbit-actions ${relPath}/execute`) + console.log(` orbit-actions ${relPath}/verify`) + } else if (isArbosDir) { + console.log('') + console.log('Commands:') + console.log(` orbit-actions ${relPath}/deploy `) + console.log(` orbit-actions ${relPath}/execute`) + console.log(` orbit-actions ${relPath}/verify`) } } @@ -78,12 +99,24 @@ export async function router( const scriptsDir = getScriptsDir() if (!pathArg) { + console.log('orbit-actions - CLI for Orbit chain upgrade actions') + console.log('') + console.log('Browse and run upgrade scripts for Orbit chains. Configuration') + console.log('is read from .env in the project root. Forge behavior (broadcast,') + console.log('auth, verbosity) is controlled via FOUNDRY_* / ETH_* env vars.') + console.log('') + console.log('Available:') const contents = fs.readdirSync(scriptsDir) for (const item of contents) { if (!item.startsWith('.')) { - console.log(item) + console.log(` ${item}/`) } } + console.log('') + console.log('Usage:') + console.log(' orbit-actions Browse scripts') + console.log(' orbit-actions /deploy Run a script') + console.log(' orbit-actions help Full usage details') return } diff --git a/src/cli/utils/forge.ts b/src/cli/utils/forge.ts index b0e0fb34..f8addf26 100644 --- a/src/cli/utils/forge.ts +++ b/src/cli/utils/forge.ts @@ -15,7 +15,7 @@ export async function runForgeScript( ): Promise { const args = ['script', options.script, '--rpc-url', options.rpcUrl] - log(`Running: forge ${args.slice(0, 2).join(' ')}...`) + log(`Running: forge ${args.join(' ')}`) try { await execa('forge', args, { @@ -70,7 +70,7 @@ export async function runCastCall(options: CastCallOptions): Promise { ]) return result.stdout } catch { - return 'N/A' + die(`cast call failed: ${options.sig} on ${options.to}`) } } From 54c190c83363fb8829381525df19c4599f3e49ed Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Tue, 10 Mar 2026 13:12:16 +0000 Subject: [PATCH 30/44] refactor: simplify env templates and remove command prefix from help text - Trim env templates to FOUNDRY_BROADCAST and ETH_PRIVATE_KEY, link to Foundry config docs for the full list of supported env vars - Remove orbit-actions prefix from CLI help text so it reads correctly regardless of invocation method (yarn cli, Docker, linked binary) - Update README env var table to match --- README.md | 7 ++--- .../env-templates/.env.local-upgrade.example | 3 +- .../.env.sepolia-upgrade.example | 3 +- .../.env.arbsepolia-upgrade.example | 4 +-- .../env-templates/.env.local-upgrade.example | 4 +-- .../.env.sepolia-upgrade.example | 4 +-- .../contract-upgrades/2.1.0/.env.sample | 4 +-- .../contract-upgrades/2.1.2/.env.sample | 4 +-- .../contract-upgrades/2.1.3/.env.sample | 4 +-- src/cli/router.ts | 31 +++++++------------ 10 files changed, 23 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 458dc22f..6ecaa030 100644 --- a/README.md +++ b/README.md @@ -166,16 +166,15 @@ The CLI reads chain-specific configuration (RPC URLs, contract addresses) from a Forge behavior -- broadcasting, authentication, verbosity, verification -- is controlled via standard `FOUNDRY_*` / `ETH_*` env vars in the same `.env` file. The CLI passes `process.env` through to forge, so any env var forge recognizes will work. -Common forge env vars: +Key forge env vars: | Variable | Effect | |----------|--------| | `FOUNDRY_BROADCAST=true` | Broadcast transactions (without this, scripts run in simulation) | -| `FOUNDRY_SLOW=true` | Wait for each tx to be confirmed before sending the next | -| `FOUNDRY_VERBOSITY=3` | Equivalent to `-vvv` | -| `FOUNDRY_VERIFY=true` | Verify contracts on block explorer after deploy | | `ETH_PRIVATE_KEY=0x...` | Private key for signing transactions | +All `FOUNDRY_*` env vars are supported -- see [Foundry configuration](https://book.getfoundry.sh/reference/config/) for the full list. + ### Full upgrade flow The deploy, execute, and verify steps are run separately. This allows multisig users to submit Safe approvals between steps, and EOA users can chain the commands: diff --git a/scripts/foundry/arbos-upgrades/at-timestamp/env-templates/.env.local-upgrade.example b/scripts/foundry/arbos-upgrades/at-timestamp/env-templates/.env.local-upgrade.example index b9b9535b..1f91f266 100644 --- a/scripts/foundry/arbos-upgrades/at-timestamp/env-templates/.env.local-upgrade.example +++ b/scripts/foundry/arbos-upgrades/at-timestamp/env-templates/.env.local-upgrade.example @@ -1,7 +1,6 @@ ## Forge configuration (uncomment/adjust as needed) +## All FOUNDRY_* env vars are supported: https://book.getfoundry.sh/reference/config/ # FOUNDRY_BROADCAST=true -# FOUNDRY_SLOW=true -# FOUNDRY_VERBOSITY=3 # ETH_PRIVATE_KEY=0x... ## Chain and contract addresses diff --git a/scripts/foundry/arbos-upgrades/at-timestamp/env-templates/.env.sepolia-upgrade.example b/scripts/foundry/arbos-upgrades/at-timestamp/env-templates/.env.sepolia-upgrade.example index ce300d14..f39d61c2 100644 --- a/scripts/foundry/arbos-upgrades/at-timestamp/env-templates/.env.sepolia-upgrade.example +++ b/scripts/foundry/arbos-upgrades/at-timestamp/env-templates/.env.sepolia-upgrade.example @@ -1,7 +1,6 @@ ## Forge configuration (uncomment/adjust as needed) +## All FOUNDRY_* env vars are supported: https://book.getfoundry.sh/reference/config/ # FOUNDRY_BROADCAST=true -# FOUNDRY_SLOW=true -# FOUNDRY_VERBOSITY=3 # ETH_PRIVATE_KEY=0x... ## Chain and contract addresses diff --git a/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.arbsepolia-upgrade.example b/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.arbsepolia-upgrade.example index 72eb4da9..260b5a5e 100644 --- a/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.arbsepolia-upgrade.example +++ b/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.arbsepolia-upgrade.example @@ -1,9 +1,7 @@ ## Forge configuration (uncomment/adjust as needed) +## All FOUNDRY_* env vars are supported: https://book.getfoundry.sh/reference/config/ # FOUNDRY_BROADCAST=true -# FOUNDRY_SLOW=true -# FOUNDRY_VERBOSITY=3 # ETH_PRIVATE_KEY=0x... -# FOUNDRY_VERIFY=true ## Chain and contract addresses PARENT_CHAIN_RPC= diff --git a/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.local-upgrade.example b/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.local-upgrade.example index 2b55d480..49bb3dd1 100644 --- a/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.local-upgrade.example +++ b/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.local-upgrade.example @@ -1,9 +1,7 @@ ## Forge configuration (uncomment/adjust as needed) +## All FOUNDRY_* env vars are supported: https://book.getfoundry.sh/reference/config/ # FOUNDRY_BROADCAST=true -# FOUNDRY_SLOW=true -# FOUNDRY_VERBOSITY=3 # ETH_PRIVATE_KEY=0x... -# FOUNDRY_VERIFY=true ## Chain and contract addresses PARENT_CHAIN_RPC= diff --git a/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.sepolia-upgrade.example b/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.sepolia-upgrade.example index cb093990..bcf066c0 100644 --- a/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.sepolia-upgrade.example +++ b/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.sepolia-upgrade.example @@ -1,9 +1,7 @@ ## Forge configuration (uncomment/adjust as needed) +## All FOUNDRY_* env vars are supported: https://book.getfoundry.sh/reference/config/ # FOUNDRY_BROADCAST=true -# FOUNDRY_SLOW=true -# FOUNDRY_VERBOSITY=3 # ETH_PRIVATE_KEY=0x... -# FOUNDRY_VERIFY=true ## Chain and contract addresses PARENT_CHAIN_RPC= diff --git a/scripts/foundry/contract-upgrades/2.1.0/.env.sample b/scripts/foundry/contract-upgrades/2.1.0/.env.sample index aabadbca..6008a8cf 100644 --- a/scripts/foundry/contract-upgrades/2.1.0/.env.sample +++ b/scripts/foundry/contract-upgrades/2.1.0/.env.sample @@ -1,9 +1,7 @@ ## Forge configuration (uncomment/adjust as needed) +## All FOUNDRY_* env vars are supported: https://book.getfoundry.sh/reference/config/ # FOUNDRY_BROADCAST=true -# FOUNDRY_SLOW=true -# FOUNDRY_VERBOSITY=3 # ETH_PRIVATE_KEY=0x... -# FOUNDRY_VERIFY=true ## Chain and contract addresses PARENT_CHAIN_RPC= diff --git a/scripts/foundry/contract-upgrades/2.1.2/.env.sample b/scripts/foundry/contract-upgrades/2.1.2/.env.sample index cfb1757d..5d029650 100644 --- a/scripts/foundry/contract-upgrades/2.1.2/.env.sample +++ b/scripts/foundry/contract-upgrades/2.1.2/.env.sample @@ -1,9 +1,7 @@ ## Forge configuration (uncomment/adjust as needed) +## All FOUNDRY_* env vars are supported: https://book.getfoundry.sh/reference/config/ # FOUNDRY_BROADCAST=true -# FOUNDRY_SLOW=true -# FOUNDRY_VERBOSITY=3 # ETH_PRIVATE_KEY=0x... -# FOUNDRY_VERIFY=true ## Chain and contract addresses PARENT_CHAIN_RPC= diff --git a/scripts/foundry/contract-upgrades/2.1.3/.env.sample b/scripts/foundry/contract-upgrades/2.1.3/.env.sample index 3969f14e..565f8a6f 100644 --- a/scripts/foundry/contract-upgrades/2.1.3/.env.sample +++ b/scripts/foundry/contract-upgrades/2.1.3/.env.sample @@ -1,9 +1,7 @@ ## Forge configuration (uncomment/adjust as needed) +## All FOUNDRY_* env vars are supported: https://book.getfoundry.sh/reference/config/ # FOUNDRY_BROADCAST=true -# FOUNDRY_SLOW=true -# FOUNDRY_VERBOSITY=3 # ETH_PRIVATE_KEY=0x... -# FOUNDRY_VERIFY=true ## Chain and contract addresses PARENT_CHAIN_RPC= diff --git a/src/cli/router.ts b/src/cli/router.ts index 6277fb2c..95e60afb 100644 --- a/src/cli/router.ts +++ b/src/cli/router.ts @@ -13,12 +13,11 @@ import { cmdVerify as arbosVerify, } from './commands/arbos-upgrade' -const HELP_TEXT = `Usage: orbit-actions [path] [args...] +const HELP_TEXT = `Usage: [path] [args...] Browse and execute scripts from the foundry scripts directory. Browsing: - . List top-level directories contract-upgrades List available versions contract-upgrades/1.2.1 List version contents + commands contract-upgrades/1.2.1/env-templates List env templates @@ -38,13 +37,7 @@ Running upgrade scripts: arbos-upgrades/at-timestamp/verify Forge behavior (broadcast, auth, verbosity, etc.) is configured via -FOUNDRY_* / ETH_* env vars in your .env file. See env templates for examples. - -Examples: - docker run orbit-actions contract-upgrades/1.2.1 - docker run orbit-actions contract-upgrades/1.2.1/README.md - docker run -v $(pwd)/.env:/app/.env orbit-actions contract-upgrades/1.2.1/deploy - docker run -v $(pwd)/.env:/app/.env orbit-actions contract-upgrades/1.2.1/execute` +FOUNDRY_* / ETH_* env vars in your .env file. See env templates for examples.` function listDirectory(dir: string): void { const scriptsDir = getScriptsDir() @@ -76,19 +69,19 @@ function listDirectory(dir: string): void { if (isCategoryDir) { console.log('') - console.log(`Example: orbit-actions ${relPath}/${contents.find(c => !c.startsWith('.')) ?? ''}`) + console.log(`Example: ${relPath}/${contents.find(c => !c.startsWith('.')) ?? ''}`) } else if (isVersionDir) { console.log('') console.log('Commands:') - console.log(` orbit-actions ${relPath}/deploy`) - console.log(` orbit-actions ${relPath}/execute`) - console.log(` orbit-actions ${relPath}/verify`) + console.log(` ${relPath}/deploy`) + console.log(` ${relPath}/execute`) + console.log(` ${relPath}/verify`) } else if (isArbosDir) { console.log('') console.log('Commands:') - console.log(` orbit-actions ${relPath}/deploy `) - console.log(` orbit-actions ${relPath}/execute`) - console.log(` orbit-actions ${relPath}/verify`) + console.log(` ${relPath}/deploy `) + console.log(` ${relPath}/execute`) + console.log(` ${relPath}/verify`) } } @@ -114,9 +107,9 @@ export async function router( } console.log('') console.log('Usage:') - console.log(' orbit-actions Browse scripts') - console.log(' orbit-actions /deploy Run a script') - console.log(' orbit-actions help Full usage details') + console.log(' Browse scripts') + console.log(' /deploy Run a script') + console.log(' help Full usage details') return } From 8344c395d0bc7910ca57c14f6dda600c7f887de4 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Tue, 10 Mar 2026 13:12:38 +0000 Subject: [PATCH 31/44] chore: remove bin entry from package.json The orbit-actions binary only worked via yarn link and isn't used in Docker (entrypoint is node directly) or local dev (yarn cli). --- package.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/package.json b/package.json index 69e020fa..e525ef41 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,6 @@ "version": "1.0.0", "repository": "https://github.com/OffchainLabs/blockchain-eng-template.git", "license": "Apache 2.0", - "bin": { - "orbit-actions": "./dist/cli/index.js" - }, "scripts": { "build:cli": "tsc -p tsconfig.cli.json", "cli": "ts-node src/cli/index.ts", From 707cc07bdd084c923ecccda2fa5234f0b2ba599f Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Wed, 11 Mar 2026 09:46:33 +0000 Subject: [PATCH 32/44] refactor: remove log() wrapper, use console.log directly The [orbit-actions] prefix added no value -- the user already knows what tool they're running. Drop the wrapper and use console.log throughout for consistent, unprefixed output. --- src/cli/commands/arbos-upgrade.ts | 24 ++++++++++++------------ src/cli/commands/contract-upgrade.ts | 12 ++++++------ src/cli/utils/forge.ts | 4 ++-- src/cli/utils/log.ts | 6 ------ 4 files changed, 20 insertions(+), 26 deletions(-) diff --git a/src/cli/commands/arbos-upgrade.ts b/src/cli/commands/arbos-upgrade.ts index d3829d20..11cb06cf 100644 --- a/src/cli/commands/arbos-upgrade.ts +++ b/src/cli/commands/arbos-upgrade.ts @@ -1,7 +1,7 @@ import * as path from 'path' import * as fs from 'fs' import { Interface } from 'ethers' -import { log, die } from '../utils/log' +import { die } from '../utils/log' import { requireEnv, getScriptsDir } from '../utils/env' import { runForgeScript, @@ -58,15 +58,15 @@ async function executeUpgrade( PERFORM_SELECTOR ) - log('Calldata for UpgradeExecutor.execute():') + console.log('Calldata for UpgradeExecutor.execute():') console.log('') console.log(`To: ${upgradeExecutor}`) console.log(`Calldata: ${executeCalldata}`) console.log('') - log('Submit this to your multisig/Safe to execute the upgrade') + console.log('Submit this to your multisig/Safe to execute the upgrade') if (process.env.FOUNDRY_BROADCAST) { - log('Broadcasting transaction...') + console.log('Broadcasting transaction...') await runCastSend({ to: upgradeExecutor, sig: 'execute(address,bytes)', @@ -74,19 +74,19 @@ async function executeUpgrade( rpcUrl, }) - log('ArbOS upgrade scheduled successfully') + console.log('ArbOS upgrade scheduled successfully') } } async function verifyUpgrade(rpcUrl: string): Promise { - log('Checking ArbOS upgrade status...') + console.log('Checking ArbOS upgrade status...') const scheduled = await runCastCall({ to: ARB_OWNER_PUBLIC, sig: 'getScheduledUpgrade()(uint64,uint64)', rpcUrl, }) - log(`Scheduled upgrade (version, timestamp): (${scheduled.replace('\n', ', ')})`) + console.log(`Scheduled upgrade (version, timestamp): (${scheduled.replace('\n', ', ')})`) const currentRaw = await runCastCall({ to: ARB_SYS, @@ -96,19 +96,19 @@ async function verifyUpgrade(rpcUrl: string): Promise { const currentVersion = parseInt(currentRaw, 10) - ARBOS_VERSION_OFFSET - log(`Current ArbOS version: ${currentVersion}`) + console.log(`Current ArbOS version: ${currentVersion}`) } async function cmdDeploy(version: string): Promise { const rpcUrl = requireEnv('CHILD_CHAIN_RPC') - log(`Running: ${path.basename(DEPLOY_SCRIPT)} for ArbOS ${version}`) + console.log(`Running: ${path.basename(DEPLOY_SCRIPT)} for ArbOS ${version}`) await deployAction(version, rpcUrl) const chainId = await getChainId(rpcUrl) const address = parseActionAddress(DEPLOY_SCRIPT, chainId) if (address) { - log(`Deployed action address: ${address}`) - log('Set UPGRADE_ACTION_ADDRESS in .env for the execute step') + console.log(`Deployed action address: ${address}`) + console.log('Set UPGRADE_ACTION_ADDRESS in .env for the execute step') } } @@ -117,7 +117,7 @@ async function cmdExecute(): Promise { const upgradeExecutor = requireEnv('CHILD_UPGRADE_EXECUTOR_ADDRESS') const actionAddress = requireEnv('UPGRADE_ACTION_ADDRESS') - log(`Executing ArbOS upgrade action: ${actionAddress}`) + console.log(`Executing ArbOS upgrade action: ${actionAddress}`) await executeUpgrade(actionAddress, upgradeExecutor, rpcUrl) } diff --git a/src/cli/commands/contract-upgrade.ts b/src/cli/commands/contract-upgrade.ts index 33066665..91583a05 100644 --- a/src/cli/commands/contract-upgrade.ts +++ b/src/cli/commands/contract-upgrade.ts @@ -1,6 +1,6 @@ import * as path from 'path' import * as fs from 'fs' -import { log, die } from '../utils/log' +import { die } from '../utils/log' import { requireEnv, getScriptsDir } from '../utils/env' import { runForgeScript, @@ -35,7 +35,7 @@ async function cmdDeploy(version: string): Promise { die(`No deploy script found in ${versionDir}`) } - log(`Running: ${path.basename(deployScript)}`) + console.log(`Running: ${path.basename(deployScript)}`) await runForgeScript({ script: deployScript, @@ -45,8 +45,8 @@ async function cmdDeploy(version: string): Promise { const chainId = await getChainId(rpcUrl) const address = parseActionAddress(deployScript, chainId) if (address) { - log(`Deployed action address: ${address}`) - log('Set UPGRADE_ACTION_ADDRESS in .env for the execute step') + console.log(`Deployed action address: ${address}`) + console.log('Set UPGRADE_ACTION_ADDRESS in .env for the execute step') } } @@ -61,7 +61,7 @@ async function cmdExecute(version: string): Promise { die(`No execute script found in ${versionDir}`) } - log(`Running: ${path.basename(executeScript)}`) + console.log(`Running: ${path.basename(executeScript)}`) await runForgeScript({ script: executeScript, @@ -81,7 +81,7 @@ async function cmdVerify(version: string): Promise { ) } - log(`Running: ${path.basename(verifyScript)}`) + console.log(`Running: ${path.basename(verifyScript)}`) await runForgeScript({ script: verifyScript, diff --git a/src/cli/utils/forge.ts b/src/cli/utils/forge.ts index f8addf26..9d2f19b4 100644 --- a/src/cli/utils/forge.ts +++ b/src/cli/utils/forge.ts @@ -1,7 +1,7 @@ import execa from 'execa' import * as fs from 'fs' import * as path from 'path' -import { die, log } from './log' +import { die } from './log' import { getRepoRoot } from './env' export interface ForgeScriptOptions { @@ -15,7 +15,7 @@ export async function runForgeScript( ): Promise { const args = ['script', options.script, '--rpc-url', options.rpcUrl] - log(`Running: forge ${args.join(' ')}`) + console.log(`Running: forge ${args.join(' ')}`) try { await execa('forge', args, { diff --git a/src/cli/utils/log.ts b/src/cli/utils/log.ts index 355c1699..d1029944 100644 --- a/src/cli/utils/log.ts +++ b/src/cli/utils/log.ts @@ -1,9 +1,3 @@ -const PREFIX = '[orbit-actions]' - -export function log(message: string): void { - console.log(`${PREFIX} ${message}`) -} - export function die(message: string): never { console.error(`Error: ${message}`) process.exit(1) From 0efc403cf0c3f21e88e934913c46ac61545f3c16 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Wed, 11 Mar 2026 11:03:07 +0000 Subject: [PATCH 33/44] Apply suggestion from @godzillaba Co-authored-by: Henry <11198460+godzillaba@users.noreply.github.com> --- src/cli/utils/forge.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/utils/forge.ts b/src/cli/utils/forge.ts index 9d2f19b4..55a2e4a0 100644 --- a/src/cli/utils/forge.ts +++ b/src/cli/utils/forge.ts @@ -124,7 +124,7 @@ export function findScript(dir: string, pattern: RegExp): string | null { const files = fs.readdirSync(dir) for (const file of files) { - if (pattern.test(file) && file.endsWith('.s.sol')) { + if (pattern.test(file)) { return path.join(dir, file) } } From 657fd931db27c21e123371359fb3b2415f6393fa Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Wed, 11 Mar 2026 11:15:42 +0000 Subject: [PATCH 34/44] feat: auto-resolve action address from broadcast file for deploy && execute chaining Execute commands now read the deployed action address from Forge's broadcast output when UPGRADE_ACTION_ADDRESS is not set, enabling deploy && execute chaining without manual .env edits. The env var still takes precedence as an explicit override for multisig flows. --- src/cli/commands/arbos-upgrade.ts | 18 ++++++++++++++++-- src/cli/commands/contract-upgrade.ts | 27 ++++++++++++++++++++++++--- src/cli/router.ts | 4 ++++ 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/cli/commands/arbos-upgrade.ts b/src/cli/commands/arbos-upgrade.ts index 11cb06cf..2ed6ba19 100644 --- a/src/cli/commands/arbos-upgrade.ts +++ b/src/cli/commands/arbos-upgrade.ts @@ -99,6 +99,20 @@ async function verifyUpgrade(rpcUrl: string): Promise { console.log(`Current ArbOS version: ${currentVersion}`) } +async function resolveActionAddress(rpcUrl: string): Promise { + const fromEnv = process.env.UPGRADE_ACTION_ADDRESS + if (fromEnv) return fromEnv + + const chainId = await getChainId(rpcUrl) + const fromBroadcast = parseActionAddress(DEPLOY_SCRIPT, chainId) + if (fromBroadcast) return fromBroadcast + + die( + 'Could not resolve action address.\n' + + 'Either set UPGRADE_ACTION_ADDRESS in .env, or run deploy first.' + ) +} + async function cmdDeploy(version: string): Promise { const rpcUrl = requireEnv('CHILD_CHAIN_RPC') console.log(`Running: ${path.basename(DEPLOY_SCRIPT)} for ArbOS ${version}`) @@ -108,14 +122,14 @@ async function cmdDeploy(version: string): Promise { const address = parseActionAddress(DEPLOY_SCRIPT, chainId) if (address) { console.log(`Deployed action address: ${address}`) - console.log('Set UPGRADE_ACTION_ADDRESS in .env for the execute step') + console.log('Run execute next, or set UPGRADE_ACTION_ADDRESS in .env to override') } } async function cmdExecute(): Promise { const rpcUrl = requireEnv('CHILD_CHAIN_RPC') const upgradeExecutor = requireEnv('CHILD_UPGRADE_EXECUTOR_ADDRESS') - const actionAddress = requireEnv('UPGRADE_ACTION_ADDRESS') + const actionAddress = await resolveActionAddress(rpcUrl) console.log(`Executing ArbOS upgrade action: ${actionAddress}`) diff --git a/src/cli/commands/contract-upgrade.ts b/src/cli/commands/contract-upgrade.ts index 91583a05..1d146e21 100644 --- a/src/cli/commands/contract-upgrade.ts +++ b/src/cli/commands/contract-upgrade.ts @@ -46,26 +46,47 @@ async function cmdDeploy(version: string): Promise { const address = parseActionAddress(deployScript, chainId) if (address) { console.log(`Deployed action address: ${address}`) - console.log('Set UPGRADE_ACTION_ADDRESS in .env for the execute step') + console.log('Run execute next, or set UPGRADE_ACTION_ADDRESS in .env to override') } } +async function resolveActionAddress( + versionDir: string, + rpcUrl: string +): Promise { + const fromEnv = process.env.UPGRADE_ACTION_ADDRESS + if (fromEnv) return fromEnv + + const deployScript = findScript(versionDir, /^Deploy.*\.s\.sol$/) + if (deployScript) { + const chainId = await getChainId(rpcUrl) + const fromBroadcast = parseActionAddress(deployScript, chainId) + if (fromBroadcast) return fromBroadcast + } + + die( + 'Could not resolve action address.\n' + + 'Either set UPGRADE_ACTION_ADDRESS in .env, or run deploy first.' + ) +} + async function cmdExecute(version: string): Promise { const versionDir = getVersionDir(version) - const rpcUrl = requireEnv('PARENT_CHAIN_RPC') - requireEnv('UPGRADE_ACTION_ADDRESS') + const actionAddress = await resolveActionAddress(versionDir, rpcUrl) const executeScript = findScript(versionDir, /^Execute.*\.s\.sol$/) if (!executeScript) { die(`No execute script found in ${versionDir}`) } + console.log(`Using action address: ${actionAddress}`) console.log(`Running: ${path.basename(executeScript)}`) await runForgeScript({ script: executeScript, rpcUrl, + env: { UPGRADE_ACTION_ADDRESS: actionAddress }, }) } diff --git a/src/cli/router.ts b/src/cli/router.ts index 95e60afb..1d6a7542 100644 --- a/src/cli/router.ts +++ b/src/cli/router.ts @@ -36,6 +36,10 @@ Running upgrade scripts: arbos-upgrades/at-timestamp/execute arbos-upgrades/at-timestamp/verify +Commands can be chained (e.g. deploy && execute). The execute step +automatically reads the deployed address from the broadcast output. +Set UPGRADE_ACTION_ADDRESS in .env to override (e.g. for multisig flows). + Forge behavior (broadcast, auth, verbosity, etc.) is configured via FOUNDRY_* / ETH_* env vars in your .env file. See env templates for examples.` From e7f838763c2203a300a4a589642579ab81dac667 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Wed, 11 Mar 2026 12:07:16 +0000 Subject: [PATCH 35/44] refactor: consolidate resolveActionAddress into forge utils Extract duplicate resolveActionAddress from arbos-upgrade and contract-upgrade into a shared function in forge.ts. Make parseActionAddress private and document its last-CREATE assumption. --- src/cli/commands/arbos-upgrade.ts | 28 ++++------------------ src/cli/commands/contract-upgrade.ts | 35 +++++----------------------- src/cli/utils/forge.ts | 24 ++++++++++++++++++- 3 files changed, 34 insertions(+), 53 deletions(-) diff --git a/src/cli/commands/arbos-upgrade.ts b/src/cli/commands/arbos-upgrade.ts index 2ed6ba19..d0ead2d2 100644 --- a/src/cli/commands/arbos-upgrade.ts +++ b/src/cli/commands/arbos-upgrade.ts @@ -8,8 +8,7 @@ import { runCastSend, runCastCall, castCalldata, - getChainId, - parseActionAddress, + resolveActionAddress, } from '../utils/forge' const ARBOS_DIR = path.join(getScriptsDir(), 'arbos-upgrades', 'at-timestamp') @@ -99,37 +98,20 @@ async function verifyUpgrade(rpcUrl: string): Promise { console.log(`Current ArbOS version: ${currentVersion}`) } -async function resolveActionAddress(rpcUrl: string): Promise { - const fromEnv = process.env.UPGRADE_ACTION_ADDRESS - if (fromEnv) return fromEnv - - const chainId = await getChainId(rpcUrl) - const fromBroadcast = parseActionAddress(DEPLOY_SCRIPT, chainId) - if (fromBroadcast) return fromBroadcast - - die( - 'Could not resolve action address.\n' + - 'Either set UPGRADE_ACTION_ADDRESS in .env, or run deploy first.' - ) -} - async function cmdDeploy(version: string): Promise { const rpcUrl = requireEnv('CHILD_CHAIN_RPC') console.log(`Running: ${path.basename(DEPLOY_SCRIPT)} for ArbOS ${version}`) await deployAction(version, rpcUrl) - const chainId = await getChainId(rpcUrl) - const address = parseActionAddress(DEPLOY_SCRIPT, chainId) - if (address) { - console.log(`Deployed action address: ${address}`) - console.log('Run execute next, or set UPGRADE_ACTION_ADDRESS in .env to override') - } + const address = await resolveActionAddress(DEPLOY_SCRIPT, rpcUrl) + console.log(`Deployed action address: ${address}`) + console.log('Run execute next, or set UPGRADE_ACTION_ADDRESS in .env to override') } async function cmdExecute(): Promise { const rpcUrl = requireEnv('CHILD_CHAIN_RPC') const upgradeExecutor = requireEnv('CHILD_UPGRADE_EXECUTOR_ADDRESS') - const actionAddress = await resolveActionAddress(rpcUrl) + const actionAddress = await resolveActionAddress(DEPLOY_SCRIPT, rpcUrl) console.log(`Executing ArbOS upgrade action: ${actionAddress}`) diff --git a/src/cli/commands/contract-upgrade.ts b/src/cli/commands/contract-upgrade.ts index 1d146e21..db92a30f 100644 --- a/src/cli/commands/contract-upgrade.ts +++ b/src/cli/commands/contract-upgrade.ts @@ -4,8 +4,7 @@ import { die } from '../utils/log' import { requireEnv, getScriptsDir } from '../utils/env' import { runForgeScript, - getChainId, - parseActionAddress, + resolveActionAddress, findScript, } from '../utils/forge' @@ -42,38 +41,16 @@ async function cmdDeploy(version: string): Promise { rpcUrl, }) - const chainId = await getChainId(rpcUrl) - const address = parseActionAddress(deployScript, chainId) - if (address) { - console.log(`Deployed action address: ${address}`) - console.log('Run execute next, or set UPGRADE_ACTION_ADDRESS in .env to override') - } -} - -async function resolveActionAddress( - versionDir: string, - rpcUrl: string -): Promise { - const fromEnv = process.env.UPGRADE_ACTION_ADDRESS - if (fromEnv) return fromEnv - - const deployScript = findScript(versionDir, /^Deploy.*\.s\.sol$/) - if (deployScript) { - const chainId = await getChainId(rpcUrl) - const fromBroadcast = parseActionAddress(deployScript, chainId) - if (fromBroadcast) return fromBroadcast - } - - die( - 'Could not resolve action address.\n' + - 'Either set UPGRADE_ACTION_ADDRESS in .env, or run deploy first.' - ) + const address = await resolveActionAddress(deployScript, rpcUrl) + console.log(`Deployed action address: ${address}`) + console.log('Run execute next, or set UPGRADE_ACTION_ADDRESS in .env to override') } async function cmdExecute(version: string): Promise { const versionDir = getVersionDir(version) const rpcUrl = requireEnv('PARENT_CHAIN_RPC') - const actionAddress = await resolveActionAddress(versionDir, rpcUrl) + const deployScript = findScript(versionDir, /^Deploy.*\.s\.sol$/) + const actionAddress = await resolveActionAddress(deployScript, rpcUrl) const executeScript = findScript(versionDir, /^Execute.*\.s\.sol$/) if (!executeScript) { diff --git a/src/cli/utils/forge.ts b/src/cli/utils/forge.ts index 55a2e4a0..de896ba6 100644 --- a/src/cli/utils/forge.ts +++ b/src/cli/utils/forge.ts @@ -87,7 +87,10 @@ export async function getChainId(rpcUrl: string): Promise { return result.stdout.trim() } -export function parseActionAddress( +// Assumes the action contract is the last CREATE in the broadcast file. +// This holds for all current deploy scripts, which deploy dependencies first +// and the action contract last. +function parseActionAddress( scriptPath: string, chainId: string ): string | null { @@ -117,6 +120,25 @@ export function parseActionAddress( return createTxs[createTxs.length - 1]?.contractAddress ?? null } +export async function resolveActionAddress( + deployScript: string | null, + rpcUrl: string +): Promise { + const fromEnv = process.env.UPGRADE_ACTION_ADDRESS + if (fromEnv) return fromEnv + + if (deployScript) { + const chainId = await getChainId(rpcUrl) + const fromBroadcast = parseActionAddress(deployScript, chainId) + if (fromBroadcast) return fromBroadcast + } + + die( + 'Could not resolve action address.\n' + + 'Either set UPGRADE_ACTION_ADDRESS in .env, or run deploy first.' + ) +} + export function findScript(dir: string, pattern: RegExp): string | null { if (!fs.existsSync(dir)) { return null From 7a6d9a606ab96e1e16271ffa6bffe26f5d2bef30 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Wed, 11 Mar 2026 12:10:18 +0000 Subject: [PATCH 36/44] fix: update docker tests for removed deploy-execute-verify command Test was checking for deploy-execute-verify which no longer exists. Updated to check for deploy/execute/verify as separate commands. Also fixes prettier formatting. --- src/cli/commands/arbos-upgrade.ts | 13 +++++++------ src/cli/commands/contract-upgrade.ts | 4 +++- test/docker/test-docker.bash | 5 ++--- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/cli/commands/arbos-upgrade.ts b/src/cli/commands/arbos-upgrade.ts index d0ead2d2..c99a02b5 100644 --- a/src/cli/commands/arbos-upgrade.ts +++ b/src/cli/commands/arbos-upgrade.ts @@ -33,10 +33,7 @@ function checkDeployScript(): void { } } -async function deployAction( - version: string, - rpcUrl: string -): Promise { +async function deployAction(version: string, rpcUrl: string): Promise { checkDeployScript() await runForgeScript({ @@ -85,7 +82,9 @@ async function verifyUpgrade(rpcUrl: string): Promise { sig: 'getScheduledUpgrade()(uint64,uint64)', rpcUrl, }) - console.log(`Scheduled upgrade (version, timestamp): (${scheduled.replace('\n', ', ')})`) + console.log( + `Scheduled upgrade (version, timestamp): (${scheduled.replace('\n', ', ')})` + ) const currentRaw = await runCastCall({ to: ARB_SYS, @@ -105,7 +104,9 @@ async function cmdDeploy(version: string): Promise { const address = await resolveActionAddress(DEPLOY_SCRIPT, rpcUrl) console.log(`Deployed action address: ${address}`) - console.log('Run execute next, or set UPGRADE_ACTION_ADDRESS in .env to override') + console.log( + 'Run execute next, or set UPGRADE_ACTION_ADDRESS in .env to override' + ) } async function cmdExecute(): Promise { diff --git a/src/cli/commands/contract-upgrade.ts b/src/cli/commands/contract-upgrade.ts index db92a30f..b8142dfd 100644 --- a/src/cli/commands/contract-upgrade.ts +++ b/src/cli/commands/contract-upgrade.ts @@ -43,7 +43,9 @@ async function cmdDeploy(version: string): Promise { const address = await resolveActionAddress(deployScript, rpcUrl) console.log(`Deployed action address: ${address}`) - console.log('Run execute next, or set UPGRADE_ACTION_ADDRESS in .env to override') + console.log( + 'Run execute next, or set UPGRADE_ACTION_ADDRESS in .env to override' + ) } async function cmdExecute(version: string): Promise { diff --git a/test/docker/test-docker.bash b/test/docker/test-docker.bash index d24cd210..e863e216 100755 --- a/test/docker/test-docker.bash +++ b/test/docker/test-docker.bash @@ -70,7 +70,7 @@ fi # List version contents (should show virtual commands) echo -n "Testing list contract-upgrades/1.2.1... " CONTENTS_OUTPUT=$(docker run --rm "$IMAGE_NAME" contract-upgrades/1.2.1 2>&1) -if echo "$CONTENTS_OUTPUT" | grep "deploy-execute-verify" > /dev/null; then +if echo "$CONTENTS_OUTPUT" | grep "deploy" > /dev/null; then echo "OK" else echo "FAILED" @@ -142,11 +142,10 @@ cat > "$TEMP_ENV" <&1) +DRYRUN_OUTPUT=$(docker run --rm -v "$TEMP_ENV:/app/.env" "$IMAGE_NAME" arbos-upgrades/at-timestamp/execute 2>&1) if echo "$DRYRUN_OUTPUT" | grep "Calldata:" > /dev/null; then echo "OK" else From 784b100e0fc3fcdd6b8659d761664bb11dbe335a Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Wed, 11 Mar 2026 12:19:15 +0000 Subject: [PATCH 37/44] fix: update local test to use yarn cli instead of removed bin/router --- test/local/test-local.bash | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/test/local/test-local.bash b/test/local/test-local.bash index ef5358ec..54cbea4e 100755 --- a/test/local/test-local.bash +++ b/test/local/test-local.bash @@ -2,14 +2,12 @@ set -euo pipefail # Local (non-Docker) smoke tests for orbit-actions CLI -# Tests the bin/router and related scripts directly +# Tests the CLI via yarn cli SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -ROUTER="$REPO_ROOT/bin/router" echo "=== Local Smoke Tests ===" -echo "Router: $ROUTER" echo "" PASSED=0 @@ -43,32 +41,29 @@ check_output() { fi } +cli() { + yarn --silent --cwd "$REPO_ROOT" cli -- "$@" +} + echo "--- Prerequisites ---" check "forge installed" command -v forge check "cast installed" command -v cast -check "jq installed" command -v jq +check "yarn installed" command -v yarn echo "" echo "--- Directory Browsing ---" -check "list top level" "$ROUTER" -check_output "list contract-upgrades" "1.2.1" "$ROUTER" contract-upgrades -check_output "list contract-upgrades/1.2.1" "deploy" "$ROUTER" contract-upgrades/1.2.1 -check_output "list arbos-upgrades" "at-timestamp" "$ROUTER" arbos-upgrades +check "list top level" cli +check_output "list contract-upgrades" "1.2.1" cli contract-upgrades +check_output "list contract-upgrades/1.2.1" "deploy" cli contract-upgrades/1.2.1 +check_output "list arbos-upgrades" "at-timestamp" cli arbos-upgrades echo "" echo "--- File Viewing ---" -check_output "view README" "Nitro contracts" "$ROUTER" contract-upgrades/1.2.1/README.md +check_output "view README" "Nitro contracts" cli contract-upgrades/1.2.1/README.md echo "" echo "--- Help ---" -check_output "help command" "Usage:" "$ROUTER" help -check_output "contract-upgrade help" "deploy-execute-verify" "$REPO_ROOT/bin/contract-upgrade" --help -check_output "arbos-upgrade help" "deploy-execute-verify" "$REPO_ROOT/bin/arbos-upgrade" --help - -echo "" -echo "--- Passthrough ---" -check_output "forge passthrough" "forge" "$ROUTER" forge --version -check_output "cast passthrough" "cast" "$ROUTER" cast --version +check_output "help command" "Usage:" cli help echo "" echo "=== Summary ===" From ede1c954765d379ad95129d96b13b04fc08b0e86 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Wed, 11 Mar 2026 14:13:34 +0000 Subject: [PATCH 38/44] chore: fix prettier formatting in router.ts --- src/cli/router.ts | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/cli/router.ts b/src/cli/router.ts index 1d6a7542..49d69529 100644 --- a/src/cli/router.ts +++ b/src/cli/router.ts @@ -57,7 +57,9 @@ function listDirectory(dir: string): void { } else if (isVersionDir || isArbosDir) { const hasEnvTemplates = fs.existsSync(path.join(dir, 'env-templates')) if (hasEnvTemplates) { - console.log('Configure .env before running. See env-templates/ for examples.') + console.log( + 'Configure .env before running. See env-templates/ for examples.' + ) } else { console.log('Configure .env before running. See the README for details.') } @@ -73,7 +75,9 @@ function listDirectory(dir: string): void { if (isCategoryDir) { console.log('') - console.log(`Example: ${relPath}/${contents.find(c => !c.startsWith('.')) ?? ''}`) + console.log( + `Example: ${relPath}/${contents.find(c => !c.startsWith('.')) ?? ''}` + ) } else if (isVersionDir) { console.log('') console.log('Commands:') @@ -98,9 +102,15 @@ export async function router( if (!pathArg) { console.log('orbit-actions - CLI for Orbit chain upgrade actions') console.log('') - console.log('Browse and run upgrade scripts for Orbit chains. Configuration') - console.log('is read from .env in the project root. Forge behavior (broadcast,') - console.log('auth, verbosity) is controlled via FOUNDRY_* / ETH_* env vars.') + console.log( + 'Browse and run upgrade scripts for Orbit chains. Configuration' + ) + console.log( + 'is read from .env in the project root. Forge behavior (broadcast,' + ) + console.log( + 'auth, verbosity) is controlled via FOUNDRY_* / ETH_* env vars.' + ) console.log('') console.log('Available:') const contents = fs.readdirSync(scriptsDir) From 2853daa91ca7c7bccaebef76ab7d28d824ff839d Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Thu, 12 Mar 2026 10:59:18 +0000 Subject: [PATCH 39/44] fix: handle deploy simulation mode gracefully in CLI Skip action address resolution when not broadcasting - forge runs in simulation mode and doesn't produce a broadcast file at the normal path. Also early-return from deploy if UPGRADE_ACTION_ADDRESS is already set. Remove unused getEnv helper from CLI env utils. --- src/cli/commands/arbos-upgrade.ts | 20 +++++++++++++++++--- src/cli/commands/contract-upgrade.ts | 17 +++++++++++++++++ src/cli/utils/env.ts | 4 ---- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/cli/commands/arbos-upgrade.ts b/src/cli/commands/arbos-upgrade.ts index c99a02b5..2c7ebea7 100644 --- a/src/cli/commands/arbos-upgrade.ts +++ b/src/cli/commands/arbos-upgrade.ts @@ -58,8 +58,6 @@ async function executeUpgrade( console.log('') console.log(`To: ${upgradeExecutor}`) console.log(`Calldata: ${executeCalldata}`) - console.log('') - console.log('Submit this to your multisig/Safe to execute the upgrade') if (process.env.FOUNDRY_BROADCAST) { console.log('Broadcasting transaction...') @@ -98,14 +96,30 @@ async function verifyUpgrade(rpcUrl: string): Promise { } async function cmdDeploy(version: string): Promise { + if (process.env.UPGRADE_ACTION_ADDRESS) { + console.log( + `Action already deployed at: ${process.env.UPGRADE_ACTION_ADDRESS}` + ) + console.log( + 'Run execute next, or remove UPGRADE_ACTION_ADDRESS from .env to redeploy.' + ) + return + } const rpcUrl = requireEnv('CHILD_CHAIN_RPC') console.log(`Running: ${path.basename(DEPLOY_SCRIPT)} for ArbOS ${version}`) await deployAction(version, rpcUrl) + if (!process.env.FOUNDRY_BROADCAST) { + console.log( + 'Rerun with FOUNDRY_BROADCAST=true to deploy, then run execute.' + ) + return + } + const address = await resolveActionAddress(DEPLOY_SCRIPT, rpcUrl) console.log(`Deployed action address: ${address}`) console.log( - 'Run execute next, or set UPGRADE_ACTION_ADDRESS in .env to override' + 'Run "execute" next, or set UPGRADE_ACTION_ADDRESS in .env to override' ) } diff --git a/src/cli/commands/contract-upgrade.ts b/src/cli/commands/contract-upgrade.ts index b8142dfd..ab6b05b1 100644 --- a/src/cli/commands/contract-upgrade.ts +++ b/src/cli/commands/contract-upgrade.ts @@ -25,6 +25,16 @@ function getVersionDir(version: string): string { } async function cmdDeploy(version: string): Promise { + if (process.env.UPGRADE_ACTION_ADDRESS) { + console.log( + `Action already deployed at: ${process.env.UPGRADE_ACTION_ADDRESS}` + ) + console.log( + 'Run execute next, or remove UPGRADE_ACTION_ADDRESS from .env to redeploy.' + ) + return + } + const versionDir = getVersionDir(version) const rpcUrl = requireEnv('PARENT_CHAIN_RPC') @@ -41,6 +51,13 @@ async function cmdDeploy(version: string): Promise { rpcUrl, }) + if (!process.env.FOUNDRY_BROADCAST) { + console.log( + 'Rerun with FOUNDRY_BROADCAST=true to deploy, then run execute.' + ) + return + } + const address = await resolveActionAddress(deployScript, rpcUrl) console.log(`Deployed action address: ${address}`) console.log( diff --git a/src/cli/utils/env.ts b/src/cli/utils/env.ts index 195305b5..ebc014da 100644 --- a/src/cli/utils/env.ts +++ b/src/cli/utils/env.ts @@ -31,10 +31,6 @@ export function requireEnv(name: string): string { return value } -export function getEnv(name: string): string | undefined { - return process.env[name] -} - export function getScriptsDir(): string { const repoRoot = findRepoRoot() if (repoRoot) { From d336a84346105d248482a64072fda2f523a304ed Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Thu, 12 Mar 2026 11:02:52 +0000 Subject: [PATCH 40/44] refactor: use ethers for ABI encoding/decoding instead of cast Replace cast calldata/sig-based interfaces with pre-encoded --data param. Encode calldata and decode results with ethers Interface, removing the async castCalldata shell-out entirely. --- src/cli/commands/arbos-upgrade.ts | 45 ++++++++++++++++++------------- src/cli/utils/forge.ts | 37 ++++++++----------------- 2 files changed, 37 insertions(+), 45 deletions(-) diff --git a/src/cli/commands/arbos-upgrade.ts b/src/cli/commands/arbos-upgrade.ts index 2c7ebea7..53244cf2 100644 --- a/src/cli/commands/arbos-upgrade.ts +++ b/src/cli/commands/arbos-upgrade.ts @@ -7,7 +7,6 @@ import { runForgeScript, runCastSend, runCastCall, - castCalldata, resolveActionAddress, } from '../utils/forge' @@ -48,25 +47,20 @@ async function executeUpgrade( upgradeExecutor: string, rpcUrl: string ): Promise { - const executeCalldata = await castCalldata( - 'execute(address,bytes)', + const iface = new Interface(['function execute(address,bytes)']) + const calldata = iface.encodeFunctionData('execute', [ actionAddress, - PERFORM_SELECTOR - ) + PERFORM_SELECTOR, + ]) console.log('Calldata for UpgradeExecutor.execute():') console.log('') console.log(`To: ${upgradeExecutor}`) - console.log(`Calldata: ${executeCalldata}`) + console.log(`Calldata: ${calldata}`) if (process.env.FOUNDRY_BROADCAST) { console.log('Broadcasting transaction...') - await runCastSend({ - to: upgradeExecutor, - sig: 'execute(address,bytes)', - args: [actionAddress, PERFORM_SELECTOR], - rpcUrl, - }) + await runCastSend({ to: upgradeExecutor, data: calldata, rpcUrl }) console.log('ArbOS upgrade scheduled successfully') } @@ -75,22 +69,35 @@ async function executeUpgrade( async function verifyUpgrade(rpcUrl: string): Promise { console.log('Checking ArbOS upgrade status...') - const scheduled = await runCastCall({ + const scheduledIface = new Interface([ + 'function getScheduledUpgrade() view returns (uint64, uint64)', + ]) + const scheduledRaw = await runCastCall({ to: ARB_OWNER_PUBLIC, - sig: 'getScheduledUpgrade()(uint64,uint64)', + data: scheduledIface.encodeFunctionData('getScheduledUpgrade'), rpcUrl, }) + const [version, timestamp] = scheduledIface.decodeFunctionResult( + 'getScheduledUpgrade', + scheduledRaw + ) console.log( - `Scheduled upgrade (version, timestamp): (${scheduled.replace('\n', ', ')})` + `Scheduled upgrade (version, timestamp): (${version}, ${timestamp})` ) - const currentRaw = await runCastCall({ + const arbSysIface = new Interface([ + 'function arbOSVersion() view returns (uint64)', + ]) + const versionRaw = await runCastCall({ to: ARB_SYS, - sig: 'arbOSVersion()(uint64)', + data: arbSysIface.encodeFunctionData('arbOSVersion'), rpcUrl, }) - - const currentVersion = parseInt(currentRaw, 10) - ARBOS_VERSION_OFFSET + const [currentVersionRaw] = arbSysIface.decodeFunctionResult( + 'arbOSVersion', + versionRaw + ) + const currentVersion = Number(currentVersionRaw) - ARBOS_VERSION_OFFSET console.log(`Current ArbOS version: ${currentVersion}`) } diff --git a/src/cli/utils/forge.ts b/src/cli/utils/forge.ts index de896ba6..f08890e5 100644 --- a/src/cli/utils/forge.ts +++ b/src/cli/utils/forge.ts @@ -29,25 +29,17 @@ export async function runForgeScript( export interface CastSendOptions { to: string - sig: string - args: string[] + data: string rpcUrl: string } export async function runCastSend(options: CastSendOptions): Promise { - const args = [ - 'send', - options.to, - options.sig, - ...options.args, - '--rpc-url', - options.rpcUrl, - ] - try { - await execa('cast', args, { - stdio: 'inherit', - }) + await execa( + 'cast', + ['send', options.to, '--data', options.data, '--rpc-url', options.rpcUrl], + { stdio: 'inherit' } + ) } catch { die('Cast send failed') } @@ -55,7 +47,7 @@ export async function runCastSend(options: CastSendOptions): Promise { export interface CastCallOptions { to: string - sig: string + data: string rpcUrl: string } @@ -63,25 +55,18 @@ export async function runCastCall(options: CastCallOptions): Promise { try { const result = await execa('cast', [ 'call', + options.to, + '--data', + options.data, '--rpc-url', options.rpcUrl, - options.to, - options.sig, ]) return result.stdout } catch { - die(`cast call failed: ${options.sig} on ${options.to}`) + die(`cast call failed on ${options.to}`) } } -export async function castCalldata( - sig: string, - ...args: string[] -): Promise { - const result = await execa('cast', ['calldata', sig, ...args]) - return result.stdout -} - export async function getChainId(rpcUrl: string): Promise { const result = await execa('cast', ['chain-id', '--rpc-url', rpcUrl]) return result.stdout.trim() From a022a659bec95cc30b2af49f59df5e9276bb8ecb Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Thu, 12 Mar 2026 11:05:03 +0000 Subject: [PATCH 41/44] fix: inherit stderr in cast call utilities for visible error output --- src/cli/utils/forge.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/cli/utils/forge.ts b/src/cli/utils/forge.ts index f08890e5..5554bba4 100644 --- a/src/cli/utils/forge.ts +++ b/src/cli/utils/forge.ts @@ -53,14 +53,11 @@ export interface CastCallOptions { export async function runCastCall(options: CastCallOptions): Promise { try { - const result = await execa('cast', [ - 'call', - options.to, - '--data', - options.data, - '--rpc-url', - options.rpcUrl, - ]) + const result = await execa( + 'cast', + ['call', options.to, '--data', options.data, '--rpc-url', options.rpcUrl], + { stderr: 'inherit' } + ) return result.stdout } catch { die(`cast call failed on ${options.to}`) @@ -68,7 +65,9 @@ export async function runCastCall(options: CastCallOptions): Promise { } export async function getChainId(rpcUrl: string): Promise { - const result = await execa('cast', ['chain-id', '--rpc-url', rpcUrl]) + const result = await execa('cast', ['chain-id', '--rpc-url', rpcUrl], { + stderr: 'inherit', + }) return result.stdout.trim() } From 1d77f3a33b7b0abe45c591d246d2eca22fcb1ef4 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Thu, 12 Mar 2026 12:52:32 +0000 Subject: [PATCH 42/44] docs: document last-CREATE convention in deploy scripts and README The CLI identifies the action contract by taking the last CREATE from the broadcast file. Add comments to all deploy scripts noting this constraint, and document it in the README for future script authors. --- README.md | 12 ++++++++---- .../DeployUpgradeArbOSVersionAtTimestampAction.s.sol | 3 ++- ...loyNitroContracts1Point2Point1UpgradeAction.s.sol | 3 ++- ...loyNitroContracts2Point1Point0UpgradeAction.s.sol | 3 ++- ...loyNitroContracts2Point1Point2UpgradeAction.s.sol | 3 ++- ...loyNitroContracts2Point1Point3UpgradeAction.s.sol | 3 ++- .../fast-confirm/DeployEnableFastConfirmAction.s.sol | 3 ++- ...ploySetSequencerInboxMaxTimeVariationAction.s.sol | 3 ++- .../DeployAddWasmCacheManagerAction.s.sol | 3 ++- 9 files changed, 24 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 6ecaa030..11a0183c 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ For ArbOS upgrades, a common pre-requisite is to deploy new Nitro contracts to t ### Nitro contracts 3.1.0 (for [BoLD](https://docs.arbitrum.io/how-arbitrum-works/bold/gentle-introduction)) -The [`nitro-contracts 3.1.0` upgrade guide](scripts/foundry/contract-upgrades/3.1.0) will use the [BOLDUpgradeAction](https://github.com/OffchainLabs/nitro-contracts/blob/main/src/rollup/BOLDUpgradeAction.sol) from the [nitro-contract](https://github.com/OffchainLabs/nitro-contracts) repo. There is no associated ArbOS upgrade for BoLD. +The [`nitro-contracts 3.1.0` upgrade guide](scripts/foundry/contract-upgrades/3.1.0) will use the [BOLDUpgradeAction](https://github.com/OffchainLabs/nitro-contracts/blob/main/src/rollup/BOLDUpgradeAction.sol) from the [nitro-contract](https://github.com/OffchainLabs/nitro-contracts) repo. There is no associated ArbOS upgrade for BoLD. ### Nitro contracts 2.1.3 @@ -168,10 +168,10 @@ Forge behavior -- broadcasting, authentication, verbosity, verification -- is co Key forge env vars: -| Variable | Effect | -|----------|--------| +| Variable | Effect | +| ------------------------ | ---------------------------------------------------------------- | | `FOUNDRY_BROADCAST=true` | Broadcast transactions (without this, scripts run in simulation) | -| `ETH_PRIVATE_KEY=0x...` | Private key for signing transactions | +| `ETH_PRIVATE_KEY=0x...` | Private key for signing transactions | All `FOUNDRY_*` env vars are supported -- see [Foundry configuration](https://book.getfoundry.sh/reference/config/) for the full list. @@ -191,6 +191,10 @@ yarn cli -- contract-upgrades/2.1.3/execute yarn cli -- contract-upgrades/2.1.3/verify ``` +### Writing new deploy scripts + +The CLI identifies the action contract address by reading the last `CREATE` transaction from Forge's broadcast file. Deploy scripts must deploy all dependencies first and the action contract last. + ## Docker The CLI is available as a Docker image at `offchainlabs/orbit-actions`: diff --git a/scripts/foundry/arbos-upgrades/at-timestamp/DeployUpgradeArbOSVersionAtTimestampAction.s.sol b/scripts/foundry/arbos-upgrades/at-timestamp/DeployUpgradeArbOSVersionAtTimestampAction.s.sol index 7a326e6f..e57c8e4e 100644 --- a/scripts/foundry/arbos-upgrades/at-timestamp/DeployUpgradeArbOSVersionAtTimestampAction.s.sol +++ b/scripts/foundry/arbos-upgrades/at-timestamp/DeployUpgradeArbOSVersionAtTimestampAction.s.sol @@ -25,7 +25,8 @@ contract DeployUpgradeArbOSVersionAtTimestampActionScript is Script { vm.startBroadcast(); - // finally deploy upgrade action + // Deploy the action contract last. The CLI identifies the deployed action + // by taking the last CREATE from the broadcast file. new UpgradeArbOSVersionAtTimestampAction({ _newArbOSVersion: uint64(arbosVersion), _upgradeTimestamp: uint64(scheduleTimestamp) }); diff --git a/scripts/foundry/contract-upgrades/1.2.1/DeployNitroContracts1Point2Point1UpgradeAction.s.sol b/scripts/foundry/contract-upgrades/1.2.1/DeployNitroContracts1Point2Point1UpgradeAction.s.sol index 5bd18a0b..53abf4fc 100644 --- a/scripts/foundry/contract-upgrades/1.2.1/DeployNitroContracts1Point2Point1UpgradeAction.s.sol +++ b/scripts/foundry/contract-upgrades/1.2.1/DeployNitroContracts1Point2Point1UpgradeAction.s.sol @@ -82,7 +82,8 @@ contract DeployNitroContracts1Point2Point1UpgradeActionScript is Script { abi.encode(vm.envUint("MAX_DATA_SIZE"), reader4844Address, vm.envBool("IS_FEE_TOKEN_CHAIN")) ); - // finally deploy upgrade action + // Deploy the action contract last. The CLI identifies the deployed action + // by taking the last CREATE from the broadcast file. new NitroContracts1Point2Point1UpgradeAction({ _newWasmModuleRoot: vm.envBytes32("WASM_MODULE_ROOT"), _newSequencerInboxImpl: seqInbox, diff --git a/scripts/foundry/contract-upgrades/2.1.0/DeployNitroContracts2Point1Point0UpgradeAction.s.sol b/scripts/foundry/contract-upgrades/2.1.0/DeployNitroContracts2Point1Point0UpgradeAction.s.sol index 8816c21b..b0828174 100644 --- a/scripts/foundry/contract-upgrades/2.1.0/DeployNitroContracts2Point1Point0UpgradeAction.s.sol +++ b/scripts/foundry/contract-upgrades/2.1.0/DeployNitroContracts2Point1Point0UpgradeAction.s.sol @@ -86,7 +86,8 @@ contract DeployNitroContracts2Point1Point0UpgradeActionScript is DeploymentHelpe "/node_modules/@arbitrum/nitro-contracts-2.1.0/build/contracts/src/rollup/RollupUserLogic.sol/RollupUserLogic.json" ); - // finally deploy upgrade action + // Deploy the action contract last. The CLI identifies the deployed action + // by taking the last CREATE from the broadcast file. new NitroContracts2Point1Point0UpgradeAction({ _newWasmModuleRoot: WASM_MODULE_ROOT, _newChallengeManagerImpl: challengeManager, diff --git a/scripts/foundry/contract-upgrades/2.1.2/DeployNitroContracts2Point1Point2UpgradeAction.s.sol b/scripts/foundry/contract-upgrades/2.1.2/DeployNitroContracts2Point1Point2UpgradeAction.s.sol index b0e68052..f48e4498 100644 --- a/scripts/foundry/contract-upgrades/2.1.2/DeployNitroContracts2Point1Point2UpgradeAction.s.sol +++ b/scripts/foundry/contract-upgrades/2.1.2/DeployNitroContracts2Point1Point2UpgradeAction.s.sol @@ -19,7 +19,8 @@ contract DeployNitroContracts2Point1Point2UpgradeActionScript is DeploymentHelpe "/node_modules/@arbitrum/nitro-contracts-2.1.2/build/contracts/src/bridge/ERC20Bridge.sol/ERC20Bridge.json" ); - // deploy upgrade action + // Deploy the action contract last. The CLI identifies the deployed action + // by taking the last CREATE from the broadcast file. new NitroContracts2Point1Point2UpgradeAction(newBridgeImpl); vm.stopBroadcast(); diff --git a/scripts/foundry/contract-upgrades/2.1.3/DeployNitroContracts2Point1Point3UpgradeAction.s.sol b/scripts/foundry/contract-upgrades/2.1.3/DeployNitroContracts2Point1Point3UpgradeAction.s.sol index 819251b5..8d2ed77a 100644 --- a/scripts/foundry/contract-upgrades/2.1.3/DeployNitroContracts2Point1Point3UpgradeAction.s.sol +++ b/scripts/foundry/contract-upgrades/2.1.3/DeployNitroContracts2Point1Point3UpgradeAction.s.sol @@ -53,7 +53,8 @@ contract DeployNitroContracts2Point1Point3UpgradeActionScript is DeploymentHelpe abi.encode(vm.envUint("MAX_DATA_SIZE"), reader4844Address, true) ); - // deploy upgrade action + // Deploy the action contract last. The CLI identifies the deployed action + // by taking the last CREATE from the broadcast file. new NitroContracts2Point1Point3UpgradeAction( newEthInboxImpl, newERC20InboxImpl, newEthSeqInboxImpl, newErc20SeqInboxImpl ); diff --git a/scripts/foundry/fast-confirm/DeployEnableFastConfirmAction.s.sol b/scripts/foundry/fast-confirm/DeployEnableFastConfirmAction.s.sol index 26d2f7b8..b819a8c2 100644 --- a/scripts/foundry/fast-confirm/DeployEnableFastConfirmAction.s.sol +++ b/scripts/foundry/fast-confirm/DeployEnableFastConfirmAction.s.sol @@ -16,7 +16,8 @@ contract DeployEnableFastConfirmAction is DeploymentHelpersScript { function run() public { vm.startBroadcast(); - // deploy action + // Deploy the action contract last. The CLI identifies the deployed action + // by taking the last CREATE from the broadcast file. new EnableFastConfirmAction({ gnosisSafeProxyFactory: GNOSIS_SAFE_PROXY_FACTORY, gnosisSafe1_3_0: GNOSIS_SAFE_1_3_0, diff --git a/scripts/foundry/sequencer/max-time-variation/DeploySetSequencerInboxMaxTimeVariationAction.s.sol b/scripts/foundry/sequencer/max-time-variation/DeploySetSequencerInboxMaxTimeVariationAction.s.sol index 69357172..556ae36c 100644 --- a/scripts/foundry/sequencer/max-time-variation/DeploySetSequencerInboxMaxTimeVariationAction.s.sol +++ b/scripts/foundry/sequencer/max-time-variation/DeploySetSequencerInboxMaxTimeVariationAction.s.sol @@ -14,7 +14,8 @@ contract DeploySetSequencerInboxMaxTimeVariationActionScript is Script { function run() public { vm.startBroadcast(); - // finally deploy upgrade action + // Deploy the action contract last. The CLI identifies the deployed action + // by taking the last CREATE from the broadcast file. new SetSequencerInboxMaxTimeVariationAction({ _delayBlocks: vm.envUint("DELAY_BLOCKS"), _futureBlocks: vm.envUint("FUTURE_BLOCKS"), diff --git a/scripts/foundry/stylus/setCacheManager/DeployAddWasmCacheManagerAction.s.sol b/scripts/foundry/stylus/setCacheManager/DeployAddWasmCacheManagerAction.s.sol index 2d3296ef..d029c955 100644 --- a/scripts/foundry/stylus/setCacheManager/DeployAddWasmCacheManagerAction.s.sol +++ b/scripts/foundry/stylus/setCacheManager/DeployAddWasmCacheManagerAction.s.sol @@ -31,7 +31,8 @@ contract DeployAddWasmCacheManagerActionScript is DeploymentHelpersScript { ICacheManager cacheManager = ICacheManager(cacheManagerProxy); cacheManager.initialize(uint64(vm.envUint("INIT_CACHE_SIZE")), uint64(vm.envUint("INIT_DECAY"))); - // deploy action + // Deploy the action contract last. The CLI identifies the deployed action + // by taking the last CREATE from the broadcast file. new AddWasmCacheManagerAction({ _wasmCachemanager: address(cacheManager), _targetArbOSVersion: TARGET_ARBOS_VERSION }); From cb1cbeb3ad6cb2baf5f0006fac2f33196d9c1c6b Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Thu, 12 Mar 2026 13:05:17 +0000 Subject: [PATCH 43/44] fix: address PR review findings across CLI, Docker, and verify scripts - Block path traversal in router (reject ".." in path args) - Anchor version regex to prevent misrouting on nested paths - Fix .dockerignore excluding .env.sample and env template files - Remove incorrect 2.1.3 verify script (checked nativeTokenDecimals, a 2.1.2 concern; reverts on ETH-native chains) - Add ROLLUP env var to 1.2.1 and 2.1.0 env templates for verify scripts - Use node:22 and foundryup --version stable in Dockerfile - Use submodules: recursive in CI instead of forge install - Add broadcast guidance on arbos execute without FOUNDRY_BROADCAST - Add missing deploy script null check in contract-upgrade cmdExecute - Wrap getChainId with try/catch for consistent error handling - Load .env from repo root instead of cwd for path consistency - Warn on fallback to /app when repo root not found - Drop isCategoryDir messaging in directory listing --- .dockerignore | 4 +-- .github/workflows/publish-docker.yml | 8 +----- .github/workflows/test-docker.yml | 8 +----- Dockerfile | 4 +-- .../.env.arbsepolia-upgrade.example | 3 ++- .../env-templates/.env.local-upgrade.example | 1 + .../.env.sepolia-upgrade.example | 3 ++- .../contract-upgrades/2.1.0/.env.sample | 3 ++- .../foundry/contract-upgrades/2.1.3/README.md | 9 +++---- ...fyNitroContracts2Point1Point3Upgrade.s.sol | 25 ------------------- src/cli/commands/arbos-upgrade.ts | 4 +++ src/cli/commands/contract-upgrade.ts | 6 +++++ src/cli/router.ts | 22 +++++++--------- src/cli/utils/env.ts | 8 ++++-- src/cli/utils/forge.ts | 12 ++++++--- 15 files changed, 49 insertions(+), 71 deletions(-) delete mode 100644 scripts/foundry/contract-upgrades/2.1.3/VerifyNitroContracts2Point1Point3Upgrade.s.sol diff --git a/.dockerignore b/.dockerignore index 20bf558b..10384db8 100644 --- a/.dockerignore +++ b/.dockerignore @@ -9,9 +9,7 @@ artifacts/ typechain-types/ # Environment files (contain secrets) -.env -.env.* -!.env.example +/.env # Git .git/ diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml index b22fe35c..422469bd 100644 --- a/.github/workflows/publish-docker.yml +++ b/.github/workflows/publish-docker.yml @@ -13,14 +13,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 with: - version: stable - - - name: Install forge dependencies - run: forge install + submodules: recursive - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 04a7f78a..eea090e4 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -11,14 +11,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 with: - version: stable - - - name: Install forge dependencies - run: forge install + submodules: recursive - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 diff --git a/Dockerfile b/Dockerfile index 28512779..73760fdb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:18-slim +FROM node:22-slim # Install dependencies for Foundry, git, and jq (for JSON parsing in upgrade scripts) RUN apt-get update && apt-get install -y \ @@ -9,7 +9,7 @@ RUN apt-get update && apt-get install -y \ # Install Foundry ENV PATH="/root/.foundry/bin:${PATH}" -RUN curl -L https://foundry.paradigm.xyz | bash && foundryup +RUN curl -L https://foundry.paradigm.xyz | bash && foundryup --version stable # Install Yarn Classic (v1) - matches the repo's yarn.lock format RUN npm install -g --force yarn@1.22.22 diff --git a/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.arbsepolia-upgrade.example b/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.arbsepolia-upgrade.example index 260b5a5e..1f536f21 100644 --- a/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.arbsepolia-upgrade.example +++ b/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.arbsepolia-upgrade.example @@ -12,4 +12,5 @@ MAX_DATA_SIZE=104857 UPGRADE_ACTION_ADDRESS="" INBOX_ADDRESS="" PROXY_ADMIN_ADDRESS="" -PARENT_UPGRADE_EXECUTOR_ADDRESS="" \ No newline at end of file +PARENT_UPGRADE_EXECUTOR_ADDRESS="" +ROLLUP= \ No newline at end of file diff --git a/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.local-upgrade.example b/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.local-upgrade.example index 49bb3dd1..b7b88cd8 100644 --- a/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.local-upgrade.example +++ b/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.local-upgrade.example @@ -13,3 +13,4 @@ UPGRADE_ACTION_ADDRESS="0x7B5F0B437EE68A22992DdD629AA22525Fc5dfa9A" INBOX_ADDRESS="0xc76802b32fc760324d3061a80890cd47d0e135e8" PROXY_ADMIN_ADDRESS="0xef83c42810b3816882d83cfdc922333c05448b55" PARENT_UPGRADE_EXECUTOR_ADDRESS="0x9115d7feb9698fd4349bc9c4f1fbe96b583dc044" +ROLLUP= diff --git a/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.sepolia-upgrade.example b/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.sepolia-upgrade.example index bcf066c0..f00d9e5a 100644 --- a/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.sepolia-upgrade.example +++ b/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.sepolia-upgrade.example @@ -12,4 +12,5 @@ MAX_DATA_SIZE=117964 UPGRADE_ACTION_ADDRESS="0xBC1e0ca800781F58F3a2f73dA4D895FdD61B0Cb5" INBOX_ADDRESS="0xaAe29B0366299461418F5324a79Afc425BE5ae21" PROXY_ADMIN_ADDRESS="0xdd63bCAA89d7c3199Ef220c1Dd59C49F821078B8" -PARENT_UPGRADE_EXECUTOR_ADDRESS="0x5FEe78FE9AD96c1d8557C6D6BB22Eb5A61eeD315" \ No newline at end of file +PARENT_UPGRADE_EXECUTOR_ADDRESS="0x5FEe78FE9AD96c1d8557C6D6BB22Eb5A61eeD315" +ROLLUP= \ No newline at end of file diff --git a/scripts/foundry/contract-upgrades/2.1.0/.env.sample b/scripts/foundry/contract-upgrades/2.1.0/.env.sample index 6008a8cf..bad2a3dd 100644 --- a/scripts/foundry/contract-upgrades/2.1.0/.env.sample +++ b/scripts/foundry/contract-upgrades/2.1.0/.env.sample @@ -10,4 +10,5 @@ INBOX_ADDRESS= PROXY_ADMIN_ADDRESS= PARENT_UPGRADE_EXECUTOR_ADDRESS= TARGET_WASM_MODULE_ROOT=0x184884e1eb9fefdc158f6c8ac912bb183bf3cf83f0090317e0bc4ac5860baa39 -PARENT_CHAIN_IS_ARBITRUM=true \ No newline at end of file +PARENT_CHAIN_IS_ARBITRUM=true +ROLLUP= \ No newline at end of file diff --git a/scripts/foundry/contract-upgrades/2.1.3/README.md b/scripts/foundry/contract-upgrades/2.1.3/README.md index adff9181..0d036527 100644 --- a/scripts/foundry/contract-upgrades/2.1.3/README.md +++ b/scripts/foundry/contract-upgrades/2.1.3/README.md @@ -72,11 +72,10 @@ forge script --sender $EXECUTOR --rpc-url $PARENT_CHAIN_RPC --broadcast ExecuteN If you have a multisig as executor, you can still run the above command without broadcasting to get the payload for the multisig transaction. -4. That's it, upgrade has been performed. You can verify by running: - -```bash -forge script --rpc-url $PARENT_CHAIN_RPC VerifyNitroContracts2Point1Point3Upgrade -vvv -``` +4. That's it, upgrade has been performed. There is no automated verification script for + 2.1.3 at this time. (The previous script incorrectly checked `nativeTokenDecimals`, + which is a 2.1.2 concern -- 2.1.3 upgrades Inbox and SequencerInbox, not the bridge, + and `nativeTokenDecimals()` reverts on ETH-native chains.) ## FAQ diff --git a/scripts/foundry/contract-upgrades/2.1.3/VerifyNitroContracts2Point1Point3Upgrade.s.sol b/scripts/foundry/contract-upgrades/2.1.3/VerifyNitroContracts2Point1Point3Upgrade.s.sol deleted file mode 100644 index 353130df..00000000 --- a/scripts/foundry/contract-upgrades/2.1.3/VerifyNitroContracts2Point1Point3Upgrade.s.sol +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.16; - -import "forge-std/Script.sol"; - -interface IInbox { - function bridge() external view returns (address); -} - -interface IBridge { - function nativeTokenDecimals() external view returns (uint8); -} - -/** - * @title VerifyNitroContracts2Point1Point3Upgrade - * @notice Verifies the upgrade to Nitro Contracts 2.1.3 by checking nativeTokenDecimals - */ -contract VerifyNitroContracts2Point1Point3Upgrade is Script { - function run() public view { - address inbox = vm.envAddress("INBOX_ADDRESS"); - address bridge = IInbox(inbox).bridge(); - uint8 decimals = IBridge(bridge).nativeTokenDecimals(); - console.log("nativeTokenDecimals:", decimals); - } -} diff --git a/src/cli/commands/arbos-upgrade.ts b/src/cli/commands/arbos-upgrade.ts index 53244cf2..e2606f13 100644 --- a/src/cli/commands/arbos-upgrade.ts +++ b/src/cli/commands/arbos-upgrade.ts @@ -63,6 +63,10 @@ async function executeUpgrade( await runCastSend({ to: upgradeExecutor, data: calldata, rpcUrl }) console.log('ArbOS upgrade scheduled successfully') + } else { + console.log( + 'Set FOUNDRY_BROADCAST=true in .env to broadcast this transaction.' + ) } } diff --git a/src/cli/commands/contract-upgrade.ts b/src/cli/commands/contract-upgrade.ts index ab6b05b1..578fde99 100644 --- a/src/cli/commands/contract-upgrade.ts +++ b/src/cli/commands/contract-upgrade.ts @@ -69,6 +69,12 @@ async function cmdExecute(version: string): Promise { const versionDir = getVersionDir(version) const rpcUrl = requireEnv('PARENT_CHAIN_RPC') const deployScript = findScript(versionDir, /^Deploy.*\.s\.sol$/) + if (!deployScript && !process.env.UPGRADE_ACTION_ADDRESS) { + die( + `No deploy script found in ${versionDir}.\n` + + 'Set UPGRADE_ACTION_ADDRESS in .env to provide the action address manually.' + ) + } const actionAddress = await resolveActionAddress(deployScript, rpcUrl) const executeScript = findScript(versionDir, /^Execute.*\.s\.sol$/) diff --git a/src/cli/router.ts b/src/cli/router.ts index 49d69529..23d7c077 100644 --- a/src/cli/router.ts +++ b/src/cli/router.ts @@ -48,13 +48,9 @@ function listDirectory(dir: string): void { let relPath = path.relative(scriptsDir, dir) if (relPath === '.') relPath = '' - const isVersionDir = /^contract-upgrades\/[0-9]/.test(relPath) + const isVersionDir = /^contract-upgrades\/[0-9][^/]*$/.test(relPath) const isArbosDir = relPath === 'arbos-upgrades/at-timestamp' - const isCategoryDir = /^[^/]+$/.test(relPath) && relPath !== '' - - if (isCategoryDir) { - console.log('Browse deeper to see available scripts and commands.') - } else if (isVersionDir || isArbosDir) { + if (isVersionDir || isArbosDir) { const hasEnvTemplates = fs.existsSync(path.join(dir, 'env-templates')) if (hasEnvTemplates) { console.log( @@ -73,12 +69,7 @@ function listDirectory(dir: string): void { } } - if (isCategoryDir) { - console.log('') - console.log( - `Example: ${relPath}/${contents.find(c => !c.startsWith('.')) ?? ''}` - ) - } else if (isVersionDir) { + if (isVersionDir) { console.log('') console.log('Commands:') console.log(` ${relPath}/deploy`) @@ -132,6 +123,11 @@ export async function router( return } + // Block path traversal outside the scripts directory + if (pathArg.includes('..')) { + die('Invalid path: ".." is not allowed') + } + const fullPath = path.join(scriptsDir, pathArg) const parentPath = path.dirname(fullPath) const basename = path.basename(pathArg) @@ -139,7 +135,7 @@ export async function router( if (!fs.existsSync(fullPath) && fs.existsSync(parentPath)) { const relParent = path.relative(scriptsDir, parentPath) - if (/^contract-upgrades\/[0-9]/.test(relParent)) { + if (/^contract-upgrades\/[0-9][^/]*$/.test(relParent)) { const version = path.basename(relParent) switch (basename) { diff --git a/src/cli/utils/env.ts b/src/cli/utils/env.ts index ebc014da..bc906555 100644 --- a/src/cli/utils/env.ts +++ b/src/cli/utils/env.ts @@ -17,7 +17,8 @@ function findRepoRoot(): string | null { } export function loadEnv(): void { - const envPath = path.join(process.cwd(), '.env') + const repoRoot = findRepoRoot() + const envPath = path.join(repoRoot ?? process.cwd(), '.env') if (fs.existsSync(envPath)) { dotenv.config({ path: envPath }) } @@ -40,5 +41,8 @@ export function getScriptsDir(): string { } export function getRepoRoot(): string { - return findRepoRoot() || '/app' + const root = findRepoRoot() + if (root) return root + console.warn('Could not find repo root, assuming /app') + return '/app' } diff --git a/src/cli/utils/forge.ts b/src/cli/utils/forge.ts index 5554bba4..711b93ec 100644 --- a/src/cli/utils/forge.ts +++ b/src/cli/utils/forge.ts @@ -65,10 +65,14 @@ export async function runCastCall(options: CastCallOptions): Promise { } export async function getChainId(rpcUrl: string): Promise { - const result = await execa('cast', ['chain-id', '--rpc-url', rpcUrl], { - stderr: 'inherit', - }) - return result.stdout.trim() + try { + const result = await execa('cast', ['chain-id', '--rpc-url', rpcUrl], { + stderr: 'inherit', + }) + return result.stdout.trim() + } catch { + die(`Failed to get chain ID from ${rpcUrl}`) + } } // Assumes the action contract is the last CREATE in the broadcast file. From e4fed498f5de2e26474c5cc68cfedc15c3ec6d7b Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Thu, 12 Mar 2026 14:06:51 +0000 Subject: [PATCH 44/44] fix: use --install flag for foundryup v1.5.0 CLI change foundryup was rewritten and --version now prints the tool version instead of installing a specific Foundry version. The new flag is --install. --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 73760fdb..14fcdadc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,7 @@ RUN apt-get update && apt-get install -y \ # Install Foundry ENV PATH="/root/.foundry/bin:${PATH}" -RUN curl -L https://foundry.paradigm.xyz | bash && foundryup --version stable +RUN curl -L https://foundry.paradigm.xyz | bash && foundryup --install stable # Install Yarn Classic (v1) - matches the repo's yarn.lock format RUN npm install -g --force yarn@1.22.22