From 1a2accdb51631b5669d602fe6262baf53fca5f07 Mon Sep 17 00:00:00 2001 From: bemic <50417294+bemic@users.noreply.github.com> Date: Wed, 22 Jul 2026 23:38:21 +0200 Subject: [PATCH 1/5] ethereum: stop sourcing env files in deployment helpers --- ethereum/sh/deployCoreBridge.sh | 4 +--- ethereum/sh/deployCoreBridgeTron.js | 3 +-- ethereum/sh/deployCoreBridgeTron.sh | 8 +++----- ethereum/sh/deployCoreShutdown.sh | 2 -- ethereum/sh/deployDelegatedGuardians.sh | 3 --- ethereum/sh/deployNFTBridge.sh | 2 -- ethereum/sh/deployTokenBridge.sh | 2 -- ethereum/sh/deployTokenBridgeShutdown.sh | 2 -- 8 files changed, 5 insertions(+), 21 deletions(-) diff --git a/ethereum/sh/deployCoreBridge.sh b/ethereum/sh/deployCoreBridge.sh index f84a0806b44..b512e8ec04d 100755 --- a/ethereum/sh/deployCoreBridge.sh +++ b/ethereum/sh/deployCoreBridge.sh @@ -2,8 +2,6 @@ # MNEMONIC= ./sh/deployCoreBridge.sh -. .env - [[ -z $INIT_SIGNERS ]] && { echo "Missing INIT_SIGNERS"; exit 1; } [[ -z $INIT_CHAIN_ID ]] && { echo "Missing INIT_CHAIN_ID"; exit 1; } [[ -z $INIT_GOV_CHAIN_ID ]] && { echo "Missing INIT_GOV_CHAIN_ID"; exit 1; } @@ -29,4 +27,4 @@ echo "-- Wormhole Core Addresses ----------------------------------------------- echo "| Setup address | $SETUP_ADDRESS |" echo "| Implementation address | $IMPLEMENTATION_ADDRESS |" echo "| Wormhole address | $WORMHOLE_ADDRESS |" -echo "-----------------------------------------------------------------------------" \ No newline at end of file +echo "-----------------------------------------------------------------------------" diff --git a/ethereum/sh/deployCoreBridgeTron.js b/ethereum/sh/deployCoreBridgeTron.js index f62a2adeaa0..52203efd034 100755 --- a/ethereum/sh/deployCoreBridgeTron.js +++ b/ethereum/sh/deployCoreBridgeTron.js @@ -15,8 +15,7 @@ const FEE_LIMIT_SUN = Number(process.env.TRON_FEE_LIMIT_SUN || 5_000_000_000); / const ORIGIN_ENERGY_LIMIT = Number(process.env.TRON_ORIGIN_ENERGY_LIMIT || 10_000_000); const USER_FEE_PERCENT = Number(process.env.TRON_USER_FEE_PERCENT || 100); -// .env is shell-sourced, which strips the double quotes around array elements. -// Re-quote any bare 0x-hex strings so JSON.parse can handle either form. +// Accept both JSON arrays and the legacy bare-hex array format. const INIT_SIGNERS = JSON.parse( (process.env.INIT_SIGNERS || '[]') .replace(/"/g, '') diff --git a/ethereum/sh/deployCoreBridgeTron.sh b/ethereum/sh/deployCoreBridgeTron.sh index bbe2a67c41b..97bca40ff29 100755 --- a/ethereum/sh/deployCoreBridgeTron.sh +++ b/ethereum/sh/deployCoreBridgeTron.sh @@ -1,13 +1,11 @@ #!/bin/bash # Deploy Wormhole Core to Tron via tronweb-style HTTP API. -# Usage: from ethereum/, copy env/.env.tron.testnet to .env, fill in TRON_PRIVATE_KEY, -# then run ./sh/deployCoreBridgeTron.sh +# Usage: from ethereum/, provide the required values in the operator environment, +# then run ./sh/deployCoreBridgeTron.sh. set -euo pipefail -set -a -. .env -set +a +# Configuration must be supplied by the operator environment. [[ -z "${TRON_PRIVATE_KEY:-${MNEMONIC:-}}" ]] && { echo "Missing TRON_PRIVATE_KEY (or MNEMONIC)"; exit 1; } [[ -z "${INIT_SIGNERS:-}" ]] && { echo "Missing INIT_SIGNERS"; exit 1; } diff --git a/ethereum/sh/deployCoreShutdown.sh b/ethereum/sh/deployCoreShutdown.sh index 674708f96fd..8af8ae6d736 100755 --- a/ethereum/sh/deployCoreShutdown.sh +++ b/ethereum/sh/deployCoreShutdown.sh @@ -2,8 +2,6 @@ # MNEMONIC= ./sh/deployCoreShutdown.sh -. .env - [[ -z $INIT_EVM_CHAIN_ID ]] && { echo "Missing INIT_EVM_CHAIN_ID"; exit 1; } [[ -z $MNEMONIC ]] && { echo "Missing MNEMONIC"; exit 1; } diff --git a/ethereum/sh/deployDelegatedGuardians.sh b/ethereum/sh/deployDelegatedGuardians.sh index 8d9219c2a9f..1796a99e079 100644 --- a/ethereum/sh/deployDelegatedGuardians.sh +++ b/ethereum/sh/deployDelegatedGuardians.sh @@ -1,8 +1,5 @@ #!/bin/bash -. .env - - [[ -z $MNEMONIC ]] && { echo "Missing MNEMONIC"; exit 1; } [[ -z $RPC_URL ]] && { echo "Missing RPC_URL"; exit 1; } [[ -z $WORMHOLE_ADDRESS ]] && { echo "Missing WORMHOLE_ADDRESS"; exit 1; } diff --git a/ethereum/sh/deployNFTBridge.sh b/ethereum/sh/deployNFTBridge.sh index 283654787ea..70d74eda90f 100755 --- a/ethereum/sh/deployNFTBridge.sh +++ b/ethereum/sh/deployNFTBridge.sh @@ -2,8 +2,6 @@ # MNEMONIC= WORMHOLE_ADDRESS= ./sh/deployNFTBridge.sh -. .env - [[ -z $INIT_EVM_CHAIN_ID ]] && { echo "Missing INIT_EVM_CHAIN_ID"; exit 1; } [[ -z $BRIDGE_INIT_CHAIN_ID ]] && { echo "Missing BRIDGE_INIT_CHAIN_ID"; exit 1; } diff --git a/ethereum/sh/deployTokenBridge.sh b/ethereum/sh/deployTokenBridge.sh index f42d53af340..3603b3a4293 100755 --- a/ethereum/sh/deployTokenBridge.sh +++ b/ethereum/sh/deployTokenBridge.sh @@ -2,8 +2,6 @@ # MNEMONIC= WORMHOLE_ADDRESS= ./sh/deployTokenBridge.sh -. .env - [[ -z $INIT_EVM_CHAIN_ID ]] && { echo "Missing INIT_EVM_CHAIN_ID"; exit 1; } [[ -z $BRIDGE_INIT_CHAIN_ID ]] && { echo "Missing BRIDGE_INIT_CHAIN_ID"; exit 1; } diff --git a/ethereum/sh/deployTokenBridgeShutdown.sh b/ethereum/sh/deployTokenBridgeShutdown.sh index be22b72cd30..3d07358af58 100755 --- a/ethereum/sh/deployTokenBridgeShutdown.sh +++ b/ethereum/sh/deployTokenBridgeShutdown.sh @@ -2,8 +2,6 @@ # MNEMONIC= ./sh/deployTokenBridgeShutdown.sh -. .env - [[ -z $INIT_EVM_CHAIN_ID ]] && { echo "Missing INIT_EVM_CHAIN_ID"; exit 1; } [[ -z $MNEMONIC ]] && { echo "Missing MNEMONIC"; exit 1; } From e272ccd3589628f7a90aba68297f4161d4e085c3 Mon Sep 17 00:00:00 2001 From: bemic <50417294+bemic@users.noreply.github.com> Date: Wed, 22 Jul 2026 23:40:16 +0200 Subject: [PATCH 2/5] ethereum: stop executing env files in registration helpers --- ethereum/sh/devnetInitialization.sh | 6 ++-- ethereum/sh/registerAllChainsOnTokenBridge.sh | 33 +++++++++++-------- ethereum/sh/registerChainsNFTBridge.sh | 17 +++++----- ethereum/sh/registerChainsTokenBridge.sh | 17 +++++----- 4 files changed, 37 insertions(+), 36 deletions(-) diff --git a/ethereum/sh/devnetInitialization.sh b/ethereum/sh/devnetInitialization.sh index 8826b3b907b..28671418699 100755 --- a/ethereum/sh/devnetInitialization.sh +++ b/ethereum/sh/devnetInitialization.sh @@ -14,10 +14,8 @@ else fi fi -# Load the environment variables from .env -if [ -f .env ]; then - source .env -else +# Keep .env as data for registration VAA collection below; do not execute it. +if [ ! -f .env ]; then echo "The .env file does not exist." exit 1 fi diff --git a/ethereum/sh/registerAllChainsOnTokenBridge.sh b/ethereum/sh/registerAllChainsOnTokenBridge.sh index b4a1bc93892..4bd53acd1a1 100755 --- a/ethereum/sh/registerAllChainsOnTokenBridge.sh +++ b/ethereum/sh/registerAllChainsOnTokenBridge.sh @@ -13,22 +13,19 @@ if [ $# != 3 ]; then exit 1 fi -[[ -z $MNEMONIC ]] && { echo "Missing MNEMONIC"; exit 1; } +[[ -z ${MNEMONIC:-} ]] && { echo "Missing MNEMONIC"; exit 1; } network=$1 chain=$2 token_bridge_address=$3 -# Figure out which env file and VAA files to use. -env_file="" +# Figure out which VAA file to use. input_file="" case "$network" in mainnet) - env_file="env/.env.${chain}" input_file="../deployments/mainnet/tokenBridgeVAAs.csv" ;; testnet) - env_file="env/.env.${chain}.testnet" input_file="../deployments/testnet/tokenBridgeVAAs.csv" ;; *) echo "unknown network $network, must be testnet or mainnet" >&2 @@ -36,10 +33,14 @@ case "$network" in ;; esac -# Source in the env file to get the RPC and forge arguments. -. ${env_file} +# Do not continue with an empty registration batch when the CSV is unavailable. +[[ -r "$input_file" ]] || { echo "Unable to read $input_file" >&2; exit 1; } -[[ -z $RPC_URL ]] && { echo "Missing RPC_URL"; exit 1; } +# Configuration must be supplied by the operator environment. +[[ -z ${RPC_URL:-} ]] && { echo "Missing RPC_URL"; exit 1; } + +# Match the target by Wormhole chain ID, not an ambiguous name substring. +worm_chain_id=$(worm info chain-id "$chain") || exit 1 # Build one long string of all the vaas in the input file. vaas="" @@ -57,8 +58,8 @@ do vaa=`echo $line | cut -d, -f2` # Skip this chain. (We don't want to register this chain on itself.) - echo $tag | grep -i ${chain} > /dev/null - if [ $? == 0 ]; then + if [[ "$tag" == *"($worm_chain_id)"* ]]; then + found_us=$((found_us + 1)) continue fi @@ -68,16 +69,20 @@ do fi vaas="${vaas}0x${vaa}" - count=$(($count+1)) + count=$(($count+1)) done < "$input_file" +# The target row is required as a sanity check and must be unambiguous. +[ "$found_us" -eq 1 ] || { echo "Expected exactly one VAA for $chain" >&2; exit 1; } + # Make it look like an array. vaas="[${vaas}]" echo $vaas echo "Submitting ${count} VAAs to ${network} ${chain} token bridge at address ${token_bridge_address} and rpc ${RPC_URL}" +# Keep the repository-derived VAA list inside one Forge argument. forge script ./forge-scripts/RegisterChainsTokenBridge.s.sol:RegisterChainsTokenBridge \ - --sig "run(address,bytes[])" $token_bridge_address $vaas \ - --rpc-url $RPC_URL \ - --private-key $MNEMONIC \ + --sig "run(address,bytes[])" "$token_bridge_address" "$vaas" \ + --rpc-url "$RPC_URL" \ + --private-key "$MNEMONIC" \ --broadcast ${FORGE_ARGS} diff --git a/ethereum/sh/registerChainsNFTBridge.sh b/ethereum/sh/registerChainsNFTBridge.sh index dd7bbc0c285..2c7c4f6f35d 100644 --- a/ethereum/sh/registerChainsNFTBridge.sh +++ b/ethereum/sh/registerChainsNFTBridge.sh @@ -2,15 +2,14 @@ # MNEMONIC= ./sh/registerChainsNFTBridge.sh -. .env - -[[ -z $MNEMONIC ]] && { echo "Missing MNEMONIC"; exit 1; } -[[ -z $RPC_URL ]] && { echo "Missing RPC_URL"; exit 1; } -[[ -z $NFT_BRIDGE_ADDRESS ]] && { echo "Missing NFT_BRIDGE_ADDRESS"; exit 1; } -[[ -z $NFT_BRIDGE_REGISTRATION_VAAS ]] && { echo "Missing NFT_BRIDGE_REGISTRATION_VAAS"; exit 1; } +[[ -z ${MNEMONIC:-} ]] && { echo "Missing MNEMONIC"; exit 1; } +[[ -z ${RPC_URL:-} ]] && { echo "Missing RPC_URL"; exit 1; } +[[ -z ${NFT_BRIDGE_ADDRESS:-} ]] && { echo "Missing NFT_BRIDGE_ADDRESS"; exit 1; } +[[ -z ${NFT_BRIDGE_REGISTRATION_VAAS:-} ]] && { echo "Missing NFT_BRIDGE_REGISTRATION_VAAS"; exit 1; } +# Keep each operator value in its intended Forge argument. forge script ./forge-scripts/RegisterChainsNFTBridge.s.sol:RegisterChainsNFTBridge \ - --sig "run(address,bytes[])" $NFT_BRIDGE_ADDRESS $NFT_BRIDGE_REGISTRATION_VAAS \ - --rpc-url $RPC_URL \ - --private-key $MNEMONIC \ + --sig "run(address,bytes[])" "$NFT_BRIDGE_ADDRESS" "$NFT_BRIDGE_REGISTRATION_VAAS" \ + --rpc-url "$RPC_URL" \ + --private-key "$MNEMONIC" \ --broadcast diff --git a/ethereum/sh/registerChainsTokenBridge.sh b/ethereum/sh/registerChainsTokenBridge.sh index c9a01f80eb3..f00ca1af8b8 100755 --- a/ethereum/sh/registerChainsTokenBridge.sh +++ b/ethereum/sh/registerChainsTokenBridge.sh @@ -2,15 +2,14 @@ # MNEMONIC= ./sh/registerChainsTokenBridge.sh -. .env - -[[ -z $MNEMONIC ]] && { echo "Missing MNEMONIC"; exit 1; } -[[ -z $RPC_URL ]] && { echo "Missing RPC_URL"; exit 1; } -[[ -z $TOKEN_BRIDGE_ADDRESS ]] && { echo "Missing TOKEN_BRIDGE_ADDRESS"; exit 1; } -[[ -z $TOKEN_BRIDGE_REGISTRATION_VAAS ]] && { echo "Missing TOKEN_BRIDGE_REGISTRATION_VAAS"; exit 1; } +[[ -z ${MNEMONIC:-} ]] && { echo "Missing MNEMONIC"; exit 1; } +[[ -z ${RPC_URL:-} ]] && { echo "Missing RPC_URL"; exit 1; } +[[ -z ${TOKEN_BRIDGE_ADDRESS:-} ]] && { echo "Missing TOKEN_BRIDGE_ADDRESS"; exit 1; } +[[ -z ${TOKEN_BRIDGE_REGISTRATION_VAAS:-} ]] && { echo "Missing TOKEN_BRIDGE_REGISTRATION_VAAS"; exit 1; } +# Keep each operator value in its intended Forge argument. forge script ./forge-scripts/RegisterChainsTokenBridge.s.sol:RegisterChainsTokenBridge \ - --sig "run(address,bytes[])" $TOKEN_BRIDGE_ADDRESS $TOKEN_BRIDGE_REGISTRATION_VAAS \ - --rpc-url $RPC_URL \ - --private-key $MNEMONIC \ + --sig "run(address,bytes[])" "$TOKEN_BRIDGE_ADDRESS" "$TOKEN_BRIDGE_REGISTRATION_VAAS" \ + --rpc-url "$RPC_URL" \ + --private-key "$MNEMONIC" \ --broadcast From fef0ffd5adc83a951e3504ccf12d502f75e9a7b7 Mon Sep 17 00:00:00 2001 From: bemic <50417294+bemic@users.noreply.github.com> Date: Wed, 22 Jul 2026 23:44:31 +0200 Subject: [PATCH 3/5] ethereum: restore upgrade helper without env execution --- ethereum/sh/upgrade.sh | 58 ++++++++++++++---------------- ethereum/sh/upgrade_all_testnet.sh | 18 ++++++---- ethereum/verify | 23 ++++++++---- 3 files changed, 54 insertions(+), 45 deletions(-) diff --git a/ethereum/sh/upgrade.sh b/ethereum/sh/upgrade.sh index 947b166181d..feb0ed075ea 100755 --- a/ethereum/sh/upgrade.sh +++ b/ethereum/sh/upgrade.sh @@ -1,9 +1,9 @@ #!/bin/bash -# For testnet: +# For testnet: #MNEMONIC= GUARDIAN_MNEMONIC= ./sh/upgrade.sh testnet Core blast -# For mainnet: +# For mainnet: #MNEMONIC= ./sh/upgrade.sh mainnet Core blast set -euo pipefail @@ -17,6 +17,14 @@ if [ -z "$network" ] || [ -z "$module" ] || [ -z "$chain" ]; then exit 1 fi +case "$network" in + mainnet|testnet) ;; + *) + echo "unknown network $network, must be testnet or mainnet" >&2 + exit 1 + ;; +esac + if [ -z "${MNEMONIC:-}" ]; then echo "MNEMONIC unset" exit 1 @@ -57,33 +65,13 @@ case "$module" in ;; esac -ENV_FILE="env/.env.${chain}" -if [ "$network" = testnet ]; then - ENV_FILE="${ENV_FILE}.testnet" -else - ENV_FILE="${ENV_FILE}.mainnet" -fi - -if ! [ -f ./$ENV_FILE ]; then - echo "Environment file \"${ENV_FILE}\" does not exist." - exit 1 +if [ -z "${RPC_URL:-}" ]; then + RPC_URL=$(worm info rpc "$network" "$chain") fi -. ./$ENV_FILE - -exit 1 - -[[ -z $INIT_EVM_CHAIN_ID ]] && { echo "Missing INIT_EVM_CHAIN_ID"; exit 1; } -[[ -z $MNEMONIC ]] && { echo "Missing MNEMONIC"; exit 1; } - -if [ -z ${RPC_URL+x} ]; then - ret=0 - RPC_URL=$(worm info rpc "$network" "$chain" 2>/dev/null) || ret=$? - if [ $ret != 0 ]; then - echo "Missing RPC_URL and \"worm info rpc\" does not support this chain." - exit 1 - fi -fi +# Use the RPC's chain ID for the Foundry broadcast directory. +evm_chain_id=$(cast chain-id --rpc-url "$RPC_URL") +[[ "$evm_chain_id" =~ ^[0-9]+$ ]] || exit 1 if [ -z ${FORGE_ARGS+x} ]; then FORGE_ARGS="" @@ -98,25 +86,29 @@ if [ $ret != 0 ]; then fi ret=0 -(./verify -n "$network" -c "$chain" $FILE "$implementation" > /dev/null) || ret=$? +(./verify -n "$network" -c "$chain" "$FILE" "$implementation" > /dev/null) || ret=$? +# Only status 1 means bytecode mismatch; operational errors must not deploy. if [ $ret = 0 ]; then printf "✔ %s %s: skipping (implementation matches same bytecode)\n" "$chain" "$module" exit +elif [ $ret != 1 ]; then + exit $ret fi forge script ./forge-scripts/${SCRIPT} \ --rpc-url "$RPC_URL" \ - --private-key "$MNEMONIC" \ + --private-key "$secret" \ --broadcast ${FORGE_ARGS} -returnInfo=$(cat ./broadcast/${SOLFILE}/$INIT_EVM_CHAIN_ID/run-latest.json) +returnInfo=$(cat "./broadcast/${SOLFILE}/${evm_chain_id}/run-latest.json") # Extract the address values from 'returnInfo' new_implementation=$(jq -r '.returns.deployedAddress.value' <<< "$returnInfo") ret=0 -(./verify -n "$network" -c "$chain" $FILE "$new_implementation" > /dev/null) || ret=$? +(./verify -n "$network" -c "$chain" "$FILE" "$new_implementation" > /dev/null) || ret=$? +# Governance is allowed only after the deployed bytecode matches. if [ $ret = 0 ]; then printf "✔ %s %s: deployed (%s)\n" "$chain" "$module" "$new_implementation" else @@ -125,7 +117,9 @@ else fi if [ "$network" = testnet ]; then - worm submit $(worm generate upgrade -c "$chain" -a "$new_implementation" -m "$module" -g "$guardian_secret") -n "$network" + # Keep generated governance data in one submit argument. + upgrade_vaa=$(worm generate upgrade -c "$chain" -a "$new_implementation" -m "$module" -g "$guardian_secret") + worm submit "$upgrade_vaa" -n "$network" else echo "../scripts/contract-upgrade-governance.sh -c $chain -m $verify_module -a $new_implementation" fi diff --git a/ethereum/sh/upgrade_all_testnet.sh b/ethereum/sh/upgrade_all_testnet.sh index 334fcde5986..b14120f6131 100755 --- a/ethereum/sh/upgrade_all_testnet.sh +++ b/ethereum/sh/upgrade_all_testnet.sh @@ -1,23 +1,27 @@ #!/bin/bash -# Upgrade Core, TokenBridge and NFTBridge contracts on all chains: +# Upgrade Core and TokenBridge contracts on all chains: #MNEMONIC= GUARDIAN_MNEMONIC= ./sh/upgrade_all_testnet.sh -# Upgrade TokenBridge on a few chains chains: +# Upgrade TokenBridge on a few chains: #MNEMONIC= GUARDIAN_MNEMONIC= CHAINS="avalanche polygon oasis" MODULES=TokenBridge ./sh/upgrade_all_testnet.sh -# Upgrade Core and TokenBridge contracts on all chains: -#MNEMONIC= GUARDIAN_MNEMONIC= MODULES="Core TokenBridge" ./sh/upgrade_all_testnet.sh +# Chains that require nondefault Foundry options should be grouped by the same +# FORGE_ARGS value using the CHAINS selector above. if [ "${CHAINS}X" == "X" ]; then - CHAINS=$(worm evm chains) + CHAINS=$(worm evm chains) || exit 1 + # Empty enumeration must not look like a successful bulk run. + [ -n "$CHAINS" ] || exit 1 fi if [ "${MODULES}X" == "X" ]; then - MODULES=(Core TokenBridge NFTBridge) + MODULES=(Core TokenBridge) fi -set -uo pipefail +# Stop on the first failed child and resolve an RPC separately per chain. +set -euo pipefail +unset RPC_URL network=testnet for module in ${MODULES[@]}; do diff --git a/ethereum/verify b/ethereum/verify index ed2d6122722..95c22918c01 100755 --- a/ethereum/verify +++ b/ethereum/verify @@ -14,7 +14,7 @@ Usage: The -n and -r flags are mutually exclusive. EOF -exit 1 +exit 2 } chain="" @@ -56,6 +56,11 @@ fi set -euo pipefail +# Exit 1 is reserved for a verified bytecode mismatch. Any tooling or RPC +# failure must use another status so callers do not mistake it for permission +# to deploy a replacement implementation. +trap 'exit 2' ERR + # network and rpc flags are mutually exlusive [[ -n $network && -n $rpc ]] && usage @@ -65,7 +70,7 @@ if [[ -n $network ]]; then if ! command -v worm &> /dev/null then echo "worm binary could not be found. See installation instructions in clients/js/README.md" - exit 1 + exit 2 fi rpc=$(worm info rpc "$network" "$chain") fi @@ -79,22 +84,28 @@ fi deployed=$(mktemp) local=$(mktemp) +# Reject missing or malformed local bytecode as an operational error. if [[ -z $use_forge ]]; then - cat "$json_file" | jq -r .deployedBytecode > "$local" + jq -er '.deployedBytecode | select(type == "string" and startswith("0x"))' \ + "$json_file" > "$local" else - cat "$json_file" | jq -r .deployedBytecode | jq -r .object > "$local" + jq -er '.deployedBytecode.object | select(type == "string" and startswith("0x"))' \ + "$json_file" > "$local" fi ret=0 # Grab bytecode from the JSON RPC using the eth_getCode method. +# A null or malformed result is not a bytecode mismatch. curl "$rpc" \ -X POST \ -H "Content-Type: application/json" \ - --data "{\"method\":\"eth_getCode\",\"params\":[\"$contract_addr\",\"latest\"],\"id\":1,\"jsonrpc\":\"2.0\"}" --silent | jq -r .result > "$deployed" || ret=$? + --data "{\"method\":\"eth_getCode\",\"params\":[\"$contract_addr\",\"latest\"],\"id\":1,\"jsonrpc\":\"2.0\"}" --silent \ + | jq -er '.result | select(type == "string" and startswith("0x"))' \ + > "$deployed" || ret=$? if [ $ret -gt 0 ]; then printf "\033[0;31mFailed to query eth RPC '%s' while verifying %s on %s\033[0m\n" "$rpc" "$contract_addr" "$chain" - exit 1 + exit 2 fi # hash, then see if they match up From 10c10127ae711d92ec3962b26d83f37fd5586a69 Mon Sep 17 00:00:00 2001 From: bemic <50417294+bemic@users.noreply.github.com> Date: Wed, 22 Jul 2026 23:47:39 +0200 Subject: [PATCH 4/5] ethereum: document release-helper environment handling --- ethereum/README.md | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/ethereum/README.md b/ethereum/README.md index 8df7f09863d..2272928de92 100644 --- a/ethereum/README.md +++ b/ethereum/README.md @@ -15,17 +15,16 @@ To build the contracts: ### Deploying using Forge -#### Create the ENV file +#### Provide the deployment environment -Before you can deploy the contracts, you need to create a file in `ethereum/env` with a name like `.env.blast` for mainnet -or `.env.blast.testnet` for testnet. Substitute the appropriate chain name (as it will be in the worm client) and use the -mentioned one as an example. +Release helpers do not source repository `.env` files. Export the required values +from a trusted operator environment before running them. Files under `ethereum/env` +may be used as reviewed references, but must not be sourced. -#### Create a symbolic link - -```shell -ethereum$ ln -s env/.env.blast.testnet .env -``` +`upgrade.sh` resolves an RPC with `worm info rpc` when `RPC_URL` is absent; +register-all requires an explicit `RPC_URL`. Bulk upgrades clear inherited RPC +overrides, so use `upgrade.sh` directly for a custom endpoint. `FORGE_ARGS` remains +a whitespace-separated list of trusted operator-provided Foundry arguments. #### Deploy the Core contract @@ -70,20 +69,24 @@ This will put the flattened files in `ethereum/flattened`. #### Upgrade the Core or TokenBridge Implementation ```shell -ethereum$ MNEMONIC= ./sh/upgrade.sh testnet Core blast -ethereum$ MNEMONIC= ./sh/upgrade.sh testnet TokenBridge blast +ethereum$ MNEMONIC= GUARDIAN_MNEMONIC= ./sh/upgrade.sh testnet Core blast +ethereum$ MNEMONIC= GUARDIAN_MNEMONIC= ./sh/upgrade.sh testnet TokenBridge blast +ethereum$ MNEMONIC= ./sh/upgrade.sh mainnet Core blast ``` #### Registering Other Chains on a New TokenBridge ```shell -ethereum$ MNEMONIC= ./sh/registerAllChainsOnTokenBridge.sh +ethereum$ MNEMONIC= RPC_URL= ./sh/registerAllChainsOnTokenBridge.sh ``` ### Testing Run all ethereum tests using `make test` +Run the release-helper compatibility and security tests without building the +contracts using `make test-release-helpers`. + ### User methods `submitVAA(bytes vaa)` can be used to execute a VAA. From a1500c5200c6e0193acc1ff4f61f146d529be768 Mon Sep 17 00:00:00 2001 From: bemic <50417294+bemic@users.noreply.github.com> Date: Thu, 23 Jul 2026 00:33:23 +0200 Subject: [PATCH 5/5] ethereum: add release-helper security regression tests --- ethereum/Makefile | 6 +- ethereum/sh/tests/release_helpers_test.sh | 919 ++++++++++++++++++++++ 2 files changed, 924 insertions(+), 1 deletion(-) create mode 100755 ethereum/sh/tests/release_helpers_test.sh diff --git a/ethereum/Makefile b/ethereum/Makefile index f20164957de..2e3a4cbdab8 100644 --- a/ethereum/Makefile +++ b/ethereum/Makefile @@ -59,7 +59,11 @@ flattened: $(patsubst contracts/%, flattened/%, $(FLATTEN_FILES)) .env: .env.test cp $< $@ -test: test-forge test-identifiers +test: test-forge test-identifiers test-release-helpers + +.PHONY: test-release-helpers +test-release-helpers: + ./sh/tests/release_helpers_test.sh .PHONY: test-upgrade test-upgrade: build .env node_modules diff --git a/ethereum/sh/tests/release_helpers_test.sh b/ethereum/sh/tests/release_helpers_test.sh new file mode 100755 index 00000000000..a8aba4d518b --- /dev/null +++ b/ethereum/sh/tests/release_helpers_test.sh @@ -0,0 +1,919 @@ +#!/bin/bash + +set -uo pipefail + +script_dir=$(CDPATH= cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P) +sh_dir=$(CDPATH= cd "$script_dir/.." && pwd -P) +repo_root=$(CDPATH= cd "$sh_dir/../.." && pwd -P) + +tests=0 +failures=0 +tmp_root=$(mktemp -d "${TMPDIR:-/tmp}/wormhole-release-helpers.XXXXXX") +trap 'rm -rf -- "$tmp_root"' EXIT + +pass() { + tests=$((tests + 1)) + printf 'ok %d - %s\n' "$tests" "$1" +} + +fail() { + tests=$((tests + 1)) + failures=$((failures + 1)) + printf 'not ok %d - %s\n' "$tests" "$1" >&2 +} + +assert_status() { + local expected="$1" actual="$2" description="$3" + if [ "$actual" -eq "$expected" ]; then + pass "$description" + else + fail "$description (expected status $expected, got $actual)" + fi +} + +assert_absent() { + local path="$1" description="$2" + if [ ! -e "$path" ]; then + pass "$description" + else + fail "$description" + fi +} + +assert_contains() { + local path="$1" expected="$2" description="$3" + if grep -Fq -- "$expected" "$path"; then + pass "$description" + else + fail "$description" + fi +} + +assert_not_contains() { + local path="$1" unexpected="$2" description="$3" + if grep -Fq -- "$unexpected" "$path"; then + fail "$description" + else + pass "$description" + fi +} + +count_nul_arg() { + local path="$1" expected="$2" argument count=0 + while IFS= read -r -d '' argument; do + if [ "$argument" = "$expected" ]; then + count=$((count + 1)) + fi + done < "$path" + printf '%s\n' "$count" +} + +assert_nul_args() { + local path="$1" description="$2" argument index=0 + shift 2 + local actual=() + while IFS= read -r -d '' argument; do + actual+=("$argument") + done < "$path" + + if [ "${#actual[@]}" -ne "$#" ]; then + fail "$description (expected $# arguments, got ${#actual[@]})" + return + fi + + for argument in "$@"; do + if [ "${actual[$index]}" != "$argument" ]; then + fail "$description (argument $index differs)" + return + fi + index=$((index + 1)) + done + pass "$description" +} + +tree="$tmp_root/tree" +eth="$tree/ethereum" +bin="$eth/test-bin" +verify_bin="$eth/verify-test-bin" +mkdir -p "$eth/sh" "$eth/env" "$eth/build-forge/Implementation.sol" \ + "$eth/build-forge/BridgeImplementation.sol" "$bin" "$verify_bin" \ + "$tree/deployments/testnet" "$tree/deployments/mainnet" +: > "$eth/build-forge/Implementation.sol/Implementation.json" +: > "$eth/build-forge/BridgeImplementation.sol/BridgeImplementation.json" + +scripts=( + deployCoreBridge.sh + deployCoreBridgeTron.sh + deployCoreShutdown.sh + deployCustomConsistencyLevel.sh + deployDelegatedManagerSet.sh + deployDelegatedGuardians.sh + deployDummyContract.sh + deployNFTBridge.sh + deployTokenBridge.sh + deployTokenBridgeShutdown.sh + devnetInitialization.sh + registerAllChainsOnTokenBridge.sh + registerChainsNFTBridge.sh + registerChainsTokenBridge.sh + upgrade.sh + upgrade_all_testnet.sh +) +for script in "${scripts[@]}"; do + ln -s "$sh_dir/$script" "$eth/sh/$script" +done + +# Stubs record argv and emulate only the external behavior used by the helpers. +cat > "$bin/worm" <<'WORM' +#!/bin/bash +printf 'worm' >> "$TOOL_LOG" +printf ' <%s>' "$@" >> "$TOOL_LOG" +printf '\n' >> "$TOOL_LOG" + +if [ "$1" = info ] && [ "$2" = rpc ]; then + [ "${WORM_RPC_FAIL:-0}" = 1 ] && exit 1 + printf 'https://%s.rpc.invalid\n' "$4" +elif [ "$1" = info ] && [ "$2" = chain-id ]; then + case "$3" in + base) printf '30\n' ;; + demo) printf '999\n' ;; + blast) printf '36\n' ;; + ethereum) printf '2\n' ;; + *) printf '1000\n' ;; + esac +elif [ "$1" = evm ] && [ "$2" = info ]; then + [ "${WORM_NO_DEPLOYMENT:-0}" = 1 ] && exit 1 + if [ "${WORM_INVALID_IMPLEMENTATION:-0}" = 1 ]; then + printf 'not-an-address\n' + exit 0 + fi + printf '0x1111111111111111111111111111111111111111\n' +elif [ "$1" = evm ] && [ "$2" = chains ]; then + [ "${WORM_CHAINS_FAIL:-0}" = 1 ] && exit 1 + [ "${WORM_CHAINS_EMPTY:-0}" = 1 ] && exit 0 + printf 'alpha\nbeta\n' +elif [ "$1" = generate ] && [ "$2" = upgrade ]; then + [ "${WORM_GENERATE_FAIL:-0}" = 1 ] && exit 1 + [ "${WORM_GENERATE_EMPTY:-0}" = 1 ] && exit 0 + printf 'test-upgrade-vaa\n' +elif [ "$1" = submit ]; then + [ "${WORM_SUBMIT_FAIL:-0}" = 1 ] && exit 1 + [ -z "${2:-}" ] && exit 1 + exit 0 +else + exit 2 +fi +WORM + +cat > "$bin/cast" <<'CAST' +#!/bin/bash +printf 'cast' >> "$TOOL_LOG" +printf ' <%s>' "$@" >> "$TOOL_LOG" +printf '\n' >> "$TOOL_LOG" + +[ "${CAST_FAIL:-0}" = 1 ] && exit 1 +if [ "${CAST_INVALID:-0}" = 1 ]; then + printf 'not-a-chain-id\n' + exit 0 +fi +rpc_url="" +while [ "$#" -gt 0 ]; do + if [ "$1" = --rpc-url ]; then + rpc_url="$2" + break + fi + shift +done +case "$rpc_url" in + *alpha*) printf '111\n' ;; + *beta*) printf '222\n' ;; + *reviewed*) printf '333\n' ;; + *) printf '444\n' ;; +esac +CAST + +cat > "$bin/forge" <<'FORGE' +#!/bin/bash +printf '%s\0' "$@" >> "$FORGE_LOG" +[ "${FORGE_FAIL:-0}" = 1 ] && exit 1 + +target="$2" +solfile="${target#./forge-scripts/}" +solfile="${solfile%%:*}" +rpc_url="" +arguments=("$@") +index=0 +while [ "$index" -lt "${#arguments[@]}" ]; do + if [ "${arguments[index]}" = --rpc-url ]; then + index=$((index + 1)) + rpc_url="${arguments[index]}" + break + fi + index=$((index + 1)) +done +[ -n "${FORGE_FAIL_RPC_CONTAINS:-}" ] \ + && [[ "$rpc_url" == *"$FORGE_FAIL_RPC_CONTAINS"* ]] \ + && exit 1 +case "$rpc_url" in + *alpha*) chain_id=111 ;; + *beta*) chain_id=222 ;; + *reviewed*) chain_id=333 ;; + *demo*) chain_id=444 ;; + *) chain_id="${INIT_EVM_CHAIN_ID:-${EVM_CHAIN_ID:-1337}}" ;; +esac +mkdir -p "broadcast/$solfile/$chain_id" +cat > "broadcast/$solfile/$chain_id/run-latest.json" <<'JSON' +{"returns":{"deployedAddress":{"value":"0x2222222222222222222222222222222222222222"},"setupAddress":{"value":"0x2222222222222222222222222222222222222222"},"implAddress":{"value":"0x2222222222222222222222222222222222222222"},"tokenImplementationAddress":{"value":"0x2222222222222222222222222222222222222222"},"bridgeSetupAddress":{"value":"0x2222222222222222222222222222222222222222"},"bridgeImplementationAddress":{"value":"0x2222222222222222222222222222222222222222"},"nftImplementationAddress":{"value":"0x2222222222222222222222222222222222222222"},"implementationAddress":{"value":"0x2222222222222222222222222222222222222222"},"deployedDelegatedGuardians":{"value":"0x2222222222222222222222222222222222222222"}}} +JSON +FORGE + +cat > "$bin/jq" <<'JQ' +#!/bin/bash +input_file="" +for argument in "$@"; do + [ -f "$argument" ] && input_file=$argument +done +if [ -n "$input_file" ]; then + cat "$input_file" >/dev/null +else + cat >/dev/null +fi +[ "${JQ_EMPTY:-0}" = 1 ] && exit 0 +if [ "${JQ_NULL:-0}" = 1 ]; then + printf 'null\n' + exit 0 +fi +if [ "${JQ_INVALID:-0}" = 1 ]; then + printf 'not-an-address\n' + exit 0 +fi +printf '0x2222222222222222222222222222222222222222\n' +JQ + +cat > "$bin/npm" <<'NPM' +#!/bin/bash +printf 'npm' >> "$TOOL_LOG" +printf ' <%s>' "$@" >> "$TOOL_LOG" +printf '\n' >> "$TOOL_LOG" +NPM + +cat > "$bin/node" <<'NODE' +#!/bin/bash +printf 'node' >> "$TOOL_LOG" +printf ' <%s>' "$@" >> "$TOOL_LOG" +printf '\n' >> "$TOOL_LOG" +NODE + +cat > "$verify_bin/curl" <<'CURL' +#!/bin/bash +case "${VERIFY_CURL_MODE:-fail}" in + match) printf '{"result":"0x00"}\n' ;; + mismatch) printf '{"result":"0x01"}\n' ;; + null) printf '{"result":null}\n' ;; + *) exit 7 ;; +esac +CURL + +cat > "$eth/verify" <<'VERIFY' +#!/bin/bash +printf 'verify' >> "$TOOL_LOG" +printf ' <%s>' "$@" >> "$TOOL_LOG" +printf '\n' >> "$TOOL_LOG" + +last="" +for argument in "$@"; do + last="$argument" +done +if [ "${VERIFY_MODE:-}" = skip ]; then + exit 0 +fi +if [ "${VERIFY_MODE:-}" = operational-fail ]; then + exit 2 +fi +if [[ ! "$last" =~ ^0x[0-9A-Fa-f]{40}$ ]]; then + exit 2 +fi +if [ "$last" = 0x2222222222222222222222222222222222222222 ]; then + [ "${VERIFY_MODE:-}" = post-fail ] && exit 1 + exit 0 +fi +exit 1 +VERIFY + +chmod +x "$bin/worm" "$bin/cast" "$bin/forge" "$bin/jq" "$bin/npm" \ + "$bin/node" "$verify_bin/curl" "$eth/verify" + +cat > "$tree/deployments/testnet/tokenBridgeVAAs.csv" <<'CSV' +# chain,vaa +Demo (999) Testnet Token Bridge,0011 +Other (1000) Testnet Token Bridge,aabb +CSV +cp "$tree/deployments/testnet/tokenBridgeVAAs.csv" \ + "$tree/deployments/mainnet/tokenBridgeVAAs.csv" + +# Any accidental env sourcing creates this harmless marker. +marker="$eth/repository-env-executed" +cat > "$eth/.env" </dev/null; then + fail 'EVM helpers contain no direct env-file execution' +else + pass 'EVM helpers contain no direct env-file execution' +fi + +syntax_status=0 +for script in "${scripts[@]}"; do + /bin/bash -n "$sh_dir/$script" || syntax_status=1 +done +/bin/bash -n "$repo_root/ethereum/verify" || syntax_status=1 +assert_status 0 "$syntax_status" 'all covered helpers pass Bash syntax checking' + +node_syntax_status=0 +node --check "$sh_dir/deployCoreBridgeTron.js" >/dev/null 2>&1 || node_syntax_status=1 +assert_status 0 "$node_syntax_status" 'Tron deployment JavaScript passes syntax checking' + +# Exercise the real verifier's match, mismatch, and operational-error contract. +verify_artifact="$eth/build-forge/Implementation.sol/verify-test.json" +printf '{"deployedBytecode":{"object":"0x00"}}\n' > "$verify_artifact" +( + PATH="$verify_bin:$PATH" "$repo_root/ethereum/verify" \ + -r http://local.invalid -c demo \ + "$verify_artifact" 0x1111111111111111111111111111111111111111 +) >/dev/null 2>&1 +status=$? +assert_status 2 "$status" 'verify distinguishes an RPC failure from a bytecode mismatch' + +for verify_mode in match mismatch null; do + ( + PATH="$verify_bin:$PATH" VERIFY_CURL_MODE=$verify_mode \ + "$repo_root/ethereum/verify" \ + -r http://local.invalid -c demo \ + "$verify_artifact" 0x1111111111111111111111111111111111111111 + ) >/dev/null 2>&1 + status=$? + case "$verify_mode" in + match) assert_status 0 "$status" 'verify returns success for matching bytecode' ;; + mismatch) assert_status 1 "$status" 'verify reserves status 1 for bytecode mismatch' ;; + null) assert_status 2 "$status" 'verify treats a null RPC result as an operational failure' ;; + esac +done + +( + PATH="$verify_bin:$PATH" VERIFY_CURL_MODE=mismatch \ + "$repo_root/ethereum/verify" \ + -r http://local.invalid -c demo \ + "$eth/build-forge/Implementation.sol/missing.json" \ + 0x1111111111111111111111111111111111111111 +) >/dev/null 2>&1 +status=$? +assert_status 2 "$status" 'verify treats a missing build artifact as an operational failure' + +# Direct registration keeps the original Forge argv while ignoring .env. +: > "$forge_log" +: > "$tool_log" +( + cd "$eth" || exit 1 + PATH="$path" TOOL_LOG="$tool_log" FORGE_LOG="$forge_log" \ + MNEMONIC=test-only-secret \ + RPC_URL='https://reviewed.invalid/path?a=1&b=2' \ + TOKEN_BRIDGE_ADDRESS=0x1111111111111111111111111111111111111111 \ + TOKEN_BRIDGE_REGISTRATION_VAAS='[0x0011,0xaabb]' \ + ./sh/registerChainsTokenBridge.sh +) >/dev/null 2>&1 +status=$? +assert_status 0 "$status" 'token registration accepts operator environment' +[ "$(count_nul_arg "$forge_log" 'https://reviewed.invalid/path?a=1&b=2')" -eq 1 ] \ + && pass 'registration RPC URL remains one argv element' \ + || fail 'registration RPC URL remains one argv element' +[ "$(count_nul_arg "$forge_log" '[0x0011,0xaabb]')" -eq 1 ] \ + && pass 'registration bytes array remains one argv element' \ + || fail 'registration bytes array remains one argv element' +assert_nul_args "$forge_log" 'direct registration passes the exact Forge argv vector' \ + script \ + ./forge-scripts/RegisterChainsTokenBridge.s.sol:RegisterChainsTokenBridge \ + --sig 'run(address,bytes[])' \ + 0x1111111111111111111111111111111111111111 \ + '[0x0011,0xaabb]' \ + --rpc-url 'https://reviewed.invalid/path?a=1&b=2' \ + --private-key test-only-secret \ + --broadcast +assert_absent "$marker" 'direct registration does not execute local .env' + +: > "$forge_log" +( + cd "$eth" || exit 1 + PATH="$path" TOOL_LOG="$tool_log" FORGE_LOG="$forge_log" \ + MNEMONIC=test-only-secret ./sh/registerChainsTokenBridge.sh +) >/dev/null 2>&1 +status=$? +assert_status 1 "$status" 'missing operator configuration fails closed' +if [ ! -s "$forge_log" ]; then + pass 'missing operator configuration prevents Forge invocation' +else + fail 'missing operator configuration prevents Forge invocation' +fi + +# Register-all preserves CSV aggregation and optional Forge arguments. +: > "$forge_log" +( + cd "$eth" || exit 1 + PATH="$path" TOOL_LOG="$tool_log" FORGE_LOG="$forge_log" \ + MNEMONIC=test-only-secret RPC_URL=https://reviewed.invalid FORGE_ARGS=--slow \ + ./sh/registerAllChainsOnTokenBridge.sh testnet demo \ + 0x1111111111111111111111111111111111111111 +) >/dev/null 2>&1 +status=$? +assert_status 0 "$status" 'register-all accepts operator environment' +[ "$(count_nul_arg "$forge_log" '[0xaabb]')" -eq 1 ] \ + && pass 'register-all preserves VAA aggregation' \ + || fail 'register-all preserves VAA aggregation' +[ "$(count_nul_arg "$forge_log" '--slow')" -eq 1 ] \ + && pass 'register-all preserves FORGE_ARGS expansion' \ + || fail 'register-all preserves FORGE_ARGS expansion' +assert_nul_args "$forge_log" 'register-all passes the exact Forge argv vector' \ + script \ + ./forge-scripts/RegisterChainsTokenBridge.s.sol:RegisterChainsTokenBridge \ + --sig 'run(address,bytes[])' \ + 0x1111111111111111111111111111111111111111 \ + '[0xaabb]' \ + --rpc-url https://reviewed.invalid \ + --private-key test-only-secret \ + --broadcast --slow +assert_absent "$marker" 'register-all does not execute selected repo env' + +# Repository CSV text must remain inside the single ABI argument. +printf 'Demo (999) Testnet Token Bridge,0011\nOther (1000) Testnet Token Bridge,aabb --slow --rpc-url https://evil.invalid\n' \ + > "$tree/deployments/testnet/tokenBridgeVAAs.csv" +: > "$forge_log" +( + cd "$eth" || exit 1 + PATH="$path" TOOL_LOG="$tool_log" FORGE_LOG="$forge_log" \ + MNEMONIC=test-only-secret RPC_URL=https://reviewed.invalid \ + ./sh/registerAllChainsOnTokenBridge.sh testnet demo \ + 0x1111111111111111111111111111111111111111 +) >/dev/null 2>&1 +status=$? +assert_status 0 "$status" 'CSV option-like text remains data' +[ "$(count_nul_arg "$forge_log" '[0xaabb --slow --rpc-url https://evil.invalid]')" -eq 1 ] \ + && pass 'CSV option-like text remains one Forge argument' \ + || fail 'CSV option-like text remains one Forge argument' +[ "$(count_nul_arg "$forge_log" 'https://evil.invalid')" -eq 0 ] \ + && pass 'CSV text cannot replace the reviewed RPC argument' \ + || fail 'CSV text cannot replace the reviewed RPC argument' + +# Numeric IDs distinguish overlapping names such as Base and Base Sepolia. +printf 'Base (30) Testnet Token Bridge,aaaa\nBase Sepolia (10004) Testnet Token Bridge,bbbb\nOther (1000) Testnet Token Bridge,cccc\n' \ + > "$tree/deployments/testnet/tokenBridgeVAAs.csv" +: > "$forge_log" +( + cd "$eth" || exit 1 + PATH="$path" TOOL_LOG="$tool_log" FORGE_LOG="$forge_log" \ + MNEMONIC=test-only-secret RPC_URL=https://reviewed.invalid \ + ./sh/registerAllChainsOnTokenBridge.sh testnet base \ + 0x1111111111111111111111111111111111111111 +) >/dev/null 2>&1 +status=$? +assert_status 0 "$status" 'register-all matches the exact target chain ID' +[ "$(count_nul_arg "$forge_log" '[0xbbbb,0xcccc]')" -eq 1 ] \ + && pass 'register-all keeps Base Sepolia when targeting Base' \ + || fail 'register-all keeps Base Sepolia when targeting Base' + +# Missing or duplicate target IDs must fail before Forge. +target_cases=(missing duplicate) +target_rows=( + 'Other (1000) Testnet Token Bridge,cccc' + $'Base (30) Testnet Token Bridge,aaaa\nBase duplicate (30) Testnet Token Bridge,dddd' +) +for index in "${!target_cases[@]}"; do + printf '%s\n' "${target_rows[index]}" > "$tree/deployments/testnet/tokenBridgeVAAs.csv" + : > "$forge_log" + ( + cd "$eth" || exit 1 + PATH="$path" TOOL_LOG="$tool_log" FORGE_LOG="$forge_log" \ + MNEMONIC=test-only-secret RPC_URL=https://reviewed.invalid \ + ./sh/registerAllChainsOnTokenBridge.sh testnet base \ + 0x1111111111111111111111111111111111111111 + ) >/dev/null 2>&1 + status=$? + assert_status 1 "$status" \ + "register-all rejects a ${target_cases[index]} target chain ID" + if [ ! -s "$forge_log" ]; then + pass "${target_cases[index]} target chain data prevents Forge invocation" + else + fail "${target_cases[index]} target chain data prevents Forge invocation" + fi +done + +# Restore the normal fixture and verify explicit RPC configuration. +printf '# chain,vaa\nDemo (999) Testnet Token Bridge,0011\nOther (1000) Testnet Token Bridge,aabb\n' \ + > "$tree/deployments/testnet/tokenBridgeVAAs.csv" +: > "$forge_log" +: > "$tool_log" +( + cd "$eth" || exit 1 + PATH="$path" TOOL_LOG="$tool_log" FORGE_LOG="$forge_log" \ + MNEMONIC=test-only-secret \ + ./sh/registerAllChainsOnTokenBridge.sh testnet demo \ + 0x1111111111111111111111111111111111111111 +) >/dev/null 2>&1 +status=$? +assert_status 1 "$status" 'register-all requires an explicit RPC URL' +if [ ! -s "$forge_log" ]; then + pass 'missing register-all RPC prevents Forge invocation' +else + fail 'missing register-all RPC prevents Forge invocation' +fi + +# Verify compatibility with the repository's real CSV files. +cp "$repo_root/deployments/testnet/tokenBridgeVAAs.csv" \ + "$tree/deployments/testnet/tokenBridgeVAAs.csv" +: > "$forge_log" +( + cd "$eth" || exit 1 + PATH="$path" TOOL_LOG="$tool_log" FORGE_LOG="$forge_log" \ + MNEMONIC=test-only-secret RPC_URL=https://reviewed.invalid \ + ./sh/registerAllChainsOnTokenBridge.sh testnet blast \ + 0x1111111111111111111111111111111111111111 +) >/dev/null 2>&1 +status=$? +assert_status 0 "$status" 'register-all accepts the repository testnet CSV format' + +cp "$repo_root/deployments/mainnet/tokenBridgeVAAs.csv" \ + "$tree/deployments/mainnet/tokenBridgeVAAs.csv" +: > "$forge_log" +( + cd "$eth" || exit 1 + PATH="$path" TOOL_LOG="$tool_log" FORGE_LOG="$forge_log" \ + MNEMONIC=test-only-secret RPC_URL=https://reviewed.invalid \ + ./sh/registerAllChainsOnTokenBridge.sh mainnet ethereum \ + 0x1111111111111111111111111111111111111111 +) >/dev/null 2>&1 +status=$? +assert_status 0 "$status" 'register-all accepts the repository mainnet CSV format' + +# Standalone deployment helpers continue to consume exported values. +common_deploy_env=( + PATH="$path" + TOOL_LOG="$tool_log" + FORGE_LOG="$forge_log" + INIT_EVM_CHAIN_ID=1337 + MNEMONIC=test-only-secret + RPC_URL=https://reviewed.invalid + FORGE_ARGS= +) +for helper in deployCoreShutdown.sh deployTokenBridgeShutdown.sh; do + ( + cd "$eth" || exit 1 + env "${common_deploy_env[@]}" "./sh/$helper" + ) >/dev/null 2>&1 + status=$? + assert_status 0 "$status" "$helper accepts operator environment" +done + +( + cd "$eth" || exit 1 + PATH="$path" TOOL_LOG="$tool_log" FORGE_LOG="$forge_log" \ + TRON_PRIVATE_KEY=test-only-secret TRON_FULL_HOST=https://reviewed.invalid \ + INIT_SIGNERS='[0x1111111111111111111111111111111111111111]' \ + INIT_CHAIN_ID=2 INIT_GOV_CHAIN_ID=1 INIT_GOV_CONTRACT=0x00 \ + INIT_EVM_CHAIN_ID=1337 ./sh/deployCoreBridgeTron.sh +) >/dev/null 2>&1 +status=$? +assert_status 0 "$status" 'Tron deployment accepts operator environment' +assert_contains "$tool_log" 'node ' \ + 'Tron deployment reaches its unchanged Node command' + +# The complete devnet workflow reads registration VAAs as data but never runs +# the shell command placed in .env. +( + cd "$eth" || exit 1 + PATH="$path" TOOL_LOG="$tool_log" FORGE_LOG="$forge_log" \ + DEV=True CHAIN_ID=2 EVM_CHAIN_ID=1337 FORGE_ARGS= \ + INIT_SIGNERS='[0x1111111111111111111111111111111111111111]' \ + INIT_CHAIN_ID=2 INIT_GOV_CHAIN_ID=1 INIT_GOV_CONTRACT=0x00 \ + INIT_EVM_CHAIN_ID=1337 BRIDGE_INIT_CHAIN_ID=2 \ + BRIDGE_INIT_GOV_CHAIN_ID=1 BRIDGE_INIT_GOV_CONTRACT=0x00 \ + BRIDGE_INIT_WETH=0x1111111111111111111111111111111111111111 \ + BRIDGE_INIT_FINALITY=1 ./sh/devnetInitialization.sh +) >/dev/null 2>&1 +status=$? +assert_status 0 "$status" 'devnet workflow remains functional with operator environment' +assert_absent "$marker" 'devnet treats .env as registration data, not shell code' + +# Upgrade: testnet deploy/verify/submit, mainnet output, skip, bulk, and failure. +: > "$forge_log" +: > "$tool_log" +( + cd "$eth" || exit 1 + PATH="$path" TOOL_LOG="$tool_log" FORGE_LOG="$forge_log" \ + MNEMONIC=test-signing-key GUARDIAN_MNEMONIC=test-guardian-key \ + FORGE_ARGS='--slow --gas-estimate-multiplier 130' \ + ./sh/upgrade.sh testnet Core demo +) > "$eth/upgrade-testnet.out" 2>&1 +status=$? +assert_status 0 "$status" 'testnet Core upgrade completes' +assert_contains "$tool_log" 'worm <-n> ' \ + 'testnet upgrade submits generated governance VAA' +[ "$(count_nul_arg "$forge_log" '--gas-estimate-multiplier')" -eq 1 ] \ + && pass 'upgrade preserves operator FORGE_ARGS' \ + || fail 'upgrade preserves operator FORGE_ARGS' +assert_nul_args "$forge_log" 'upgrade passes the exact Forge argv vector' \ + script \ + ./forge-scripts/DeployCoreImplementationOnly.s.sol:DeployCoreImplementationOnly \ + --rpc-url https://demo.rpc.invalid \ + --private-key test-signing-key \ + --broadcast --slow --gas-estimate-multiplier 130 +assert_absent "$marker" 'upgrade does not execute selected repo env' + +: > "$forge_log" +: > "$tool_log" +( + cd "$eth" || exit 1 + PATH="$path" TOOL_LOG="$tool_log" FORGE_LOG="$forge_log" \ + MNEMONIC=test-mainnet-key RPC_URL=https://reviewed.rpc.invalid \ + ./sh/upgrade.sh mainnet TokenBridge demo +) > "$eth/upgrade-mainnet.out" 2>&1 +status=$? +assert_status 0 "$status" 'mainnet TokenBridge upgrade completes without guardian key' +assert_contains "$eth/upgrade-mainnet.out" \ + '../scripts/contract-upgrade-governance.sh -c demo -m token_bridge -a 0x2222222222222222222222222222222222222222' \ + 'mainnet upgrade prints the governance command' +assert_not_contains "$tool_log" 'worm ' \ + 'mainnet upgrade does not submit testnet governance' + +: > "$forge_log" +: > "$tool_log" +( + cd "$eth" || exit 1 + PATH="$path" TOOL_LOG="$tool_log" FORGE_LOG="$forge_log" \ + MNEMONIC=test-key GUARDIAN_MNEMONIC=test-guardian VERIFY_MODE=skip \ + ./sh/upgrade.sh testnet Core demo +) > "$eth/upgrade-skip.out" 2>&1 +status=$? +assert_status 0 "$status" 'matching implementation skips upgrade successfully' +if [ ! -s "$forge_log" ]; then + pass 'matching implementation does not invoke Forge' +else + fail 'matching implementation does not invoke Forge' +fi + +: > "$forge_log" +: > "$tool_log" +( + cd "$eth" || exit 1 + PATH="$path" TOOL_LOG="$tool_log" FORGE_LOG="$forge_log" \ + MNEMONIC=test-key GUARDIAN_MNEMONIC=test-guardian VERIFY_MODE=operational-fail \ + ./sh/upgrade.sh testnet Core demo +) > "$eth/upgrade-verification-error.out" 2>&1 +status=$? +assert_status 2 "$status" 'operational verification failure stops the upgrade' +if [ ! -s "$forge_log" ]; then + pass 'operational verification failure prevents deployment' +else + fail 'operational verification failure prevents deployment' +fi + +: > "$forge_log" +: > "$tool_log" +( + cd "$eth" || exit 1 + PATH="$path" TOOL_LOG="$tool_log" FORGE_LOG="$forge_log" \ + MNEMONIC=test-bulk-key GUARDIAN_MNEMONIC=test-bulk-guardian \ + CHAINS='alpha beta' MODULES=Core ./sh/upgrade_all_testnet.sh +) > "$eth/upgrade-bulk.out" 2>&1 +status=$? +assert_status 0 "$status" 'bulk upgrade handles two chains' +assert_contains "$tool_log" 'cast <--rpc-url> ' \ + 'bulk upgrade derives the alpha chain ID' +assert_contains "$tool_log" 'cast <--rpc-url> ' \ + 'bulk upgrade derives the beta chain ID' + +: > "$forge_log" +: > "$tool_log" +( + cd "$eth" || exit 1 + PATH="$path" TOOL_LOG="$tool_log" FORGE_LOG="$forge_log" \ + MNEMONIC=test-key GUARDIAN_MNEMONIC=test-guardian \ + RPC_URL=https://reviewed.rpc.invalid CHAINS='alpha beta' MODULES=Core \ + ./sh/upgrade_all_testnet.sh +) > "$eth/upgrade-multi-chain-rpc.out" 2>&1 +status=$? +assert_status 0 "$status" 'bulk upgrade ignores an inherited RPC override' +assert_not_contains "$tool_log" 'cast <--rpc-url> ' \ + 'bulk upgrade does not reuse one inherited RPC across chains' + +: > "$forge_log" +: > "$tool_log" +( + cd "$eth" || exit 1 + PATH="$path" TOOL_LOG="$tool_log" FORGE_LOG="$forge_log" \ + MNEMONIC=test-bulk-key GUARDIAN_MNEMONIC=test-bulk-guardian \ + CHAINS='alpha beta' MODULES=Core FORGE_FAIL_RPC_CONTAINS=alpha \ + ./sh/upgrade_all_testnet.sh +) > "$eth/upgrade-bulk-partial-failure.out" 2>&1 +status=$? +assert_status 1 "$status" 'bulk upgrade reports a partial failure' +assert_not_contains "$tool_log" 'cast <--rpc-url> ' \ + 'bulk upgrade stops after the first failed child' + +: > "$tool_log" +( + cd "$eth" || exit 1 + unset CHAINS MODULES + PATH="$path" TOOL_LOG="$tool_log" FORGE_LOG="$forge_log" \ + MNEMONIC=test-key GUARDIAN_MNEMONIC=test-guardian WORM_CHAINS_FAIL=1 \ + ./sh/upgrade_all_testnet.sh +) > "$eth/upgrade-chain-enumeration-failure.out" 2>&1 +status=$? +assert_status 1 "$status" 'bulk upgrade rejects failed chain enumeration' +assert_not_contains "$tool_log" 'worm ' \ + 'failed chain enumeration prevents upgrade attempts' + +: > "$tool_log" +( + cd "$eth" || exit 1 + unset CHAINS MODULES + PATH="$path" TOOL_LOG="$tool_log" FORGE_LOG="$forge_log" \ + MNEMONIC=test-key GUARDIAN_MNEMONIC=test-guardian WORM_CHAINS_EMPTY=1 \ + ./sh/upgrade_all_testnet.sh +) > "$eth/upgrade-empty-chain-list.out" 2>&1 +status=$? +assert_status 1 "$status" 'bulk upgrade rejects an empty chain list' +assert_not_contains "$tool_log" 'worm ' \ + 'empty chain enumeration prevents upgrade attempts' + +: > "$tool_log" +( + cd "$eth" || exit 1 + unset CHAINS MODULES + PATH="$path" TOOL_LOG="$tool_log" FORGE_LOG="$forge_log" \ + MNEMONIC=test-key GUARDIAN_MNEMONIC=test-guardian VERIFY_MODE=skip \ + ./sh/upgrade_all_testnet.sh +) > "$eth/upgrade-default-modules.out" 2>&1 +status=$? +assert_status 0 "$status" 'bulk upgrade default modules are supported' +assert_not_contains "$tool_log" '' \ + 'bulk upgrade defaults exclude unsupported NFTBridge' + +: > "$forge_log" +: > "$tool_log" +( + cd "$eth" || exit 1 + PATH="$path" TOOL_LOG="$tool_log" FORGE_LOG="$forge_log" \ + MNEMONIC=test-key RPC_URL=https://reviewed.rpc.invalid CAST_INVALID=1 \ + ./sh/upgrade.sh mainnet Core demo +) > "$eth/upgrade-invalid-chain.out" 2>&1 +status=$? +assert_status 1 "$status" 'invalid RPC chain ID fails closed' +if [ ! -s "$forge_log" ]; then + pass 'invalid RPC chain ID prevents Forge invocation' +else + fail 'invalid RPC chain ID prevents Forge invocation' +fi + +: > "$forge_log" +: > "$tool_log" +( + cd "$eth" || exit 1 + PATH="$path" TOOL_LOG="$tool_log" FORGE_LOG="$forge_log" \ + MNEMONIC=test-key ./sh/upgrade.sh typo Core demo +) > "$eth/upgrade-invalid-network.out" 2>&1 +status=$? +assert_status 1 "$status" 'invalid network fails closed' +if [ ! -s "$tool_log" ] && [ ! -s "$forge_log" ]; then + pass 'invalid network fails before external commands' +else + fail 'invalid network fails before external commands' +fi + +: > "$forge_log" +: > "$tool_log" +( + cd "$eth" || exit 1 + PATH="$path" TOOL_LOG="$tool_log" FORGE_LOG="$forge_log" \ + MNEMONIC=test-key GUARDIAN_MNEMONIC=test-guardian WORM_RPC_FAIL=1 \ + ./sh/upgrade.sh testnet Core demo +) > "$eth/upgrade-rpc-failure.out" 2>&1 +status=$? +assert_status 1 "$status" 'RPC lookup failure fails closed' +assert_not_contains "$tool_log" 'cast ' \ + 'RPC lookup failure prevents chain-ID lookup' +if [ ! -s "$forge_log" ]; then + pass 'RPC lookup failure prevents Forge invocation' +else + fail 'RPC lookup failure prevents Forge invocation' +fi + +: > "$forge_log" +: > "$tool_log" +( + cd "$eth" || exit 1 + PATH="$path" TOOL_LOG="$tool_log" FORGE_LOG="$forge_log" \ + MNEMONIC=test-key GUARDIAN_MNEMONIC=test-guardian FORGE_FAIL=1 \ + ./sh/upgrade.sh testnet Core demo +) > "$eth/upgrade-forge-failure.out" 2>&1 +status=$? +assert_status 1 "$status" 'Forge failure stops the upgrade' +assert_not_contains "$tool_log" 'worm ' \ + 'Forge failure prevents governance submission' + +: > "$forge_log" +: > "$tool_log" +( + cd "$eth" || exit 1 + PATH="$path" TOOL_LOG="$tool_log" FORGE_LOG="$forge_log" \ + MNEMONIC=test-key GUARDIAN_MNEMONIC=test-guardian VERIFY_MODE=post-fail \ + ./sh/upgrade.sh testnet Core demo +) > "$eth/upgrade-verification-failure.out" 2>&1 +status=$? +assert_status 1 "$status" 'post-deployment verification failure stops the upgrade' +assert_not_contains "$tool_log" 'worm ' \ + 'post-deployment verification failure prevents governance submission' + +: > "$forge_log" +: > "$tool_log" +( + cd "$eth" || exit 1 + PATH="$path" TOOL_LOG="$tool_log" FORGE_LOG="$forge_log" \ + MNEMONIC=test-key GUARDIAN_MNEMONIC=test-guardian WORM_GENERATE_FAIL=1 \ + ./sh/upgrade.sh testnet Core demo +) > "$eth/upgrade-generation-failure.out" 2>&1 +status=$? +assert_status 1 "$status" 'governance generation failure stops the upgrade' +assert_not_contains "$tool_log" 'worm ' \ + 'governance generation failure prevents submission' + +: > "$forge_log" +: > "$tool_log" +( + cd "$eth" || exit 1 + PATH="$path" TOOL_LOG="$tool_log" FORGE_LOG="$forge_log" \ + MNEMONIC=test-key GUARDIAN_MNEMONIC=test-guardian WORM_GENERATE_EMPTY=1 \ + ./sh/upgrade.sh testnet Core demo +) > "$eth/upgrade-empty-governance.out" 2>&1 +status=$? +assert_status 1 "$status" 'empty generated governance VAA stops the upgrade' +assert_contains "$tool_log" 'worm <> <-n> ' \ + 'empty generated governance VAA is rejected by submission' + +: > "$forge_log" +: > "$tool_log" +( + cd "$eth" || exit 1 + PATH="$path" TOOL_LOG="$tool_log" FORGE_LOG="$forge_log" \ + MNEMONIC=test-key GUARDIAN_MNEMONIC=test-guardian WORM_SUBMIT_FAIL=1 \ + ./sh/upgrade.sh testnet Core demo +) > "$eth/upgrade-submission-failure.out" 2>&1 +status=$? +assert_status 1 "$status" 'governance submission failure is reported' + +: > "$forge_log" +: > "$tool_log" +( + cd "$eth" || exit 1 + PATH="$path" TOOL_LOG="$tool_log" FORGE_LOG="$forge_log" \ + MNEMONIC=test-key GUARDIAN_MNEMONIC=test-guardian WORM_INVALID_IMPLEMENTATION=1 \ + ./sh/upgrade.sh testnet Core demo +) > "$eth/upgrade-invalid-current-address.out" 2>&1 +status=$? +assert_status 2 "$status" 'malformed current implementation address fails closed' +if [ ! -s "$forge_log" ]; then + pass 'malformed current implementation prevents Forge invocation' +else + fail 'malformed current implementation prevents Forge invocation' +fi + +for jq_mode in JQ_EMPTY JQ_NULL JQ_INVALID; do + : > "$forge_log" + : > "$tool_log" + ( + cd "$eth" || exit 1 + export "$jq_mode=1" + PATH="$path" TOOL_LOG="$tool_log" FORGE_LOG="$forge_log" \ + MNEMONIC=test-key GUARDIAN_MNEMONIC=test-guardian \ + ./sh/upgrade.sh testnet Core demo + ) > "$eth/upgrade-${jq_mode}.out" 2>&1 + status=$? + assert_status 1 "$status" "$jq_mode deployment address fails closed" + assert_not_contains "$tool_log" 'worm ' \ + "$jq_mode deployment address prevents governance submission" +done + +printf '1..%d\n' "$tests" +if [ "$failures" -ne 0 ]; then + printf '%d test(s) failed\n' "$failures" >&2 + exit 1 +fi +printf '%d tests passed\n' "$tests"