From 75b3c4709417de79c1ba2161098f5f162f620524 Mon Sep 17 00:00:00 2001 From: Jason Schwarz Date: Tue, 4 Nov 2025 16:57:28 -0400 Subject: [PATCH 01/10] feat: validate upgrade script --- contracts/validate-upgrade.sh | 105 ++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100755 contracts/validate-upgrade.sh diff --git a/contracts/validate-upgrade.sh b/contracts/validate-upgrade.sh new file mode 100755 index 000000000..1541e8116 --- /dev/null +++ b/contracts/validate-upgrade.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash + +# Script to validate contract upgrade safety using OpenZeppelin's upgrades-core +# This script wraps the npx @openzeppelin/upgrades-core validate command + +set -e + +contract="" +reference="" +build_info_path="out/build-info" +skip_build=false + +show_help() { + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " --contract Name of the new contract to validate (e.g., MevCommitAVSV2)" + echo " --reference Name of the reference/old contract (e.g., MevCommitAVS)" + echo " --build-info Path to build-info directory (default: out/build-info)" + echo " --skip-build Skip building contracts before validation" + echo " --help Show this help message" + echo "" + echo "Examples:" + echo " $0 --contract MevCommitAVSV2 --reference MevCommitAVS" + echo " $0 --contract ProviderRegistryV2 --reference ProviderRegistry --skip-build" + exit 0 +} + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + --contract) + contract="$2" + shift 2 + ;; + --reference) + reference="$2" + shift 2 + ;; + --build-info) + build_info_path="$2" + shift 2 + ;; + --skip-build) + skip_build=true + shift + ;; + --help|-h) + show_help + ;; + *) + echo "Error: Unknown option '$1'" + echo "Use --help for usage information" + exit 1 + ;; + esac +done + +# Validate required arguments +if [[ -z "$contract" ]]; then + echo "Error: --contract is required" + echo "Use --help for usage information" + exit 1 +fi + +if [[ -z "$reference" ]]; then + echo "Error: --reference is required" + echo "Use --help for usage information" + exit 1 +fi + +# Build contracts if not skipped +if [[ "$skip_build" != true ]]; then + echo "Building contracts..." + if ! (forge clean && forge build); then + echo "Error: Failed to build contracts" + exit 1 + fi + echo "" +fi + +# Check if build-info directory exists +if [[ ! -d "$build_info_path" ]]; then + echo "Error: Build info directory not found: $build_info_path" + echo "Make sure contracts are built (run 'forge clean && forge build')" + exit 1 +fi + +# Run validation +echo "Validating upgrade safety..." +echo "Contract: $contract" +echo "Reference: $reference" +echo "" + +if npx @openzeppelin/upgrades-core validate "$build_info_path" --contract "$contract" --reference "$reference"; then + echo "" + echo "✓ Upgrade validation passed!" + exit 0 +else + echo "" + echo "✗ Upgrade validation failed!" + echo "Please review the errors above and fix the issues before proceeding with the upgrade." + exit 1 +fi + From 8fb1f4c90178ea71e666886a6a8db06197c637a6 Mon Sep 17 00:00:00 2001 From: Jason Schwarz Date: Tue, 4 Nov 2025 18:18:57 -0400 Subject: [PATCH 02/10] refactor: add Anvil support --- contracts/l1-deployer-cli.sh | 121 +++++++++++++++++++++++++++++------ 1 file changed, 102 insertions(+), 19 deletions(-) diff --git a/contracts/l1-deployer-cli.sh b/contracts/l1-deployer-cli.sh index 100e0b259..edeff5f3a 100755 --- a/contracts/l1-deployer-cli.sh +++ b/contracts/l1-deployer-cli.sh @@ -12,6 +12,7 @@ deploy_reward_distributor_flag=false skip_release_verification_flag=false resume_flag=false wallet_type="" +private_key="" chain="" chain_id=0 deploy_contract="" @@ -34,12 +35,13 @@ help() { echo " deploy-reward-distributor Deploy and verify the RewardDistributor contract to L1." echo echo "Required Options:" - echo " --chain, -c Specify the chain to deploy to ('mainnet', 'holesky', or 'hoodi')." + echo " --chain, -c Specify the chain to deploy to ('mainnet', 'holesky', 'hoodi', or 'anvil')." echo - echo "Wallet Options (exactly one required):" + echo "Wallet Options (one required, except for anvil where --private-key is recommended):" echo " --keystore Use a keystore for deployment." echo " --ledger Use a Ledger hardware wallet for deployment." echo " --trezor Use a Trezor hardware wallet for deployment." + echo " --private-key Use a private key for deployment (useful for anvil/local testing)." echo echo "Optional Options:" echo " --skip-release-verification Skip the GitHub release verification step." @@ -49,7 +51,8 @@ help() { echo " --help Display this help message." echo echo "Notes:" - echo " - Exactly one command and one wallet option must be specified." + echo " - Exactly one command must be specified." + echo " - One wallet option must be specified (except for anvil where --private-key is recommended)." echo " - Options and commands can be specified in any order." echo " - Required arguments must immediately follow their options." echo @@ -66,11 +69,15 @@ help() { echo " SENDER Address of the sender." echo " RPC_URL RPC URL for the deployment chain." echo + echo " For Private Key (--private-key option):" + echo " SENDER Address of the sender (optional, derived from private key if not set)." + echo " RPC_URL RPC URL for the deployment chain." + echo echo "Examples:" echo " $0 deploy-all --chain mainnet --keystore --priority-gas-price 2000000000 --with-gas-price 5000000000" echo " $0 --ledger deploy-avs --chain holesky --priority-gas-price 2000000000 --with-gas-price 5000000000" echo " $0 --chain holesky deploy-middleware --trezor --priority-gas-price 2000000000 --with-gas-price 5000000000" - echo " $0 --chain mainnet --keystore --resume --priority-gas-price 2000000000 --with-gas-price 5000000000" + echo " $0 deploy-avs --chain anvil --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" exit 1 } @@ -152,8 +159,8 @@ parse_args() { exit 1 fi chain="$2" - if [[ "$chain" != "mainnet" && "$chain" != "holesky" && "$chain" != "hoodi" ]]; then - echo "Error: Unknown chain '$chain'. Valid options are 'mainnet', 'holesky', or hoodi." + if [[ "$chain" != "mainnet" && "$chain" != "holesky" && "$chain" != "hoodi" && "$chain" != "anvil" ]]; then + echo "Error: Unknown chain '$chain'. Valid options are 'mainnet', 'holesky', 'hoodi', or 'anvil'." exit 1 fi shift 2 @@ -168,7 +175,7 @@ parse_args() { ;; --keystore) if [[ -n "$wallet_type" ]]; then - echo "Error: Multiple wallet types specified. Please specify only one of --keystore, --ledger, or --trezor." + echo "Error: Multiple wallet types specified. Please specify only one wallet option." exit 1 fi wallet_type="keystore" @@ -176,7 +183,7 @@ parse_args() { ;; --ledger) if [[ -n "$wallet_type" ]]; then - echo "Error: Multiple wallet types specified. Please specify only one of --keystore, --ledger, or --trezor." + echo "Error: Multiple wallet types specified. Please specify only one wallet option." exit 1 fi wallet_type="ledger" @@ -184,12 +191,25 @@ parse_args() { ;; --trezor) if [[ -n "$wallet_type" ]]; then - echo "Error: Multiple wallet types specified. Please specify only one of --keystore, --ledger, or --trezor." + echo "Error: Multiple wallet types specified. Please specify only one wallet option." exit 1 fi wallet_type="trezor" shift ;; + --private-key) + if [[ -z "$2" ]]; then + echo "Error: --private-key requires an argument." + exit 1 + fi + if [[ -n "$wallet_type" ]]; then + echo "Error: Multiple wallet types specified. Please specify only one wallet option." + exit 1 + fi + wallet_type="private-key" + private_key="$2" + shift 2 + ;; --priority-gas-price) if [[ -z "$2" ]]; then echo "Error: --priority-gas-price requires an argument." @@ -221,8 +241,9 @@ parse_args() { usage fi - if [[ -z "$wallet_type" ]]; then - echo "Error: A wallet option is required. Please specify one of --keystore, --ledger, or --trezor." + if [[ -z "$wallet_type" && "$chain" != "anvil" ]]; then + echo "Error: A wallet option is required. Please specify one of --keystore, --ledger, --trezor, or --private-key." + echo "Note: For anvil, --private-key is recommended but not required." usage fi @@ -244,12 +265,27 @@ parse_args() { check_env_variables() { local missing_vars=() - local required_vars=("SENDER" "RPC_URL" "ETHERSCAN_API_KEY") + local required_vars=("RPC_URL") + + # ETHERSCAN_API_KEY is only required for non-anvil chains (for verification) + if [[ "$chain" != "anvil" ]]; then + required_vars+=("ETHERSCAN_API_KEY") + fi if [[ "$wallet_type" == "keystore" ]]; then - required_vars+=("KEYSTORES" "KEYSTORE_PASSWORD") + required_vars+=("KEYSTORES" "KEYSTORE_PASSWORD" "SENDER") elif [[ "$wallet_type" == "ledger" || "$wallet_type" == "trezor" ]]; then - required_vars+=("HD_PATHS") + required_vars+=("HD_PATHS" "SENDER") + elif [[ "$wallet_type" == "private-key" ]]; then + # SENDER is optional for private-key, can be derived from the key + # But we'll still require it for consistency unless on anvil + if [[ "$chain" != "anvil" ]]; then + required_vars+=("SENDER") + fi + elif [[ -z "$wallet_type" && "$chain" == "anvil" ]]; then + # For anvil without explicit wallet, we'll use private-key from env or default + # SENDER is optional + : fi for var in "${required_vars[@]}"; do @@ -275,10 +311,20 @@ get_chain_params() { elif [[ "$chain" == "hoodi" ]]; then chain_id=560048 deploy_contract="DeployHoodi" + elif [[ "$chain" == "anvil" ]]; then + chain_id=31337 + # For anvil, use DeployAVSWithMockEigen which doesn't have chain-specific variants + # It's a single contract that works for anvil + deploy_contract="DeployAVSWithMockEigen" fi } check_git_status() { + # Skip git checks for anvil (local testing) + if [[ "$chain" == "anvil" ]]; then + return + fi + if [[ ${chain_id:-0} -eq 1 ]]; then if ! current_tag=$(git describe --tags --exact-match 2>/dev/null); then echo "Error: Current commit is not tagged. Please ensure the commit is tagged before deploying." @@ -339,6 +385,11 @@ check_rpc_url() { } check_etherscan_api_key() { + # Skip etherscan check for anvil (not needed for local testing) + if [[ "$chain" == "anvil" ]]; then + return + fi + response=$(curl -s "https://api.etherscan.io/v2/api?chainid=${chain_id}&module=account&action=balance&address=${SENDER}&tag=latest&apikey=${ETHERSCAN_API_KEY}") status=$(echo "$response" | grep -o '"status":"[0-9]"' | cut -d':' -f2 | tr -d '"') @@ -357,12 +408,15 @@ deploy_contract_generic() { declare -a forge_args=() forge_args+=("script" "${script_path}:${deploy_contract}") forge_args+=("--rpc-url" "${RPC_URL}") - forge_args+=("--sender" "${SENDER}") forge_args+=("--via-ir") forge_args+=("--chain-id" "${chain_id}") forge_args+=("--use" "0.8.26") forge_args+=("--broadcast") - forge_args+=("--verify") + + # Add verification if ETHERSCAN_API_KEY is set (and not anvil) + if [[ -n "${ETHERSCAN_API_KEY:-}" && "$chain" != "anvil" ]]; then + forge_args+=("--verify") + fi if [[ -n "$priority_gas_price" ]]; then forge_args+=("--priority-gas-price" "${priority_gas_price}") @@ -379,19 +433,42 @@ deploy_contract_generic() { if [[ "$wallet_type" == "keystore" ]]; then forge_args+=("--keystores" "${KEYSTORES}") forge_args+=("--password" "${KEYSTORE_PASSWORD}") + forge_args+=("--sender" "${SENDER}") elif [[ "$wallet_type" == "ledger" ]]; then forge_args+=("--ledger") forge_args+=("--hd-paths" "${HD_PATHS}") + forge_args+=("--sender" "${SENDER}") elif [[ "$wallet_type" == "trezor" ]]; then forge_args+=("--trezor") forge_args+=("--hd-paths" "${HD_PATHS}") + forge_args+=("--sender" "${SENDER}") + elif [[ "$wallet_type" == "private-key" ]]; then + forge_args+=("--private-key" "${private_key}") + if [[ -n "${SENDER:-}" ]]; then + forge_args+=("--sender" "${SENDER}") + fi + elif [[ -z "$wallet_type" && "$chain" == "anvil" ]]; then + # For anvil without explicit wallet, try to use private key from env or default anvil key + if [[ -n "${PRIVATE_KEY:-}" ]]; then + forge_args+=("--private-key" "${PRIVATE_KEY}") + elif [[ -n "${private_key:-}" ]]; then + forge_args+=("--private-key" "${private_key}") + else + # Use default anvil private key (first account) + forge_args+=("--private-key" "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80") + fi + if [[ -n "${SENDER:-}" ]]; then + forge_args+=("--sender" "${SENDER}") + fi fi + local wallet_desc="${wallet_type:-private-key (anvil default)}" + if forge "${forge_args[@]}"; then - echo "Successfully ran ${script_path} on chain ID ${chain_id} using ${wallet_type}." + echo "Successfully ran ${script_path} on chain ID ${chain_id} using ${wallet_desc}." echo "Remember to update documentation with new contract addresses!" else - echo "Error: Failed to run ${script_path} on chain ID ${chain_id} using ${wallet_type}." + echo "Error: Failed to run ${script_path} on chain ID ${chain_id} using ${wallet_desc}." exit 1 fi } @@ -405,7 +482,13 @@ deploy_vanilla_rep() { } deploy_avs() { - deploy_contract_generic "scripts/validator-registry/avs/DeployAVS.s.sol" + local script_path + if [[ "$chain" == "anvil" ]]; then + script_path="scripts/validator-registry/avs/DeployAVSWithMockEigen.s.sol" + else + script_path="scripts/validator-registry/avs/DeployAVS.s.sol" + fi + deploy_contract_generic "$script_path" } deploy_middleware() { From d98febb63b35a7192f49ed9d84d9a8bc186bd478 Mon Sep 17 00:00:00 2001 From: Jason Schwarz Date: Tue, 4 Nov 2025 18:20:21 -0400 Subject: [PATCH 03/10] feat: add contracts upgrade scripts --- contracts/contracts/upgrades/README.md | 121 ++++ contracts/l1-upgrade-cli.sh | 534 ++++++++++++++++++ .../upgrades/GenericMultisigUpgrade.s.sol | 204 +++++++ .../scripts/upgrades/GenericUpgrade.s.sol | 180 ++++++ contracts/scripts/upgrades/README.md | 416 ++++++++++++++ 5 files changed, 1455 insertions(+) create mode 100644 contracts/contracts/upgrades/README.md create mode 100755 contracts/l1-upgrade-cli.sh create mode 100644 contracts/scripts/upgrades/GenericMultisigUpgrade.s.sol create mode 100644 contracts/scripts/upgrades/GenericUpgrade.s.sol create mode 100644 contracts/scripts/upgrades/README.md diff --git a/contracts/contracts/upgrades/README.md b/contracts/contracts/upgrades/README.md new file mode 100644 index 000000000..fede25df2 --- /dev/null +++ b/contracts/contracts/upgrades/README.md @@ -0,0 +1,121 @@ +# Contract Upgrade Structure + +## Complete Example: ProviderRegistry Upgrade Journey + +### Timeline of Changes + +#### T0: Initial Deployment +``` +contracts/ +├── core/ +│ ├── ProviderRegistry.sol +│ └── ProviderRegistryStorage.sol +└── upgrades/ + └── (empty) +``` + +#### T1: First Upgrade (V1 → V2) +``` +contracts/ +├── core/ +│ ├── ProviderRegistryV2.sol # Current +│ └── ProviderRegistryV2Storage.sol +└── upgrades/ + └── core/ + ├── ProviderRegistry.sol # V1 archived + └── ProviderRegistryStorage.sol +``` + +#### T2: Second Upgrade (V2 → V3) +``` +contracts/ +├── core/ +│ ├── ProviderRegistryV3.sol # Current +│ └── ProviderRegistryV3Storage.sol +└── upgrades/ + └── core/ + ├── ProviderRegistry.sol # V1 archived + ├── ProviderRegistryStorage.sol + ├── ProviderRegistryV2.sol # V2 archived + └── ProviderRegistryV2Storage.sol +``` + +--- + +## Rules & Guidelines + +### 1. Version Naming +- **V1 (Initial):** No suffix - `Contract.sol` +- **V2+:** Explicit suffix - `ContractV2.sol`, `ContractV3.sol`, etc. + +### 2. Current Version Location +- Always in the **feature folder root** +- Always has the highest version number suffix + +### 3. Previous Versions Location +- Always in the **centralized `contracts/upgrades/` folder** +- Maintains the **same subdirectory structure** as the original location +- Example: `core/ProviderRegistry.sol` → `upgrades/core/ProviderRegistry.sol` + +### 4. Storage Contracts +- Follow the same versioning pattern as their implementation +- Move to `contracts/upgrades/` alongside their implementation +- Preserve the same subdirectory structure + +### 5. Upgrade Process +1. Create new versioned contract (e.g., `ContractV3.sol`) in feature folder root +2. Move previous version to `contracts/upgrades/[feature-folder]/` +3. Ensure subdirectory structure matches original location +4. Update imports in dependent contracts +5. Update tests to reference new version +6. Update upgrade scripts + +### 6. OpenZeppelin Annotation +Always include the reference contract annotation: +```solidity +/// @custom:oz-upgrades-from ProviderRegistryV2 +contract ProviderRegistryV3 is ... +``` + +--- + +## Multi-Contract Example: validator-registry/ + +Shows multiple contracts at different version stages: + +``` +contracts/ +├── validator-registry/ +│ ├── ValidatorOptInHub.sol # V1 (no suffix, never upgraded) +│ ├── ValidatorOptInHubStorage.sol +│ ├── MevCommitAVSV2.sol # V2 (current, was upgraded once) +│ ├── MevCommitAVSV2Storage.sol +│ ├── rewards/ +│ │ ├── RewardManagerV3.sol # V3 (current, upgraded twice) +│ │ └── RewardManagerV3Storage.sol +│ └── avs/ +│ └── MevCommitAVSV2.sol +└── upgrades/ + └── validator-registry/ + ├── avs/ + │ ├── MevCommitAVS.sol # V1 archived + │ └── MevCommitAVSStorage.sol + └── rewards/ + ├── RewardManager.sol # V1 archived + ├── RewardManagerStorage.sol + ├── RewardManagerV2.sol # V2 archived + └── RewardManagerV2Storage.sol +``` + +--- + +## Benefits of This Structure + +1. **Clear Version History:** Easy to see all previous versions +2. **Current Version Obvious:** Highest version in root = current +3. **Organized Archives:** All old code in dedicated upgrades/ folder +4. **Git-Friendly:** Easier to track changes per version +5. **Team Clarity:** New developers immediately see current vs. archived + +--- + diff --git a/contracts/l1-upgrade-cli.sh b/contracts/l1-upgrade-cli.sh new file mode 100755 index 000000000..d6f04afa4 --- /dev/null +++ b/contracts/l1-upgrade-cli.sh @@ -0,0 +1,534 @@ +#!/usr/bin/env bash + +# MUST READ - /mev-commit/contracts/scripts/upgrades/README.md + +upgrade_contract_flag=false +old_contract="" +new_contract="" +proxy_address="" +wallet_type="" +private_key="" +chain="" +chain_id=0 +upgrade_script="" +priority_gas_price="" +with_gas_price="" +resume_flag=false +multisig_flag=false + +help() { + echo "Usage:" + echo " $0 upgrade --old-contract --new-contract --proxy-address --chain [optional options]" + echo + echo "Commands (one required):" + echo " upgrade Upgrade a contract from old version to new version." + echo + echo "Required Options:" + echo " --old-contract, -o Name of the old contract version (e.g., MevCommitAVS)." + echo " --new-contract, -n Name of the new contract version (e.g., MevCommitAVSV2)." + echo " --proxy-address, -p Address of the proxy contract to upgrade (not required with --multisig)." + echo " --chain, -c Specify the chain to upgrade on ('mainnet', 'holesky', 'hoodi', or 'anvil')." + echo + echo "Wallet Options (one required, except for anvil where --private-key is recommended):" + echo " --keystore Use a keystore for upgrade." + echo " --ledger Use a Ledger hardware wallet for upgrade." + echo " --trezor Use a Trezor hardware wallet for upgrade." + echo " --private-key Use a private key for upgrade (useful for anvil/local testing)." + echo + echo "Optional Options:" + echo " --multisig Deploy implementation contract only (for multisig upgrades)." + echo " When used, proxy upgrade is skipped and only the implementation is deployed." + echo " Proxy address is not required when using this flag." + echo " --resume Resume the upgrade process if previously interrupted." + echo " --priority-gas-price Sets the priority gas price for EIP1559 transactions. Useful for when gas prices are volatile." + echo " --with-gas-price Sets the gas price for broadcasted legacy transactions, or the max fee for broadcasted EIP1559 transactions." + echo " --help Display this help message." + echo + echo "Notes:" + echo " - Exactly one command must be specified." + echo " - Options and commands can be specified in any order." + echo " - Required arguments must immediately follow their options." + echo + echo "Environment Variables Required:" + echo " For Keystore:" + echo " KEYSTORES Path(s) to keystore(s) passed to forge script as --keystores flag." + echo " KEYSTORE_PASSWORD Password(s) for keystore(s) passed to forge script as --password flag." + echo " SENDER Address of the sender." + echo " RPC_URL RPC URL for the upgrade chain." + echo " ETHERSCAN_API_KEY API key for etherscan contract verification." + echo + echo " For Ledger or Trezor:" + echo " HD_PATHS Derivation path(s) for hardware wallets passed to forge script as --hd-paths flag." + echo " SENDER Address of the sender." + echo " RPC_URL RPC URL for the upgrade chain." + echo + echo " For Private Key (--private-key option):" + echo " SENDER Address of the sender (optional, derived from private key if not set)." + echo " RPC_URL RPC URL for the upgrade chain." + echo + echo " Optional:" + echo " PROXY_ADDRESS Proxy address (can be provided via --proxy-address instead)." + echo + echo "Examples:" + echo " $0 upgrade --old-contract MevCommitAVS --new-contract MevCommitAVSV2 --proxy-address 0x1234... --chain mainnet --keystore" + echo " $0 upgrade --old-contract ProviderRegistry --new-contract ProviderRegistryV2 --proxy-address 0x5678... --chain holesky --ledger --priority-gas-price 2000000000" + echo " $0 upgrade --old-contract MevCommitAVS --new-contract MevCommitAVSV2 --proxy-address 0x1234... --chain anvil --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" + echo " $0 upgrade --old-contract MevCommitAVS --new-contract MevCommitAVSV2 --chain mainnet --multisig --keystore" + exit 1 +} + +usage() { + echo "Usage:" + echo " $0 upgrade --old-contract --new-contract --proxy-address --chain [wallet option] [options]" + echo + echo "Use '$0 --help' to see available commands and options." + exit 1 +} + +check_dependencies() { + local missing_utils=() + local required_utilities=( + git + forge + cast + curl + jq + ) + for util in "${required_utilities[@]}"; do + if ! command -v "${util}" &> /dev/null; then + missing_utils+=("${util}") + fi + done + if [[ ${#missing_utils[@]} -ne 0 ]]; then + echo "Error: The following required utilities are not installed: ${missing_utils[*]}." + exit 1 + fi +} + +parse_args() { + if [[ $# -eq 0 ]]; then + usage + fi + + while [[ $# -gt 0 ]]; do + key="$1" + case $key in + upgrade) + upgrade_contract_flag=true + shift + ;; + --old-contract|-o) + if [[ -z "$2" ]]; then + echo "Error: --old-contract requires an argument." + exit 1 + fi + old_contract="$2" + shift 2 + ;; + --new-contract|-n) + if [[ -z "$2" ]]; then + echo "Error: --new-contract requires an argument." + exit 1 + fi + new_contract="$2" + shift 2 + ;; + --proxy-address|-p) + if [[ -z "$2" ]]; then + echo "Error: --proxy-address requires an argument." + exit 1 + fi + proxy_address="$2" + shift 2 + ;; + --chain|-c) + if [[ -z "$2" ]]; then + echo "Error: --chain requires an argument." + exit 1 + fi + chain="$2" + if [[ "$chain" != "mainnet" && "$chain" != "holesky" && "$chain" != "hoodi" && "$chain" != "anvil" ]]; then + echo "Error: Unknown chain '$chain'. Valid options are 'mainnet', 'holesky', 'hoodi', or 'anvil'." + exit 1 + fi + shift 2 + ;; + --multisig) + multisig_flag=true + shift + ;; + --resume) + resume_flag=true + shift + ;; + --keystore) + if [[ -n "$wallet_type" ]]; then + echo "Error: Multiple wallet types specified. Please specify only one wallet option." + exit 1 + fi + wallet_type="keystore" + shift + ;; + --ledger) + if [[ -n "$wallet_type" ]]; then + echo "Error: Multiple wallet types specified. Please specify only one wallet option." + exit 1 + fi + wallet_type="ledger" + shift + ;; + --trezor) + if [[ -n "$wallet_type" ]]; then + echo "Error: Multiple wallet types specified. Please specify only one wallet option." + exit 1 + fi + wallet_type="trezor" + shift + ;; + --private-key) + if [[ -z "$2" ]]; then + echo "Error: --private-key requires an argument." + exit 1 + fi + if [[ -n "$wallet_type" ]]; then + echo "Error: Multiple wallet types specified. Please specify only one wallet option." + exit 1 + fi + wallet_type="private-key" + private_key="$2" + shift 2 + ;; + --priority-gas-price) + if [[ -z "$2" ]]; then + echo "Error: --priority-gas-price requires an argument." + exit 1 + fi + priority_gas_price="$2" + shift 2 + ;; + --with-gas-price) + if [[ -z "$2" ]]; then + echo "Error: --with-gas-price requires an argument." + exit 1 + fi + with_gas_price="$2" + shift 2 + ;; + --help) + help + ;; + *) + echo "Error: Unknown command or option '$1'." + usage + ;; + esac + done + + if [[ -z "$chain" ]]; then + echo "Error: The --chain option is required." + usage + fi + + if [[ -z "$wallet_type" && "$chain" != "anvil" ]]; then + echo "Error: A wallet option is required. Please specify one of --keystore, --ledger, --trezor, or --private-key." + echo "Note: For anvil, --private-key is recommended but not required." + usage + fi + + if [[ "$upgrade_contract_flag" != true ]]; then + echo "Error: No command specified. Use 'upgrade' command." + usage + fi + + if [[ -z "$old_contract" ]]; then + echo "Error: The --old-contract option is required." + usage + fi + + if [[ -z "$new_contract" ]]; then + echo "Error: The --new-contract option is required." + usage + fi + + # Proxy address is only required for direct upgrades, not for multisig deployments + if [[ "$multisig_flag" != true ]]; then + if [[ -z "$proxy_address" ]]; then + # Try to get from environment variable + if [[ -n "${PROXY_ADDRESS}" ]]; then + proxy_address="${PROXY_ADDRESS}" + else + echo "Error: The --proxy-address option is required (or set PROXY_ADDRESS environment variable)." + echo "Note: If deploying for multisig upgrade, use --multisig flag (proxy address not needed)." + usage + fi + fi + fi +} + +check_env_variables() { + local missing_vars=() + local required_vars=("RPC_URL") + + if [[ "$wallet_type" == "keystore" ]]; then + required_vars+=("KEYSTORES" "KEYSTORE_PASSWORD" "SENDER") + # ETHERSCAN_API_KEY is optional for upgrades but recommended for verification + elif [[ "$wallet_type" == "ledger" || "$wallet_type" == "trezor" ]]; then + required_vars+=("HD_PATHS" "SENDER") + elif [[ "$wallet_type" == "private-key" ]]; then + # SENDER is optional for private-key, can be derived from the key + # But we'll still require it for consistency unless on anvil + if [[ "$chain" != "anvil" ]]; then + required_vars+=("SENDER") + fi + elif [[ -z "$wallet_type" && "$chain" == "anvil" ]]; then + # For anvil without explicit wallet, we'll use private-key from env or default + # SENDER is optional + : + fi + + for var in "${required_vars[@]}"; do + if [[ -z "${!var}" ]]; then + missing_vars+=("${var}") + fi + done + + if [[ ${#missing_vars[@]} -ne 0 ]]; then + echo "Error: The following environment variables are not set: ${missing_vars[*]}." + echo "Please set them before running the script." + exit 1 + fi +} + +get_chain_params() { + if [[ "$chain" == "mainnet" ]]; then + chain_id=1 + if [[ "$multisig_flag" == true ]]; then + upgrade_script="DeployMultisigImplMainnet" + else + upgrade_script="UpgradeContractMainnet" + fi + elif [[ "$chain" == "holesky" ]]; then + chain_id=17000 + if [[ "$multisig_flag" == true ]]; then + upgrade_script="DeployMultisigImplHolesky" + else + upgrade_script="UpgradeContractHolesky" + fi + elif [[ "$chain" == "hoodi" ]]; then + chain_id=560048 + if [[ "$multisig_flag" == true ]]; then + upgrade_script="DeployMultisigImplHoodi" + else + upgrade_script="UpgradeContractHoodi" + fi + elif [[ "$chain" == "anvil" ]]; then + chain_id=31337 + if [[ "$multisig_flag" == true ]]; then + upgrade_script="DeployMultisigImplAnvil" + else + upgrade_script="UpgradeContractAnvil" + fi + fi +} + +check_git_status() { + # Skip git checks for anvil (local testing) + if [[ "$chain" == "anvil" ]]; then + return + fi + + if [[ ${chain_id:-0} -eq 1 ]]; then + if ! current_tag=$(git describe --tags --exact-match 2>/dev/null); then + echo "Error: Current commit is not tagged. Please ensure the commit is tagged before upgrading on mainnet." + exit 1 + fi + + if [[ -n "$(git status --porcelain)" ]]; then + echo "Error: There are uncommitted changes. Please commit or stash them before upgrading on mainnet." + exit 1 + fi + fi +} + +check_rpc_url() { + queried_chain_id=$(cast chain-id --rpc-url "$RPC_URL" 2>/dev/null) + cast_exit_code=$? + if [[ $cast_exit_code -ne 0 ]]; then + echo "Error: Failed to retrieve chain ID using the provided RPC URL." + echo "Please ensure the RPC URL is correct and accessible." + exit 1 + fi + if [[ "$queried_chain_id" -ne "$chain_id" ]]; then + echo "Error: RPC URL does not match the expected chain ID." + echo "Expected chain ID: $chain_id, but got: $queried_chain_id." + exit 1 + fi + + # Skip RPC URL warning for anvil (local testing) + if [[ "$chain" != "anvil" && "$RPC_URL" != *"alchemy"* && "$RPC_URL" != *"infura"* ]]; then + echo "Warning: You may be using a public rate-limited RPC URL. Contract verification may fail." + read -p "Do you want to continue? (y/n) " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "Exiting script." + exit 1 + fi + fi +} + +find_contract_path() { + local contract_name="$1" + local search_path="$2" + + # Try to find the contract file + local contract_file=$(find contracts -name "${contract_name}.sol" -type f 2>/dev/null | head -n 1) + + if [[ -n "$contract_file" ]]; then + echo "$contract_file" + return 0 + fi + + # If not found in contracts, try upgrades folder + contract_file=$(find contracts/upgrades -name "${contract_name}.sol" -type f 2>/dev/null | head -n 1) + + if [[ -n "$contract_file" ]]; then + echo "$contract_file" + return 0 + fi + + return 1 +} + +upgrade_contract() { + # Find the contract paths + local new_contract_path=$(find_contract_path "$new_contract" "contracts") + local old_contract_path=$(find_contract_path "$old_contract" "contracts/upgrades") + + if [[ -z "$new_contract_path" ]]; then + echo "Error: Could not find contract file for $new_contract" + echo "Please ensure the contract exists in the contracts/ directory." + exit 1 + fi + + # Extract just the filename for deployment + local new_contract_filename=$(basename "$new_contract_path") + + if [[ "$multisig_flag" == true ]]; then + echo "Deploying implementation contract for multisig upgrade..." + else + echo "Upgrading contract..." + fi + echo "" + + forge clean + + declare -a forge_args=() + + # Select the appropriate script based on multisig flag + if [[ "$multisig_flag" == true ]]; then + forge_args+=("script" "scripts/upgrades/GenericMultisigUpgrade.s.sol:${upgrade_script}") + else + forge_args+=("script" "scripts/upgrades/GenericUpgrade.s.sol:${upgrade_script}") + fi + + forge_args+=("--rpc-url" "${RPC_URL}") + forge_args+=("--via-ir") + forge_args+=("--chain-id" "${chain_id}") + forge_args+=("--use" "0.8.26") + forge_args+=("--broadcast") + + # Add verification if ETHERSCAN_API_KEY is set (and not anvil) + if [[ -n "${ETHERSCAN_API_KEY:-}" && "$chain" != "anvil" ]]; then + forge_args+=("--verify") + fi + + if [[ -n "$priority_gas_price" ]]; then + forge_args+=("--priority-gas-price" "${priority_gas_price}") + fi + + if [[ -n "$with_gas_price" ]]; then + forge_args+=("--with-gas-price" "${with_gas_price}") + fi + + if [[ "$resume_flag" == true ]]; then + forge_args+=("--resume") + fi + + if [[ "$wallet_type" == "keystore" ]]; then + forge_args+=("--keystores" "${KEYSTORES}") + forge_args+=("--password" "${KEYSTORE_PASSWORD}") + forge_args+=("--sender" "${SENDER}") + elif [[ "$wallet_type" == "ledger" ]]; then + forge_args+=("--ledger") + forge_args+=("--hd-paths" "${HD_PATHS}") + forge_args+=("--sender" "${SENDER}") + elif [[ "$wallet_type" == "trezor" ]]; then + forge_args+=("--trezor") + forge_args+=("--hd-paths" "${HD_PATHS}") + forge_args+=("--sender" "${SENDER}") + elif [[ "$wallet_type" == "private-key" ]]; then + forge_args+=("--private-key" "${private_key}") + if [[ -n "${SENDER:-}" ]]; then + forge_args+=("--sender" "${SENDER}") + fi + elif [[ -z "$wallet_type" && "$chain" == "anvil" ]]; then + # For anvil without explicit wallet, try to use private key from env or default anvil key + if [[ -n "${PRIVATE_KEY:-}" ]]; then + forge_args+=("--private-key" "${PRIVATE_KEY}") + elif [[ -n "${private_key:-}" ]]; then + forge_args+=("--private-key" "${private_key}") + else + # Use default anvil private key (first account) + forge_args+=("--private-key" "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80") + fi + if [[ -n "${SENDER:-}" ]]; then + forge_args+=("--sender" "${SENDER}") + fi + fi + + # Pass contract names and paths as environment variables + export OLD_CONTRACT_NAME="$old_contract" + export NEW_CONTRACT_NAME="$new_contract" + export NEW_CONTRACT_PATH="$new_contract_filename" + + # Proxy address only needed for direct upgrades + if [[ "$multisig_flag" != true ]]; then + export PROXY_ADDRESS="$proxy_address" + fi + + local wallet_desc="${wallet_type:-private-key (anvil default)}" + + if forge "${forge_args[@]}"; then + if [[ "$multisig_flag" == true ]]; then + echo "" + echo "✓ Implementation contract deployed successfully!" + echo " Contract: $old_contract -> $new_contract" + echo " Chain ID: ${chain_id}" + else + echo "" + echo "✓ Successfully upgraded $old_contract to $new_contract" + echo " Chain ID: ${chain_id}" + echo " Proxy: $proxy_address" + fi + else + if [[ "$multisig_flag" == true ]]; then + echo "Error: Failed to deploy implementation contract for $new_contract on chain ID ${chain_id} using ${wallet_desc}." + else + echo "Error: Failed to upgrade $old_contract to $new_contract on chain ID ${chain_id} using ${wallet_desc}." + fi + exit 1 + fi +} + +main() { + check_dependencies + parse_args "$@" + check_env_variables + get_chain_params + check_git_status + check_rpc_url + upgrade_contract +} + +main "$@" + diff --git a/contracts/scripts/upgrades/GenericMultisigUpgrade.s.sol b/contracts/scripts/upgrades/GenericMultisigUpgrade.s.sol new file mode 100644 index 000000000..592815bf7 --- /dev/null +++ b/contracts/scripts/upgrades/GenericMultisigUpgrade.s.sol @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: BSL 1.1 + +// solhint-disable no-console + +pragma solidity 0.8.26; + +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; +import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol"; +import {Options} from "openzeppelin-foundry-upgrades/Options.sol"; + +/** + * @title GenericMultisigUpgrade + * @notice Generic script to deploy implementation contracts for multisig upgrades. + * + * This script deploys ONLY the implementation contract without initializing it. + * After deployment, use your multisig UI to call upgradeToAndCall() on the proxy. + * + * This script reads configuration from environment variables: + * - NEW_CONTRACT_NAME: Name of the new contract (used for contract name resolution, e.g., "MevCommitAVSV2") + * - NEW_CONTRACT_PATH: Path to the new contract file (e.g., "MevCommitAVSV2.sol") - used for logging only + * + * ⚠️ IMPORTANT: Run validation before deploying! + * ./validate-upgrade.sh --contract --reference + * + * Usage: + * forge script scripts/upgrades/GenericMultisigDeploy.s.sol:DeployMultisigImplMainnet --rpc-url --sender --broadcast --verify -vvvv + * + * Note: The deployer address can be any account (doesn't need to be the multisig). + * The implementation contract serves only as a blueprint and has no ownership. + */ +contract GenericMultisigUpgrade is Script { + // ANSI color codes + string private constant _RESET = "\x1b[0m"; + string private constant _GREEN = "\x1b[32m"; + string private constant _BRIGHT_GREEN = "\x1b[92m"; + string private constant _YELLOW = "\x1b[33m"; + string private constant _CYAN = "\x1b[36m"; + string private constant _BRIGHT_CYAN = "\x1b[96m"; + string private constant _BLUE = "\x1b[34m"; + + function run() public virtual { + vm.startBroadcast(); + + // Get contract name and path from environment variables + string memory newContractName = vm.envOr("NEW_CONTRACT_NAME", string("")); + string memory newContractPath = vm.envOr("NEW_CONTRACT_PATH", string("")); + string memory oldContractName = vm.envOr("OLD_CONTRACT_NAME", string("")); + + console.log(string.concat(_CYAN, "Deploying implementation contract on chain:", _RESET), block.chainid); + console.log(string.concat(_CYAN, "Contract name:", _RESET), newContractName); + if (bytes(newContractPath).length > 0) { + console.log(string.concat(_CYAN, "Contract path:", _RESET), newContractPath); + } + console.log(string.concat(_CYAN, "Deployer address:", _RESET), msg.sender); + + // Construct contract identifiers for validation and deployment + // Format: ContractName.sol (OpenZeppelin will resolve to fully qualified name) + string memory contractIdentifier = bytes(newContractPath).length > 0 + ? newContractPath + : string.concat(newContractName, ".sol"); + + // Validate upgrade safety with OpenZeppelin Foundry Upgrades + if (bytes(oldContractName).length > 0) { + console.log(""); + console.log(string.concat(_YELLOW, "Validating upgrade safety with OpenZeppelin Foundry Upgrades...", _RESET)); + Options memory opts; + // Construct reference contract identifier (just filename, OpenZeppelin will resolve it) + string memory referenceIdentifier = string.concat(oldContractName, ".sol"); + opts.referenceContract = referenceIdentifier; + Upgrades.validateUpgrade(contractIdentifier, opts); + console.log(string.concat(_BRIGHT_GREEN, "[PASS] OpenZeppelin Foundry Upgrades verification passed!", _RESET)); + console.log(""); + } + + // Deploy the implementation contract using vm.deployCode() + // This deploys the contract bytecode without calling any constructor/initializer + address implementation = vm.deployCode(contractIdentifier); + + if (implementation == address(0)) { + revert("Failed to deploy implementation"); + } + + console.log(""); + console.log(string.concat(_BRIGHT_GREEN, "========================================", _RESET)); + console.log(string.concat(_BRIGHT_GREEN, "Implementation contract deployed successfully!", _RESET)); + console.log(string.concat(_BRIGHT_GREEN, "========================================", _RESET)); + console.log(string.concat(_CYAN, "Contract name:", _RESET), newContractName); + console.log(string.concat(_BRIGHT_CYAN, "Implementation address:", _RESET), implementation); + console.log(""); + console.log(string.concat(_YELLOW, "Next steps:", _RESET)); + console.log(string.concat(_YELLOW, "1. Copy the implementation address above", _RESET)); + console.log(string.concat(_YELLOW, "2. Use your multisig UI (e.g., Safe wallet) to call upgradeToAndCall() on the proxy", _RESET)); + console.log(string.concat(_YELLOW, "3. Function: upgradeToAndCall(implementation, callData)", _RESET)); + console.log(string.concat(_YELLOW, " - implementation: the address shown above", _RESET)); + console.log(string.concat(_YELLOW, " - callData: empty bytes (\"0x\") if no initialization needed", _RESET)); + console.log(string.concat(_BRIGHT_GREEN, "========================================", _RESET)); + console.log(""); + + vm.stopBroadcast(); + } + + /** + * @notice Alternative entry point (directly called by the user) that accepts contract name as parameter + * @param newContractName The name of the new contract (e.g., "MevCommitAVSV2") + */ + function run(string calldata newContractName) public { + vm.startBroadcast(); + + string memory newContractPath = vm.envOr("NEW_CONTRACT_PATH", string("")); + string memory oldContractName = vm.envOr("OLD_CONTRACT_NAME", string("")); + + console.log(string.concat(_CYAN, "Deploying implementation contract on chain:", _RESET), block.chainid); + console.log(string.concat(_CYAN, "Contract name:", _RESET), newContractName); + if (bytes(newContractPath).length > 0) { + console.log(string.concat(_CYAN, "Contract path:", _RESET), newContractPath); + } + console.log(string.concat(_CYAN, "Deployer address:", _RESET), msg.sender); + + // Construct contract identifier for validation and deployment + // Format: ContractName.sol (OpenZeppelin will resolve to fully qualified name) + string memory contractIdentifier = bytes(newContractPath).length > 0 + ? newContractPath + : string.concat(newContractName, ".sol"); + + // Validate upgrade safety with OpenZeppelin Foundry Upgrades + if (bytes(oldContractName).length > 0) { + console.log(""); + console.log(string.concat(_YELLOW, "Validating upgrade safety with OpenZeppelin Foundry Upgrades...", _RESET)); + Options memory opts; + string memory referenceIdentifier = string.concat(oldContractName, ".sol"); + opts.referenceContract = referenceIdentifier; + Upgrades.validateUpgrade(contractIdentifier, opts); + console.log(string.concat(_BRIGHT_GREEN, "[PASS] OpenZeppelin Foundry Upgrades verification passed!", _RESET)); + console.log(""); + } + + address implementation = vm.deployCode(contractIdentifier); + + if (implementation == address(0)) { + revert("Failed to deploy implementation"); + } + + console.log(""); + console.log(string.concat(_BRIGHT_GREEN, "========================================", _RESET)); + console.log(string.concat(_BRIGHT_GREEN, "Implementation contract deployed successfully!", _RESET)); + console.log(string.concat(_BRIGHT_GREEN, "========================================", _RESET)); + console.log(string.concat(_CYAN, "Contract name:", _RESET), newContractName); + console.log(string.concat(_BRIGHT_CYAN, "Implementation address:", _RESET), implementation); + console.log(string.concat(_BRIGHT_GREEN, "========================================", _RESET)); + + vm.stopBroadcast(); + } +} + +/** + * @notice Anvil-specific variant for local testing + */ +contract DeployMultisigImplAnvil is GenericMultisigUpgrade { + function run() public override { + require(block.chainid == 31337, "must deploy on anvil"); + super.run(); + } +} + +/** + * @notice Holesky-specific variant for testnet + */ +contract DeployMultisigImplHolesky is GenericMultisigUpgrade { + function run() public override { + require(block.chainid == 17000, "must deploy on Holesky"); + super.run(); + } +} + +/** + * @notice Hoodi-specific variant for testnet + */ +contract DeployMultisigImplHoodi is GenericMultisigUpgrade { + function run() public override { + require(block.chainid == 560048, "must deploy on Hoodi"); + super.run(); + } +} + +/** + * @notice Mainnet-specific variant + */ +contract DeployMultisigImplMainnet is GenericMultisigUpgrade { + function run() public override { + require(block.chainid == 1, "must deploy on Mainnet"); + super.run(); + } +} + +/** + * @notice Generic deploy contract (default, works on any chain) + */ +contract DeployMultisigImpl is GenericMultisigUpgrade { + function run() public override { + super.run(); + } +} + diff --git a/contracts/scripts/upgrades/GenericUpgrade.s.sol b/contracts/scripts/upgrades/GenericUpgrade.s.sol new file mode 100644 index 000000000..03d26025f --- /dev/null +++ b/contracts/scripts/upgrades/GenericUpgrade.s.sol @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: BSL 1.1 + +// solhint-disable no-console + +pragma solidity 0.8.26; + +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; +import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol"; +import {Options} from "openzeppelin-foundry-upgrades/Options.sol"; + +/** + * @title GenericUpgrade + * @notice Generic upgrade script that can upgrade any contract. + * + * This script reads configuration from environment variables: + * - OLD_CONTRACT_NAME: Name of the old contract (for logging) + * - NEW_CONTRACT_NAME: Name of the new contract (for logging) + * - NEW_CONTRACT_PATH: Path to the new contract file (e.g., "validator-registry/avs/MevCommitAVSV2.sol") + * - PROXY_ADDRESS: Address of the proxy contract to upgrade + * + * ⚠️ IMPORTANT: Run validation before running this script! + * ./validate-upgrade.sh --contract --reference + * + * Usage: + * forge script scripts/upgrades/GenericUpgrade.s.sol:UpgradeContract --rpc-url --sender --broadcast --verify -vvvv + * + * IMPORTANT: Always use --sender flag with the address that owns the proxy or proxy admin. + */ +contract GenericUpgrade is Script { + // ANSI color codes + string private constant _RESET = "\x1b[0m"; + string private constant _GREEN = "\x1b[32m"; + string private constant _BRIGHT_GREEN = "\x1b[92m"; + string private constant _YELLOW = "\x1b[33m"; + string private constant _CYAN = "\x1b[36m"; + string private constant _BRIGHT_CYAN = "\x1b[96m"; + string private constant _BLUE = "\x1b[34m"; + + function run() public virtual { + vm.startBroadcast(); + + // Get proxy address from environment variable + address proxyAddress = vm.envOr("PROXY_ADDRESS", address(0)); + + if (proxyAddress == address(0)) { + revert("PROXY_ADDRESS must be set"); + } + + // Get contract names and path from environment variables + string memory oldContractName = vm.envOr("OLD_CONTRACT_NAME", string("OldContract")); + string memory newContractName = vm.envOr("NEW_CONTRACT_NAME", string("NewContract")); + string memory newContractPath = vm.envOr("NEW_CONTRACT_PATH", string("")); + + if (bytes(newContractPath).length == 0) { + revert("NEW_CONTRACT_PATH must be set"); + } + + console.log(string.concat(_CYAN, "Upgrading contract on chain:", _RESET), block.chainid); + console.log(string.concat(_CYAN, "Old contract:", _RESET), oldContractName); + console.log(string.concat(_CYAN, "New contract:", _RESET), newContractName); + console.log(string.concat(_CYAN, "New contract path:", _RESET), newContractPath); + console.log(string.concat(_CYAN, "Proxy address:", _RESET), proxyAddress); + console.log(string.concat(_CYAN, "Upgrader address:", _RESET), msg.sender); + + // Validate upgrade safety with OpenZeppelin Foundry Upgrades + if (bytes(oldContractName).length > 0) { + console.log(""); + console.log(string.concat(_YELLOW, "Validating upgrade safety with OpenZeppelin Foundry Upgrades...", _RESET)); + Options memory opts; + // Construct reference contract identifier (just filename, OpenZeppelin will resolve it) + string memory referenceIdentifier = string.concat(oldContractName, ".sol"); + opts.referenceContract = referenceIdentifier; + Upgrades.validateUpgrade(newContractPath, opts); + console.log(string.concat(_BRIGHT_GREEN, "[PASS] OpenZeppelin Foundry Upgrades verification passed!", _RESET)); + console.log(""); + } + + // Upgrade to new contract + // No function call during upgrade by default + // If you need to call a function during upgrade, modify this script or use a custom upgrade script + Upgrades.upgradeProxy( + proxyAddress, + newContractPath, + "" + ); + + vm.stopBroadcast(); + } + + /** + * @notice Alternative entry point that accepts proxy address and contract path as parameters + * @param proxyAddress The address of the proxy contract to upgrade + * @param newContractPath The path to the new contract file + */ + function run(address proxyAddress, string calldata newContractPath) public { + vm.startBroadcast(); + + string memory oldContractName = vm.envOr("OLD_CONTRACT_NAME", string("OldContract")); + string memory newContractName = vm.envOr("NEW_CONTRACT_NAME", string("NewContract")); + + console.log(string.concat(_CYAN, "Upgrading contract on chain:", _RESET), block.chainid); + console.log(string.concat(_CYAN, "Old contract:", _RESET), oldContractName); + console.log(string.concat(_CYAN, "New contract:", _RESET), newContractName); + console.log(string.concat(_CYAN, "New contract path:", _RESET), newContractPath); + console.log(string.concat(_CYAN, "Proxy address:", _RESET), proxyAddress); + console.log(string.concat(_CYAN, "Upgrader address:", _RESET), msg.sender); + + // Validate upgrade safety with OpenZeppelin Foundry Upgrades + if (bytes(oldContractName).length > 0) { + console.log(""); + console.log(string.concat(_YELLOW, "Validating upgrade safety with OpenZeppelin Foundry Upgrades...", _RESET)); + Options memory opts; + string memory referenceIdentifier = string.concat(oldContractName, ".sol"); + opts.referenceContract = referenceIdentifier; + Upgrades.validateUpgrade(newContractPath, opts); + console.log(string.concat(_BRIGHT_GREEN, "[PASS] OpenZeppelin Foundry Upgrades verification passed!", _RESET)); + console.log(""); + } + + // Upgrade to new contract + Upgrades.upgradeProxy( + proxyAddress, + newContractPath, + "" + ); + + vm.stopBroadcast(); + } +} + +/** + * @notice Anvil-specific variant for local testing + */ +contract UpgradeContractAnvil is GenericUpgrade { + function run() public override { + require(block.chainid == 31337, "must upgrade on anvil"); + super.run(); + } +} + +/** + * @notice Holesky-specific variant for testnet + */ +contract UpgradeContractHolesky is GenericUpgrade { + function run() public override { + require(block.chainid == 17000, "must upgrade on Holesky"); + super.run(); + } +} + +/** + * @notice Hoodi-specific variant for testnet + */ +contract UpgradeContractHoodi is GenericUpgrade { + function run() public override { + require(block.chainid == 560048, "must upgrade on Hoodi"); + super.run(); + } +} + +/** + * @notice Mainnet-specific variant + */ +contract UpgradeContractMainnet is GenericUpgrade { + function run() public override { + require(block.chainid == 1, "must upgrade on Mainnet"); + super.run(); + } +} + +/** + * @notice Generic upgrade contract (default, works on any chain) + */ +contract UpgradeContract is GenericUpgrade { + function run() public override { + super.run(); + } +} + diff --git a/contracts/scripts/upgrades/README.md b/contracts/scripts/upgrades/README.md new file mode 100644 index 000000000..c7c14414c --- /dev/null +++ b/contracts/scripts/upgrades/README.md @@ -0,0 +1,416 @@ +# Generic Upgrade Scripts + +## Overview + +This directory contains generic upgrade scripts for upgrading proxy contracts. These scripts are designed to be used exclusively through the `l1-upgrade-cli.sh` command-line interface. + +There are two main scripts: + +- **`GenericUpgrade.s.sol`** - For direct upgrades (EOA-owned contracts) +- **`GenericMultisigUpgrade.s.sol`** - For multisig upgrades (deploys implementation only) + +Both scripts support multiple chains and automatically enforce chain ID checks for safety. + +## Usage + +### Via CLI (Recommended) + +Both scripts are invoked through `l1-upgrade-cli.sh`: + +**Direct Upgrade (EOA-owned contracts):** +```bash +./l1-upgrade-cli.sh upgrade \ + --old-contract MevCommitAVS \ + --new-contract MevCommitAVSV2 \ + --proxy-address 0x1234... \ + --chain mainnet \ + --keystore +``` + +**Multisig Upgrade (multisig-owned contracts):** +```bash +./l1-upgrade-cli.sh upgrade \ + --old-contract MevCommitAVS \ + --new-contract MevCommitAVSV2 \ + --chain mainnet \ + --multisig \ + --keystore +``` + +The CLI script will: +1. Validate the upgrade safety using `validate-upgrade.sh` +2. Find the contract files automatically +3. Set the required environment variables +4. Call the appropriate contract variant from the selected script +5. Execute the upgrade/deployment using forge + +
+GenericUpgrade.s.sol - Direct Upgrades + +### Overview + +`GenericUpgrade.s.sol` performs complete proxy upgrades for contracts owned by a single EOA (Externally Owned Account). It handles both deployment of the new implementation and the upgrade transaction in a single operation. + +### When to Use + +- ✅ Contract is owned by a single EOA wallet +- ✅ You want to perform the upgrade in one transaction +- ✅ You have access to the wallet that owns the proxy or proxy admin + +### How It Works + +1. **Environment Variables**: The script reads configuration from environment variables: + - `PROXY_ADDRESS` - Address of the proxy to upgrade + - `OLD_CONTRACT_NAME` - Name of the old contract (for logging) + - `NEW_CONTRACT_NAME` - Name of the new contract (for logging) + - `NEW_CONTRACT_PATH` - Filename of the new contract (e.g., `MevCommitAVSV2.sol`) + +2. **Upgrade Process**: Uses OpenZeppelin's `Upgrades.upgradeProxy()` to: + - Deploy the new implementation contract + - Upgrade the proxy to point to the new implementation + - Validate storage layout compatibility (if annotation is present) + +3. **Contract Path Format**: The `NEW_CONTRACT_PATH` should be just the filename (e.g., `MevCommitAVSV2.sol`), not a full path. The library will find the artifact in `out/` automatically. + +### Contract Variants + +- **`UpgradeContract`** - Generic variant (works on any chain) +- **`UpgradeContractAnvil`** - Anvil local testing (chain ID 31337) +- **`UpgradeContractHolesky`** - Holesky testnet (chain ID 17000) +- **`UpgradeContractHoodi`** - Hoodi testnet (chain ID 560048) +- **`UpgradeContractMainnet`** - Mainnet (chain ID 1) + +### Direct Usage (Not Recommended) + +While you can call the script directly with forge, it's not recommended as you'll need to manually set all environment variables: + +```bash +# Required environment variables +export PROXY_ADDRESS="0x1234..." +export OLD_CONTRACT_NAME="MevCommitAVS" +export NEW_CONTRACT_NAME="MevCommitAVSV2" +export NEW_CONTRACT_PATH="MevCommitAVSV2.sol" +export RPC_URL="..." +export SENDER="..." + +forge script scripts/upgrades/GenericUpgrade.s.sol:UpgradeContractMainnet \ + --rpc-url $RPC_URL \ + --sender $SENDER \ + --broadcast +``` + +### Examples + +**Anvil (Local Testing):** +```bash +export RPC_URL="http://127.0.0.1:8545" + +./l1-upgrade-cli.sh upgrade \ + --old-contract MevCommitAVS \ + --new-contract MevCommitAVSV2 \ + --proxy-address 0xc5a5c42992decbae36851359345fe25997f5c42d \ + --chain anvil \ + --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \ + --skip-validation # Optional: skip validation for faster local testing +``` + +**Mainnet:** +```bash +./l1-upgrade-cli.sh upgrade \ + --old-contract MevCommitAVS \ + --new-contract MevCommitAVSV2 \ + --proxy-address 0x1234567890123456789012345678901234567890 \ + --chain mainnet \ + --keystore \ + --priority-gas-price 2000000000 \ + --with-gas-price 5000000000 +``` + +**Holesky Testnet:** +```bash +./l1-upgrade-cli.sh upgrade \ + --old-contract MevCommitAVS \ + --new-contract MevCommitAVSV2 \ + --proxy-address 0x5678901234567890123456789012345678901234 \ + --chain holesky \ + --ledger +``` + +
+ +
+GenericMultisigUpgrade.s.sol - Multisig Upgrades + +### Overview + +`GenericMultisigUpgrade.s.sol` deploys **only** the implementation contract for multisig-owned proxy upgrades. After deployment, you must use your multisig UI (e.g., Safe wallet) to call `upgradeToAndCall()` on the proxy contract. + +### When to Use + +- ✅ Contract is owned by a multisig wallet +- ✅ You need to deploy the implementation separately before multisig approval +- ✅ You want to prepare the implementation address for multisig transaction submission + +### How It Works + +1. **Environment Variables**: The script reads configuration from environment variables: + - `NEW_CONTRACT_NAME` - Name of the new contract (e.g., "MevCommitAVSV2") + - `NEW_CONTRACT_PATH` - Path to the new contract file (e.g., "MevCommitAVSV2.sol") - used for logging only + +2. **Deployment Process**: Uses `vm.deployCode()` to: + - Deploy the new implementation contract bytecode + - **Does NOT** call any constructor or initializer + - **Does NOT** upgrade the proxy (that's done via multisig) + +3. **Output**: The script outputs the implementation address, which you then use in your multisig UI to call `upgradeToAndCall(implementation, callData)`. + +### Contract Variants + +- **`DeployMultisigImpl`** - Generic variant (works on any chain) +- **`DeployMultisigImplAnvil`** - Anvil local testing (chain ID 31337) +- **`DeployMultisigImplHolesky`** - Holesky testnet (chain ID 17000) +- **`DeployMultisigImplHoodi`** - Hoodi testnet (chain ID 560048) +- **`DeployMultisigImplMainnet`** - Mainnet (chain ID 1) + +### Direct Usage (Not Recommended) + +While you can call the script directly with forge, it's not recommended as you'll need to manually set all environment variables: + +```bash +# Required environment variables +export NEW_CONTRACT_NAME="MevCommitAVSV2" +export NEW_CONTRACT_PATH="MevCommitAVSV2.sol" +export RPC_URL="..." +export SENDER="..." + +forge script scripts/upgrades/GenericMultisigUpgrade.s.sol:DeployMultisigImplMainnet \ + --rpc-url $RPC_URL \ + --sender $SENDER \ + --broadcast \ + --verify +``` + +### Examples + +**Anvil (Local Testing):** +```bash +export RPC_URL="http://127.0.0.1:8545" + +./l1-upgrade-cli.sh upgrade \ + --old-contract MevCommitAVS \ + --new-contract MevCommitAVSV2 \ + --chain anvil \ + --multisig \ + --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \ + --skip-validation # Optional: skip validation for faster local testing +``` + +**Mainnet:** +```bash +./l1-upgrade-cli.sh upgrade \ + --old-contract MevCommitAVS \ + --new-contract MevCommitAVSV2 \ + --chain mainnet \ + --multisig \ + --keystore +``` + +**After Deployment:** + +Once the implementation is deployed, use your multisig UI to complete the upgrade: + +1. Copy the implementation address from the script output +2. In your multisig UI (e.g., Safe wallet), create a new transaction to the proxy contract +3. Call function: `upgradeToAndCall(implementation, callData)` + - `implementation`: The address shown in the script output + - `callData`: Empty bytes (`"0x"`) if no initialization needed, or encoded function call if needed +4. Submit the transaction through your multisig workflow + +### Important Notes + +- The deployer address can be any account (doesn't need to be the multisig) +- The implementation contract serves only as a blueprint and has no ownership +- **Manual validation required**: The multisig workflow bypasses automatic safety checks. Always manually validate using `validate-upgrade.sh` before deploying + +
+ +## Prerequisites + +Before running any upgrade: + +1. **Validation**: Always validate the upgrade using `validate-upgrade.sh`: + ```bash + ./validate-upgrade.sh --contract MevCommitAVSV2 --reference MevCommitAVS + ``` + +2. **Contract Annotation**: Ensure the new contract has the proper annotation: + ```solidity + /// @custom:oz-upgrades-from MevCommitAVS + contract MevCommitAVSV2 is ... + ``` + +3. **Build**: Contracts must be compiled: + ```bash + forge clean && forge build + ``` + +## Important Notes + +- **Always use the CLI**: The `l1-upgrade-cli.sh` script handles all the setup, validation, and path construction automatically +- **Never skip validation on mainnet**: Always run validation before upgrading on mainnet +- **Contract filename only**: The contract path should be just the filename (e.g., `MevCommitAVSV2.sol`), the CLI extracts this automatically +- **Chain-specific variants**: Each chain has its own contract variant that enforces chain ID checks for safety +- **Multisig validation**: For multisig upgrades, manual validation is critical since automatic checks are bypassed + +
+Upgrade Checklist + +> **After completing any upgrade, immediately record it in the "Upgrade History" table in the main README.md.** + +### Phase 1: Implementing the Feat/Fix + +**Initial Implementation:** +- [ ] Feat/fix implemented and merged to `main` branch +- [ ] Upgrade branch created from appropriate release branch (see Current Deployments in README.md) +- [ ] Currently deployed contract implementation copied to new versioned file (e.g., `MevCommitAVSV2.sol`) +- [ ] New implementation contract updated with feat/fix from main (cherry-pick or manual re-implementation) + +**Storage Contract Versioning (if applicable):** +- [ ] Storage contract changes identified +- [ ] New storage contract created with incremented version (e.g., `MevCommitAVSStorageV2.sol`) +- [ ] New implementation contract inherits from new storage contract +- [ ] Old storage contract moved to `contracts/upgrades/[feature-folder]/` + +**Contract Organization:** +- [ ] Old contract moved to `contracts/upgrades/[feature-folder]/` +- [ ] `/// @custom:oz-upgrades-from` annotation added to new contract +- [ ] Reference contract properly defined (e.g., `/// @custom:oz-upgrades-from MevCommitAVS`) + +**Testing:** +- [ ] Tests updated on upgrade branch +- [ ] `setUp()` function includes upgrade from previous to new implementation +- [ ] Regression tests from main ported to upgrade branch +- [ ] All tests passing on upgrade branch + +**Validation & Safety (CRITICAL):** +- [ ] Contracts built: `forge clean && forge build` +- [ ] ✅ **UPGRADE VALIDATION PASSED**: `npx @openzeppelin/upgrades-core validate --contract ContractV2 --reference ContractV1` - **DO NOT PROCEED WITHOUT THIS** +- [ ] Validation failures addressed or upgrade deemed not possible/appropriate + +**ABI Changes (if applicable):** +- [ ] ABI changes identified and documented +- [ ] New ABI file and go bindings generated (if needed) +- [ ] Considered avoiding ABI changes if possible + +### Phase 2: Deployment Preparation + +**Branch Management:** +- [ ] Upgrade branch reviewed and merged into release branch +- [ ] Latest commit from release branch tagged (tag will populate Upgrade History table) +- [ ] Tag verified and documented + +**Environment Setup:** +- [ ] `RPC_URL` environment variable set for target chain +- [ ] Wallet type selected (keystore/ledger/trezor/private-key) + +**For Keystore Wallet:** +- [ ] `KEYSTORES` environment variable set +- [ ] `KEYSTORE_PASSWORD` environment variable set +- [ ] `SENDER` environment variable set +- [ ] `ETHERSCAN_API_KEY` set (optional, for verification) + +**For Ledger/Trezor Wallet:** +- [ ] `HD_PATHS` environment variable set +- [ ] `SENDER` environment variable set +- [ ] Hardware wallet connected and ready + +**For Private Key (Anvil/Local Testing):** +- [ ] Private key prepared (or using default anvil key) +- [ ] `SENDER` set (optional) + +**Deployment Parameters:** +- [ ] Proxy address confirmed (from Current Deployments table) +- [ ] Old contract name confirmed +- [ ] New contract name confirmed +- [ ] Chain selected (`mainnet`, `holesky`, `hoodi`, or `anvil`) +- [ ] Gas price parameters set (if needed): `--priority-gas-price` and/or `--with-gas-price` +- [ ] Backup and rollback plan documented + +### Phase 3: Testing on Anvil (Recommended) + +**Local Testing:** +- [ ] Anvil chain running locally +- [ ] Local upgrade tested using `l1-upgrade-cli.sh`: + ```bash + ./l1-upgrade-cli.sh upgrade \ + --old-contract ContractV1 \ + --new-contract ContractV2 \ + --proxy-address \ + --chain anvil \ + --private-key \ + --skip-validation # Optional for faster local testing + ``` +- [ ] Pre-upgrade state verified +- [ ] Post-upgrade state verified +- [ ] Functionality tested after upgrade + +### Phase 4: Production Deployment + +**For EOA-owned Contracts (using l1-upgrade-cli.sh):** +- [ ] Production environment variables verified +- [ ] RPC URL chain ID matches target chain +- [ ] Upgrade command prepared: + ```bash + ./l1-upgrade-cli.sh upgrade \ + --old-contract ContractV1 \ + --new-contract ContractV2 \ + --proxy-address \ + --chain \ + \ + [--priority-gas-price ] \ + [--with-gas-price ] + ``` +- [ ] Validation will run automatically (or `--skip-validation` explicitly approved for non-mainnet) +- [ ] Upgrade executed successfully +- [ ] Upgrade transaction verified on block explorer +- [ ] Contract verification completed (if ETHERSCAN_API_KEY provided) + +**For Multisig-owned Contracts:** +- [ ] ✅ **UPGRADE VALIDATION PASSED**: `npx @openzeppelin/upgrades-core validate --contract ContractV2 --reference ContractV1` - **MANUAL VALIDATION REQUIRED** +- [ ] Implementation deployment command prepared: + ```bash + ./l1-upgrade-cli.sh upgrade \ + --old-contract ContractV1 \ + --new-contract ContractV2 \ + --chain \ + --multisig \ + + ``` +- [ ] New implementation contract deployed +- [ ] Implementation address documented +- [ ] Multisig UI prepared with `upgradeToAndCall(newImplAddr, callData)` transaction +- [ ] All multisig signers informed and ready +- [ ] Upgrade transaction submitted via multisig +- [ ] Multisig transaction executed successfully + +### Phase 5: Post-Deployment + +**Documentation:** +- [ ] Upgrade recorded in "Upgrade History" table in README.md +- [ ] Timestamp (UTC) recorded +- [ ] Upgrade tag recorded +- [ ] Any notes or observations documented + +**Verification:** +- [ ] Proxy address still points to correct contract +- [ ] New implementation contract address verified +- [ ] Contract functions tested post-upgrade +- [ ] Monitoring and alerts verified (if applicable) + +
+ +## Related Documentation + +- [l1-upgrade-cli.sh](../l1-upgrade-cli.sh) - The CLI script that uses these upgrade scripts +- [Contract Upgrade Structure](../../contracts/upgrades/docs/CONTRACT_UPGRADE_STRUCTURE.md) - Documentation on contract versioning structure +- [validate-upgrade.sh](../../validate-upgrade.sh) - Validation script for upgrade safety From 52bf794ff9f2e066226f3f6189e007a120eaaa00 Mon Sep 17 00:00:00 2001 From: Jason Schwarz Date: Tue, 4 Nov 2025 18:21:00 -0400 Subject: [PATCH 04/10] refactor: update the contract README --- contracts/README.md | 93 +++++++++++++++++++++++++++++++++------------ 1 file changed, 68 insertions(+), 25 deletions(-) diff --git a/contracts/README.md b/contracts/README.md index 29fdb414f..82895bfac 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -111,7 +111,7 @@ To avoid issues with etherscan verification, use a non-public RPC that can suppo > **After completing any upgrade, immediately record it in the “Upgrade History” table above.** -Contract upgrades are not always possible, as there are [strict limitations as enforced by Solidity](https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable#modifying-your-contracts). When a contract feat/fix cannot be implemented as a contract upgrade, simply PR the changes into main, and release/deploy a new contract instance as needed. +Contract upgrades are not always possible, as there are [strict limitations as enforced by Solidity](https://docs.openzeppelin.com/upgrades-plugins/writing-upgradeable#modifying-your-contracts). When a contract feat/fix cannot be implemented as a contract upgrade, simply PR the changes into main, and release/deploy a new contract instance as needed. See [#360](https://github.com/primev/mev-commit/pull/360) for a reference example of a complete contract upgrade. @@ -133,9 +133,18 @@ If the feat/fix required changes to the storage contract (see limitations above) Example: `MevCommitAVSV2.sol` would inherit `MevCommitAVSV2Storage.sol`. -Now [define the reference contract](https://docs.openzeppelin.com/upgrades-plugins/1.x/api-core#define-reference-contracts) for the upgrade right above the new contract implementation. E.g `/// @custom:oz-upgrades-from MevCommitAVS`. +Now [define the reference contract](https://docs.openzeppelin.com/upgrades-plugins/api-core#define-reference-contracts) for the upgrade right above the new contract implementation. E.g `/// @custom:oz-upgrades-from MevCommitAVS`. -Finally, build the contracts and use [openzeppelin's cli](https://docs.openzeppelin.com/upgrades-plugins/1.x/api-core#usage) to validate the upgrade, similar to `npx @openzeppelin/upgrades-core validate --contract MevCommitAVSV2`. If this command fails, you'll need to address whether a contract upgrade is still possible/appropriate. +Finally, build the contracts and validate the upgrade. + +```bash +forge clean && forge build +npx @openzeppelin/upgrades-core validate --contract MevCommitAVSV2 --reference MevCommitAVS +``` + +If validation fails, you'll need to address whether a contract upgrade is still possible/appropriate. + +**Note:** The `l1-upgrade-cli.sh` script automatically runs validation before executing the upgrade (unless `--skip-validation` is used), so you don't need to run it manually if using the CLI. However, it's recommended to validate during development to catch issues early. ### Note on ABI changes @@ -147,9 +156,32 @@ If possible it's recommended to avoid changing a contract's ABI for an upgrade. Once your "upgrade branch" is reviewed and merged into the release branch, tag the latest commit from the release branch. This tag will be used to populate the **Upgrade History** table above. -Invoking the upgrade involves creating a script in which a new implementation contract is deployed, then calling `upgradeToAndCall` on the proxy contract, passing in the address of the new implementation contract. +The upgrade is performed via the [l1-upgrade-cli.sh](./l1-upgrade-cli.sh) script. This CLI tool automates the upgrade process, handles validation, and supports multiple wallet types and chains. + +#### Using l1-upgrade-cli.sh (Recommended) + +The `l1-upgrade-cli.sh` script provides a streamlined way to upgrade contracts. It automatically: +- Validates upgrade safety (unless `--skip-validation` is used) +- Finds contract files automatically +- Handles chain-specific configurations +- Supports multiple wallet types (keystore, ledger, trezor, private-key) + +**Basic Usage:** + +```bash +./l1-upgrade-cli.sh upgrade \ + --old-contract MevCommitAVS \ + --new-contract MevCommitAVSV2 \ + --proxy-address 0x1234... \ + --chain mainnet \ + --keystore +``` + + + +#### Manual Upgrade Scripts (Advanced) -See example below +For custom upgrade logic or multisig scenarios, you can create manual upgrade scripts. See example below: ```solidity pragma solidity 0.8.26; @@ -188,33 +220,44 @@ contract UpgradeAnvil is UpgradeAVS { } ``` -In this example, no function call is made during the upgrade. However [these examples](https://docs.openzeppelin.com/upgrades-plugins/1.x/foundry-upgrades#examples) demonstrate how to make a function call during the upgrade. +In this example, no function call is made during the upgrade. However [these examples](https://docs.openzeppelin.com/upgrades-plugins/foundry/foundry-upgrades#examples) demonstrate how to make a function call during the upgrade. -It's encouraged to test your upgrade process using anvil, then use identical code to invoke the upgrade on Holesky/mainnet. +It's encouraged to test your upgrade process using anvil, then use the `l1-upgrade-cli.sh` to invoke the upgrade on Holesky/mainnet. ### Note on multisig vs EOA -The aforementioned process can be followed exactly for contracts that are owned by a single EOA, where the forge script can be run directly using a keystore. +**For EOA-owned contracts:** -For contracts that are owned by a multisig, simply deploy an uninitialized new implementation contract from any account using a forge script etc., +The `l1-upgrade-cli.sh` script handles the complete upgrade process for contracts owned by a single EOA. You can use keystore, ledger, trezor, or private-key wallet options as described above. -```solidity -import {MevCommitAVSV2} from "../../../contracts/validator-registry/avs/MevCommitAVSV2.sol"; -import {Script} from "forge-std/Script.sol"; -import {console} from "forge-std/console.sol"; +**For multisig-owned contracts:** -contract DeployNewImpl is Script { - function run() external { - vm.startBroadcast(); - MevCommitAVSV2 newImplementation = new MevCommitAVSV2(); - console.log("Deployed new implementation contract at address: ", address(newImplementation)); - vm.stopBroadcast(); - } -} -``` +Contracts owned by a multisig require a different approach since the multisig must approve the upgrade transaction. The process is: + +1. **Validate the upgrade** (critical step): + ```bash + ./validate-upgrade.sh --contract MevCommitAVSV2 --reference MevCommitAVS + ``` + +2. **Deploy the new implementation contract** from any account. You can use a simple forge script: + + ```solidity + import {MevCommitAVSV2} from "../../../contracts/validator-registry/avs/MevCommitAVSV2.sol"; + import {Script} from "forge-std/Script.sol"; + import {console} from "forge-std/console.sol"; + + contract DeployNewImpl is Script { + function run() external { + vm.startBroadcast(); + MevCommitAVSV2 newImplementation = new MevCommitAVSV2(); + console.log("Deployed new implementation contract at address: ", address(newImplementation)); + vm.stopBroadcast(); + } + } + ``` -Then call the `upgradeToAndCall(newImplAddr, callData)` function directly on the proxy contract using your multisig UI (e.g Safe wallet). +3. **Call `upgradeToAndCall(newImplAddr, callData)`** directly on the proxy contract using your multisig UI (e.g., Safe wallet). -Ownership of the implementation contract itself would be irrelevant in this scenario, as the implementation is deployed without calling its initializer, or setting any state. Ie. the implementation contract serves only as a blueprint for state transition functionality. +Ownership of the implementation contract itself is irrelevant in this scenario, as the implementation is deployed without calling its initializer or setting any state. The implementation contract serves only as a blueprint for state transition functionality. -The multisig option bypasses safety checks that would otherwise happen by using a forge script in tandem with [OpenZeppelin Foundry Upgrades](https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades). Therefore it's very important to use the `validate` command from above to ensure the upgrade is safe to proceed with. +**Important:** The multisig workflow bypasses the automatic safety checks that `l1-upgrade-cli.sh` performs. Therefore, it's essential to manually validate the upgrade using `validate-upgrade.sh` before proceeding. See the [validation section](#implementing-the-featfix) above for details. From df8f7d76f1a191c1ca7def30abec3e4ed87b8cdb Mon Sep 17 00:00:00 2001 From: Jason Schwarz Date: Wed, 5 Nov 2025 17:08:37 -0400 Subject: [PATCH 05/10] feat: add validate-upgrade script --- contracts/validate-upgrade.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/validate-upgrade.sh b/contracts/validate-upgrade.sh index 1541e8116..7956c2a88 100755 --- a/contracts/validate-upgrade.sh +++ b/contracts/validate-upgrade.sh @@ -103,3 +103,5 @@ else exit 1 fi + + From cc0ee6a4e8f9226c9cb9a548a98c38519080a0c2 Mon Sep 17 00:00:00 2001 From: Jason Schwarz Date: Wed, 5 Nov 2025 17:09:12 -0400 Subject: [PATCH 06/10] refactor: remove Anvil support --- contracts/l1-upgrade-cli.sh | 82 +++++-------------------------------- 1 file changed, 10 insertions(+), 72 deletions(-) diff --git a/contracts/l1-upgrade-cli.sh b/contracts/l1-upgrade-cli.sh index d6f04afa4..e03a36506 100755 --- a/contracts/l1-upgrade-cli.sh +++ b/contracts/l1-upgrade-cli.sh @@ -7,7 +7,6 @@ old_contract="" new_contract="" proxy_address="" wallet_type="" -private_key="" chain="" chain_id=0 upgrade_script="" @@ -27,13 +26,12 @@ help() { echo " --old-contract, -o Name of the old contract version (e.g., MevCommitAVS)." echo " --new-contract, -n Name of the new contract version (e.g., MevCommitAVSV2)." echo " --proxy-address, -p Address of the proxy contract to upgrade (not required with --multisig)." - echo " --chain, -c Specify the chain to upgrade on ('mainnet', 'holesky', 'hoodi', or 'anvil')." + echo " --chain, -c Specify the chain to upgrade on ('mainnet', 'holesky', or 'hoodi')." echo - echo "Wallet Options (one required, except for anvil where --private-key is recommended):" + echo "Wallet Options (one required):" echo " --keystore Use a keystore for upgrade." echo " --ledger Use a Ledger hardware wallet for upgrade." echo " --trezor Use a Trezor hardware wallet for upgrade." - echo " --private-key Use a private key for upgrade (useful for anvil/local testing)." echo echo "Optional Options:" echo " --multisig Deploy implementation contract only (for multisig upgrades)." @@ -62,17 +60,12 @@ help() { echo " SENDER Address of the sender." echo " RPC_URL RPC URL for the upgrade chain." echo - echo " For Private Key (--private-key option):" - echo " SENDER Address of the sender (optional, derived from private key if not set)." - echo " RPC_URL RPC URL for the upgrade chain." - echo echo " Optional:" echo " PROXY_ADDRESS Proxy address (can be provided via --proxy-address instead)." echo echo "Examples:" echo " $0 upgrade --old-contract MevCommitAVS --new-contract MevCommitAVSV2 --proxy-address 0x1234... --chain mainnet --keystore" echo " $0 upgrade --old-contract ProviderRegistry --new-contract ProviderRegistryV2 --proxy-address 0x5678... --chain holesky --ledger --priority-gas-price 2000000000" - echo " $0 upgrade --old-contract MevCommitAVS --new-contract MevCommitAVSV2 --proxy-address 0x1234... --chain anvil --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" echo " $0 upgrade --old-contract MevCommitAVS --new-contract MevCommitAVSV2 --chain mainnet --multisig --keystore" exit 1 } @@ -147,8 +140,8 @@ parse_args() { exit 1 fi chain="$2" - if [[ "$chain" != "mainnet" && "$chain" != "holesky" && "$chain" != "hoodi" && "$chain" != "anvil" ]]; then - echo "Error: Unknown chain '$chain'. Valid options are 'mainnet', 'holesky', 'hoodi', or 'anvil'." + if [[ "$chain" != "mainnet" && "$chain" != "holesky" && "$chain" != "hoodi" ]]; then + echo "Error: Unknown chain '$chain'. Valid options are 'mainnet', 'holesky', or 'hoodi'." exit 1 fi shift 2 @@ -185,19 +178,6 @@ parse_args() { wallet_type="trezor" shift ;; - --private-key) - if [[ -z "$2" ]]; then - echo "Error: --private-key requires an argument." - exit 1 - fi - if [[ -n "$wallet_type" ]]; then - echo "Error: Multiple wallet types specified. Please specify only one wallet option." - exit 1 - fi - wallet_type="private-key" - private_key="$2" - shift 2 - ;; --priority-gas-price) if [[ -z "$2" ]]; then echo "Error: --priority-gas-price requires an argument." @@ -229,9 +209,8 @@ parse_args() { usage fi - if [[ -z "$wallet_type" && "$chain" != "anvil" ]]; then - echo "Error: A wallet option is required. Please specify one of --keystore, --ledger, --trezor, or --private-key." - echo "Note: For anvil, --private-key is recommended but not required." + if [[ -z "$wallet_type" ]]; then + echo "Error: A wallet option is required. Please specify one of --keystore, --ledger, or --trezor." usage fi @@ -274,16 +253,6 @@ check_env_variables() { # ETHERSCAN_API_KEY is optional for upgrades but recommended for verification elif [[ "$wallet_type" == "ledger" || "$wallet_type" == "trezor" ]]; then required_vars+=("HD_PATHS" "SENDER") - elif [[ "$wallet_type" == "private-key" ]]; then - # SENDER is optional for private-key, can be derived from the key - # But we'll still require it for consistency unless on anvil - if [[ "$chain" != "anvil" ]]; then - required_vars+=("SENDER") - fi - elif [[ -z "$wallet_type" && "$chain" == "anvil" ]]; then - # For anvil without explicit wallet, we'll use private-key from env or default - # SENDER is optional - : fi for var in "${required_vars[@]}"; do @@ -321,22 +290,10 @@ get_chain_params() { else upgrade_script="UpgradeContractHoodi" fi - elif [[ "$chain" == "anvil" ]]; then - chain_id=31337 - if [[ "$multisig_flag" == true ]]; then - upgrade_script="DeployMultisigImplAnvil" - else - upgrade_script="UpgradeContractAnvil" - fi fi } check_git_status() { - # Skip git checks for anvil (local testing) - if [[ "$chain" == "anvil" ]]; then - return - fi - if [[ ${chain_id:-0} -eq 1 ]]; then if ! current_tag=$(git describe --tags --exact-match 2>/dev/null); then echo "Error: Current commit is not tagged. Please ensure the commit is tagged before upgrading on mainnet." @@ -364,8 +321,7 @@ check_rpc_url() { exit 1 fi - # Skip RPC URL warning for anvil (local testing) - if [[ "$chain" != "anvil" && "$RPC_URL" != *"alchemy"* && "$RPC_URL" != *"infura"* ]]; then + if [[ "$RPC_URL" != *"alchemy"* && "$RPC_URL" != *"infura"* ]]; then echo "Warning: You may be using a public rate-limited RPC URL. Contract verification may fail." read -p "Do you want to continue? (y/n) " -n 1 -r echo @@ -437,8 +393,8 @@ upgrade_contract() { forge_args+=("--use" "0.8.26") forge_args+=("--broadcast") - # Add verification if ETHERSCAN_API_KEY is set (and not anvil) - if [[ -n "${ETHERSCAN_API_KEY:-}" && "$chain" != "anvil" ]]; then + # Add verification if ETHERSCAN_API_KEY is set + if [[ -n "${ETHERSCAN_API_KEY:-}" ]]; then forge_args+=("--verify") fi @@ -466,24 +422,6 @@ upgrade_contract() { forge_args+=("--trezor") forge_args+=("--hd-paths" "${HD_PATHS}") forge_args+=("--sender" "${SENDER}") - elif [[ "$wallet_type" == "private-key" ]]; then - forge_args+=("--private-key" "${private_key}") - if [[ -n "${SENDER:-}" ]]; then - forge_args+=("--sender" "${SENDER}") - fi - elif [[ -z "$wallet_type" && "$chain" == "anvil" ]]; then - # For anvil without explicit wallet, try to use private key from env or default anvil key - if [[ -n "${PRIVATE_KEY:-}" ]]; then - forge_args+=("--private-key" "${PRIVATE_KEY}") - elif [[ -n "${private_key:-}" ]]; then - forge_args+=("--private-key" "${private_key}") - else - # Use default anvil private key (first account) - forge_args+=("--private-key" "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80") - fi - if [[ -n "${SENDER:-}" ]]; then - forge_args+=("--sender" "${SENDER}") - fi fi # Pass contract names and paths as environment variables @@ -496,7 +434,7 @@ upgrade_contract() { export PROXY_ADDRESS="$proxy_address" fi - local wallet_desc="${wallet_type:-private-key (anvil default)}" + local wallet_desc="${wallet_type}" if forge "${forge_args[@]}"; then if [[ "$multisig_flag" == true ]]; then From 8e5857b14371f9ad0b4129fad1e0dea4c5b26bd9 Mon Sep 17 00:00:00 2001 From: Jason Schwarz Date: Wed, 5 Nov 2025 19:16:51 -0400 Subject: [PATCH 07/10] fix: CI pipeline error --- p2p/pkg/rpc/provider/service.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/p2p/pkg/rpc/provider/service.go b/p2p/pkg/rpc/provider/service.go index 6546061ab..1ec5efc14 100644 --- a/p2p/pkg/rpc/provider/service.go +++ b/p2p/pkg/rpc/provider/service.go @@ -649,7 +649,12 @@ func (s *Service) GetDecryptedTransaction( if err != nil { return nil, status.Errorf(codes.Internal, "failed to call shutter sequencer: %v", err) } - defer resp.Body.Close() + + defer func() { + if cerr := resp.Body.Close(); cerr != nil { + s.logger.Error("closing shutter sequencer response body", "err", cerr) + } + }() body, err := io.ReadAll(resp.Body) if err != nil { From 905a24eb6a699a9fb94e36f353a3719299036e1d Mon Sep 17 00:00:00 2001 From: Jason Schwarz Date: Wed, 5 Nov 2025 19:54:17 -0400 Subject: [PATCH 08/10] chore: remove ci fix attempt --- p2p/pkg/rpc/provider/service.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/p2p/pkg/rpc/provider/service.go b/p2p/pkg/rpc/provider/service.go index 1ec5efc14..6546061ab 100644 --- a/p2p/pkg/rpc/provider/service.go +++ b/p2p/pkg/rpc/provider/service.go @@ -649,12 +649,7 @@ func (s *Service) GetDecryptedTransaction( if err != nil { return nil, status.Errorf(codes.Internal, "failed to call shutter sequencer: %v", err) } - - defer func() { - if cerr := resp.Body.Close(); cerr != nil { - s.logger.Error("closing shutter sequencer response body", "err", cerr) - } - }() + defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { From c78792d2276e7f2f63734aba694a169268c160f3 Mon Sep 17 00:00:00 2001 From: Jason Schwarz Date: Thu, 6 Nov 2025 09:12:25 -0400 Subject: [PATCH 09/10] fix: CI pipeline error --- p2p/pkg/rpc/provider/service.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/p2p/pkg/rpc/provider/service.go b/p2p/pkg/rpc/provider/service.go index 6546061ab..421028ec9 100644 --- a/p2p/pkg/rpc/provider/service.go +++ b/p2p/pkg/rpc/provider/service.go @@ -649,7 +649,10 @@ func (s *Service) GetDecryptedTransaction( if err != nil { return nil, status.Errorf(codes.Internal, "failed to call shutter sequencer: %v", err) } - defer resp.Body.Close() + + defer func() { + _ = resp.Body.Close() + }() body, err := io.ReadAll(resp.Body) if err != nil { From 98aa4cf9675f5f30931825c0b390f1ed94f05b74 Mon Sep 17 00:00:00 2001 From: Jason Schwarz Date: Thu, 6 Nov 2025 11:04:48 -0400 Subject: [PATCH 10/10] fix: Gofmt CI error --- p2p/pkg/rpc/provider/service.go | 1 - 1 file changed, 1 deletion(-) diff --git a/p2p/pkg/rpc/provider/service.go b/p2p/pkg/rpc/provider/service.go index 421028ec9..713e5ba57 100644 --- a/p2p/pkg/rpc/provider/service.go +++ b/p2p/pkg/rpc/provider/service.go @@ -649,7 +649,6 @@ func (s *Service) GetDecryptedTransaction( if err != nil { return nil, status.Errorf(codes.Internal, "failed to call shutter sequencer: %v", err) } - defer func() { _ = resp.Body.Close() }()