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 64de00e4..fe1d127c 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); Logging.logFooter(); @@ -151,7 +152,7 @@ abstract contract DeployBase is Script, DeployParams, Accounts { serviceManager, serviceManagerImplementation, rewardsAgentAddress, - proxyAdmin + actualProxyAdmin ); _outputRewardsAgentInfo(rewardsAgentAddress, snowbridgeConfig.rewardsMessageOrigin); @@ -241,7 +242,7 @@ abstract contract DeployBase is Script, DeployParams, Accounts { AVSConfig memory avsConfig, ProxyAdmin proxyAdmin, IGatewayV2 gateway - ) internal returns (DataHavenServiceManager, DataHavenServiceManager) { + ) internal returns (DataHavenServiceManager, DataHavenServiceManager, ProxyAdmin) { Logging.logHeader("DATAHAVEN CUSTOM CONTRACTS DEPLOYMENT"); // Deploy the Service Manager @@ -277,7 +278,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)); @@ -292,17 +293,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 1ed926ef..1ecf7921 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,