From e60363ecc3e7ec3b1f9028b71d4703a0171fd995 Mon Sep 17 00:00:00 2001 From: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Date: Mon, 16 Mar 2026 10:55:47 +0100 Subject: [PATCH] fix: contracts upgrade environment support and deploy fixes (#473) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - **Add `--environment` option to `contracts upgrade` command**, aligning it with `deploy`, `verify`, and other contracts subcommands. Deployment file lookups now use `buildNetworkId(chain, environment)` (e.g. `stagenet-hoodi.json`). - **Fix ProxyAdmin address written as `address(0)` in deployment JSON for live deployments.** `_createServiceManagerProxy` now returns the actual ProxyAdmin so it propagates to `_outputDeployedAddresses`. - **Fix ProxyAdmin ownership transfer using wrong address in `DeployLive`.** `transferOwnership` now uses `params.avsOwner` (from `AVS_OWNER_ADDRESS`) instead of `_avsOwner` (from `AVS_OWNER_PRIVATE_KEY` with Anvil default fallback). - **Add ProxyAdmin to the `contracts verify` list** so it is verified on block explorers. - **Log AVS owner address** before the proxy upgrade transaction for signer verification. - **Handle forge receipt-fetch failures gracefully** during proxy upgrades — downgrade to a warning with the tx hash instead of a hard error when the RPC returns null for a receipt. ## Stagenet upgrade to v0.20.0 AVS contracts were upgraded to version v0.20.0 on Stagenet (Hoodi). Updated contract addresses: | Contract | Address | |---|---| | ServiceManager (Proxy) | `0xED73cCaF067cebC706B2B3a6cf2b9af2c696c6d3` | | ServiceManagerImplementation | `0x0Af4a129D0F3d57B5bD51CAB323EA114C28c064a` | | ProxyAdmin | `0xeb1a705e1aa96e6a6329d8a8eb0f5ec38eb7b69d` | | BeefyClient | `0xE65dc4eCA2Fd428361076e1f204731224CeB4292` | | AgentExecutor | `0x35d3FdCB19A246a1763421168dF69dA3dE207063` | | Gateway | `0xE9352f1488F12bFEd722c133C129ca5F467463d1` | | RewardsAgent | `0x2E039a88838241d1Ac738cf2e3C5763ba12571e7` | | DelegationManager | `0x867837a9722C512e0862d8c2E15b8bE220E8b87d` | | StrategyManager | `0xeE45e76ddbEDdA2918b8C7E3035cd37Eab3b5D41` | | AVSDirectory | `0xD58f6844f79eB1fbd9f7091d05f7cb30d3363926` | | RewardsCoordinator | `0x29e8572678e0c272350aa0b4B8f304E47EBcd5e7` | | AllocationManager | `0x95a7431400F362F3647a69535C5666cA0133CAA0` | | PermissionController | `0xdcCF401fD121d8C542E96BC1d0078884422aFAD2` | Co-authored-by: Claude Opus 4.6 Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> --- contracts/deployments/stagenet-hoodi.json | 8 ++-- contracts/script/deploy/DeployBase.s.sol | 15 +++--- contracts/script/deploy/DeployLive.s.sol | 6 +-- contracts/script/deploy/DeployLocal.s.sol | 4 +- test/cli/handlers/contracts/upgrade.ts | 58 ++++++++++++++++------- test/cli/handlers/contracts/verify.ts | 15 +++++- test/cli/index.ts | 15 +++++- 7 files changed, 86 insertions(+), 35 deletions(-) diff --git a/contracts/deployments/stagenet-hoodi.json b/contracts/deployments/stagenet-hoodi.json index aea20210..1ff70446 100644 --- a/contracts/deployments/stagenet-hoodi.json +++ b/contracts/deployments/stagenet-hoodi.json @@ -4,13 +4,13 @@ "AgentExecutor": "0x35d3FdCB19A246a1763421168dF69dA3dE207063", "Gateway": "0xE9352f1488F12bFEd722c133C129ca5F467463d1", "ServiceManager": "0xED73cCaF067cebC706B2B3a6cf2b9af2c696c6d3", - "ServiceManagerImplementation": "0x5E1DA2eE025Dac2F8c391Ac86ebA20bd34c32465", - "ProxyAdmin": "0xeb1a705e1aa96e6a6329d8a8eb0f5ec38eb7b69d", + "ServiceManagerImplementation": "0x0Af4a129D0F3d57B5bD51CAB323EA114C28c064a", "RewardsAgent": "0x2E039a88838241d1Ac738cf2e3C5763ba12571e7", "DelegationManager": "0x867837a9722C512e0862d8c2E15b8bE220E8b87d", "StrategyManager": "0xeE45e76ddbEDdA2918b8C7E3035cd37Eab3b5D41", "AVSDirectory": "0xD58f6844f79eB1fbd9f7091d05f7cb30d3363926", "RewardsCoordinator": "0x29e8572678e0c272350aa0b4B8f304E47EBcd5e7", "AllocationManager": "0x95a7431400F362F3647a69535C5666cA0133CAA0", - "PermissionController": "0xdcCF401fD121d8C542E96BC1d0078884422aFAD2" -} + "PermissionController": "0xdcCF401fD121d8C542E96BC1d0078884422aFAD2", + "ProxyAdmin": "0xeb1a705e1aa96e6a6329d8a8eb0f5ec38eb7b69d" +} \ No newline at end of file diff --git a/contracts/script/deploy/DeployBase.s.sol b/contracts/script/deploy/DeployBase.s.sol index 145c54aa..27f0d6fd 100644 --- a/contracts/script/deploy/DeployBase.s.sol +++ b/contracts/script/deploy/DeployBase.s.sol @@ -131,7 +131,8 @@ abstract contract DeployBase is Script, DeployParams, Accounts { // Deploy DataHaven contracts (same for both modes) ( DataHavenServiceManager serviceManager, - DataHavenServiceManager serviceManagerImplementation + DataHavenServiceManager serviceManagerImplementation, + ProxyAdmin actualProxyAdmin ) = _deployDataHavenContracts(avsConfig, proxyAdmin, gateway, agentAddress); Logging.logFooter(); @@ -151,7 +152,7 @@ abstract contract DeployBase is Script, DeployParams, Accounts { serviceManager, serviceManagerImplementation, agentAddress, - proxyAdmin + actualProxyAdmin ); _outputAgentInfo(agentAddress, snowbridgeConfig.messageOrigin); @@ -242,7 +243,7 @@ abstract contract DeployBase is Script, DeployParams, Accounts { ProxyAdmin proxyAdmin, IGatewayV2 gateway, address agentAddress - ) internal returns (DataHavenServiceManager, DataHavenServiceManager) { + ) internal returns (DataHavenServiceManager, DataHavenServiceManager, ProxyAdmin) { Logging.logHeader("DATAHAVEN CUSTOM CONTRACTS DEPLOYMENT"); // Deploy the Service Manager @@ -278,7 +279,7 @@ abstract contract DeployBase is Script, DeployParams, Accounts { }); // Create the service manager proxy (different logic for local vs testnet) - DataHavenServiceManager serviceManager = + (DataHavenServiceManager serviceManager, ProxyAdmin actualProxyAdmin) = _createServiceManagerProxy(serviceManagerImplementation, proxyAdmin, initParams); Logging.logContractDeployed("ServiceManager Proxy", address(serviceManager)); @@ -293,17 +294,19 @@ abstract contract DeployBase is Script, DeployParams, Accounts { Logging.logInfo("TX EXECUTION DISABLED: call updateAVSMetadataURI via multisig"); } - return (serviceManager, serviceManagerImplementation); + return (serviceManager, serviceManagerImplementation, actualProxyAdmin); } /** * @notice Create service manager proxy - implementation varies by deployment type + * @return serviceManager The proxied ServiceManager instance + * @return actualProxyAdmin The ProxyAdmin that controls the proxy (may differ from the input for live deployments) */ function _createServiceManagerProxy( DataHavenServiceManager implementation, ProxyAdmin proxyAdmin, ServiceManagerInitParams memory params - ) internal virtual returns (DataHavenServiceManager); + ) internal virtual returns (DataHavenServiceManager serviceManager, ProxyAdmin actualProxyAdmin); /** * @notice Output deployed addresses with mode-specific logic diff --git a/contracts/script/deploy/DeployLive.s.sol b/contracts/script/deploy/DeployLive.s.sol index 3becd30e..6530a6c9 100644 --- a/contracts/script/deploy/DeployLive.s.sol +++ b/contracts/script/deploy/DeployLive.s.sol @@ -109,7 +109,7 @@ contract DeployLive is DeployBase { DataHavenServiceManager implementation, ProxyAdmin, // Ignored for live deployment ServiceManagerInitParams memory params - ) internal override returns (DataHavenServiceManager) { + ) internal override returns (DataHavenServiceManager, ProxyAdmin) { // Live deployment creates its own ProxyAdmin for the service manager vm.broadcast(_deployerPrivateKey); ProxyAdmin proxyAdmin = new ProxyAdmin(); @@ -117,7 +117,7 @@ contract DeployLive is DeployBase { // Transfer ProxyAdmin ownership to AVS owner so upgrades can only be performed by AVS owner vm.broadcast(_deployerPrivateKey); - proxyAdmin.transferOwnership(_avsOwner); + proxyAdmin.transferOwnership(params.avsOwner); Logging.logStep("ProxyAdmin ownership transferred to AVS owner"); vm.broadcast(_deployerPrivateKey); @@ -134,7 +134,7 @@ contract DeployLive is DeployBase { TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(address(implementation), address(proxyAdmin), initData); - return DataHavenServiceManager(address(proxy)); + return (DataHavenServiceManager(address(proxy)), proxyAdmin); } function _outputDeployedAddresses( diff --git a/contracts/script/deploy/DeployLocal.s.sol b/contracts/script/deploy/DeployLocal.s.sol index 25ee2ebb..4c41cb0e 100644 --- a/contracts/script/deploy/DeployLocal.s.sol +++ b/contracts/script/deploy/DeployLocal.s.sol @@ -197,7 +197,7 @@ contract DeployLocal is DeployBase { DataHavenServiceManager implementation, ProxyAdmin proxyAdmin, ServiceManagerInitParams memory params - ) internal override returns (DataHavenServiceManager) { + ) internal override returns (DataHavenServiceManager, ProxyAdmin) { // Prepare strategies for service manager (local deployment has deployed strategies) _prepareStrategiesForServiceManager(params); @@ -215,7 +215,7 @@ contract DeployLocal is DeployBase { TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(address(implementation), address(proxyAdmin), initData); - return DataHavenServiceManager(address(proxy)); + return (DataHavenServiceManager(address(proxy)), proxyAdmin); } function _outputDeployedAddresses( diff --git a/test/cli/handlers/contracts/upgrade.ts b/test/cli/handlers/contracts/upgrade.ts index 31d27304..50c30341 100644 --- a/test/cli/handlers/contracts/upgrade.ts +++ b/test/cli/handlers/contracts/upgrade.ts @@ -4,12 +4,13 @@ import path from "node:path"; import { logger, printDivider } from "utils"; import { type Deployments, parseDeploymentsFile } from "utils/contracts"; import { encodeFunctionData } from "viem"; -import { CHAIN_CONFIGS } from "../../../configs/contracts/config"; +import { buildNetworkId, CHAIN_CONFIGS } from "../../../configs/contracts/config"; import { buildContracts } from "../../../scripts/deploy-contracts"; import { verifyContracts } from "./verify"; interface ContractsUpgradeOptions { chain: string; + environment?: string; rpcUrl?: string; privateKeyFile?: string; verify?: boolean; @@ -114,10 +115,14 @@ const executeCommand = async ( */ export const contractsUpgrade = async (options: ContractsUpgradeOptions) => { const isDryRun = !options.execute; + const networkId = buildNetworkId(options.chain, options.environment); try { logger.info("🔄 Starting contract upgrade..."); logger.info(`📡 Using chain: ${options.chain}`); + if (options.environment) { + logger.info(`📡 Using environment: ${options.environment}`); + } if (isDryRun) { logger.info( "â„šī¸ Dry-run mode: the proxy upgrade transaction will NOT be broadcast. Calldata will be printed for manual multisig execution." @@ -149,19 +154,19 @@ export const contractsUpgrade = async (options: ContractsUpgradeOptions) => { // Deploy new implementation contracts (signed by deployer — any funded account) const serviceManagerImplAddress = await deployImplementationContracts( - options.chain, + networkId, rpcUrl, deployerKey ); if (isDryRun) { // Print the calldata for the proxy upgrade so the multisig team can execute it - await printProxyUpgradeCalldata(options.chain, serviceManagerImplAddress, targetVersion); + await printProxyUpgradeCalldata(networkId, serviceManagerImplAddress, targetVersion); } else { // Update proxy contracts to point to new implementations AND update version in one transaction. // Must be signed by the AVS owner, who owns both the ProxyAdmin and the ServiceManager. await updateProxyContracts( - options.chain, + networkId, rpcUrl, avsOwnerKey as string, serviceManagerImplAddress, @@ -173,6 +178,7 @@ export const contractsUpgrade = async (options: ContractsUpgradeOptions) => { logger.info("🔍 Verifying upgraded contracts..."); await verifyContracts({ chain: options.chain, + environment: options.environment, rpcUrl, skipVerification: false }); @@ -195,7 +201,7 @@ export const contractsUpgrade = async (options: ContractsUpgradeOptions) => { * Deploys only the implementation contracts */ const deployImplementationContracts = async ( - chain: string, + networkId: string, rpcUrl: string, privateKey: string ): Promise => { @@ -203,15 +209,15 @@ const deployImplementationContracts = async ( // Deploy new ServiceManager implementation const serviceManagerImplAddress = await deployServiceManagerImplementation( - chain, + networkId, rpcUrl, privateKey ); logger.success(`ServiceManager Implementation deployed: ${serviceManagerImplAddress}`); // Persist the new implementation address so it becomes the source-of-truth for subsequent steps. - const deploymentPath = `../contracts/deployments/${chain}.json`; - const currentDeployments = await parseDeploymentsFile(chain); + const deploymentPath = `../contracts/deployments/${networkId}.json`; + const currentDeployments = await parseDeploymentsFile(networkId); const updatedDeployments = { ...currentDeployments, ServiceManagerImplementation: serviceManagerImplAddress as `0x${string}` @@ -226,13 +232,13 @@ const deployImplementationContracts = async ( * Deploys new ServiceManager implementation contract */ const deployServiceManagerImplementation = async ( - chain: string, + networkId: string, rpcUrl: string, privateKey: string ): Promise => { logger.info("đŸ“Ļ Deploying ServiceManager implementation..."); - const actualDeployments = await parseDeploymentsFile(chain); + const actualDeployments = await parseDeploymentsFile(networkId); // Note: Private key is passed via PRIVATE_KEY environment variable (not command-line) // to prevent it from appearing in system process lists (security best practice) @@ -315,11 +321,11 @@ const PROXY_ADMIN_ABI = [ * The call combines the proxy upgrade and the version update in one atomic transaction. */ const printProxyUpgradeCalldata = async ( - chain: string, + networkId: string, serviceManagerImplAddress: string, version: string ) => { - const deployments = await parseDeploymentsFile(chain); + const deployments = await parseDeploymentsFile(networkId); const proxyAdmin = deployments.ProxyAdmin ?? process.env.PROXY_ADMIN; if (!proxyAdmin) { @@ -380,7 +386,7 @@ const printProxyUpgradeCalldata = async ( * Updates proxy contracts to point to new implementations and sets version */ const updateProxyContracts = async ( - chain: string, + networkId: string, rpcUrl: string, avsOwnerKey: string, serviceManagerImplAddress: string, @@ -388,7 +394,7 @@ const updateProxyContracts = async ( ) => { logger.info("🔄 Updating proxy contracts and version..."); - const deployments = await parseDeploymentsFile(chain); + const deployments = await parseDeploymentsFile(networkId); // Update ServiceManager proxy to point to new implementation and update version in one transaction await updateServiceManagerProxyWithVersion( @@ -438,7 +444,11 @@ const updateServiceManagerProxyWithVersion = async ( // about using the default sender when vm.broadcast is called with a key loaded // from an environment variable rather than --private-key. const { privateKeyToAccount } = await import("viem/accounts"); - const avsOwnerAddress = privateKeyToAccount(avsOwnerKey as `0x${string}`).address; + const normalizedAvsKey = ( + avsOwnerKey.startsWith("0x") ? avsOwnerKey : `0x${avsOwnerKey}` + ) as `0x${string}`; + const avsOwnerAddress = privateKeyToAccount(normalizedAvsKey).address; + logger.info(`🔑 Proxy upgrade will be signed by AVS owner: ${avsOwnerAddress}`); const updateArgs = [ "script", @@ -459,8 +469,22 @@ const updateServiceManagerProxyWithVersion = async ( logger.success(`ServiceManager proxy updated and version set to ${version}`); logger.debug(result); } catch (error) { - logger.error(`❌ Failed to update ServiceManager proxy: ${error}`); - throw error; + const errorMessage = error instanceof Error ? error.message : String(error); + + // Forge may fail to fetch the transaction receipt from the RPC even though the + // transaction was successfully broadcast and confirmed on-chain. Detect this + // specific failure and downgrade it to a warning instead of a hard error. + if (errorMessage.includes("Failure on receiving a receipt for")) { + const txHashMatch = errorMessage.match(/receipt for (0x[a-fA-F0-9]{64})/); + const txHash = txHashMatch ? txHashMatch[1] : "unknown"; + logger.warn( + `âš ī¸ Forge could not fetch the transaction receipt (tx: ${txHash}), but the transaction was likely broadcast successfully. ` + + "Verify the transaction status on a block explorer before proceeding." + ); + } else { + logger.error(`❌ Failed to update ServiceManager proxy: ${error}`); + throw error; + } } }; diff --git a/test/cli/handlers/contracts/verify.ts b/test/cli/handlers/contracts/verify.ts index 5d6bf98b..742ef4e2 100644 --- a/test/cli/handlers/contracts/verify.ts +++ b/test/cli/handlers/contracts/verify.ts @@ -109,7 +109,20 @@ export const verifyContracts = async (options: ContractsVerifyOptions) => { contractPath: "lib/snowbridge/contracts/src/AgentExecutor.sol", constructorArgs: [], constructorArgTypes: [] - } + }, + ...(deployments.ProxyAdmin + ? [ + { + name: "ProxyAdmin", + address: deployments.ProxyAdmin, + artifactName: "ProxyAdmin", + contractPath: + "lib/eigenlayer-contracts/lib/openzeppelin-contracts-v4.9.0/contracts/proxy/transparent/ProxyAdmin.sol", + constructorArgs: [], + constructorArgTypes: [] + } + ] + : []) ]; if (!gatewayImplAddress) { diff --git a/test/cli/index.ts b/test/cli/index.ts index 27763287..338bf38f 100644 --- a/test/cli/index.ts +++ b/test/cli/index.ts @@ -289,6 +289,10 @@ contractsCommand .command("upgrade") .description("Upgrade DataHaven AVS contracts by deploying new implementations") .option("--chain ", "Target chain (hoodi, mainnet, anvil)") + .option( + "--environment ", + "Deployment environment (stagenet, testnet, mainnet). Config and deployment files will be prefixed with this value." + ) .option("--rpc-url ", "Chain RPC URL (optional, defaults based on chain)") .option("--private-key-file ", "Path to file containing private key for deployment") .option("--verify", "Verify upgraded contracts on block explorer", false) @@ -303,7 +307,7 @@ contractsCommand ) .hook("preAction", contractsPreActionHook) .action(async (options: any, command: any) => { - // Try to get chain from options or command + // Try to get chain and environment from options or parent command let chain = options.chain; if (!chain && command.parent) { chain = command.parent.getOptionValue("chain"); @@ -312,11 +316,18 @@ contractsCommand chain = command.getOptionValue("chain"); } - printHeader(`Upgrading DataHaven Contracts on ${chain}`); + let environment = options.environment; + if (!environment && command.parent) { + environment = command.parent.getOptionValue("environment"); + } + + const displayName = environment ? `${environment}-${chain}` : chain; + printHeader(`Upgrading DataHaven Contracts on ${displayName}`); try { await contractsUpgrade({ chain: chain, + environment: environment, rpcUrl: options.rpcUrl, privateKeyFile: options.privateKeyFile, verify: options.verify,