From eaeb06dbbb1f6bf8f2a0ccbfda87fec0ffda558d Mon Sep 17 00:00:00 2001 From: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Date: Thu, 12 Feb 2026 09:22:37 +0100 Subject: [PATCH] feat(contracts): deploy stagenet-hoodi AVS contracts, fix verification and update-metadata CLI (#439) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Add stagenet-hoodi deployment artifacts (contract addresses, rewards info) and update Snowbridge config with latest validator set - Fix the `bun cli contracts verify` command to correctly verify all deployed contracts, including proxy contracts and Snowbridge dependencies - Fix the `bun cli contracts update-metadata` command to use the correct config file when `--environment` is specified ## Contract verification fixes The verification CLI hardcoded all contract source paths as `src/.sol`, which failed for: - **Snowbridge contracts** (Gateway, BeefyClient, AgentExecutor) — these live in `lib/snowbridge/contracts/src/` - **Gateway proxy** — the `Gateway` deployment address is actually a `GatewayProxy`, not the Gateway implementation. The implementation address needs to be resolved from the ERC1967 storage slot - **ServiceManager proxy** — was not being verified at all Changes: - Added `contractPath` field to `ContractToVerify` so each contract specifies its source location relative to the contracts directory - Added `guessConstructorArgs` option for proxy contracts with complex encoded init data (uses forge's `--guess-constructor-args`) - Gateway is now verified as two separate contracts: Gateway Implementation (address resolved from ERC1967 proxy slot) and GatewayProxy - ServiceManager proxy is now verified as `TransparentUpgradeableProxy` ## Update-metadata fix The `update-metadata` command was ignoring the `--environment` flag when selecting the deployments file: 1. The handler received a pre-built networkId (`"stagenet-hoodi"`) as the chain parameter, which `getChainDeploymentParams` couldn't resolve (falling back to anvil). Now chain and environment are passed separately. 2. Commander.js routed `--environment` to the parent contracts command, leaving `options.environment` undefined on the subcommand. Added the same parent-fallback logic already used for `--chain`. ## Test plan - [x] `bun typecheck` passes - [x] Ran `bun cli contracts verify --chain hoodi --environment stagenet` — all contracts verified successfully on Etherscan - [x] `bun cli contracts update-metadata --chain hoodi --environment stagenet` now reads the correct `stagenet-hoodi.json` deployments file --------- Co-authored-by: Claude Opus 4.6 --- contracts/config/stagenet-hoodi.json | 6 +- .../stagenet-hoodi-rewards-info.json | 1 + contracts/deployments/stagenet-hoodi.json | 1 + .../cli/handlers/contracts/update-metadata.ts | 11 +-- test/cli/handlers/contracts/verify.ts | 72 ++++++++++++++++--- test/cli/index.ts | 12 ++-- 6 files changed, 81 insertions(+), 22 deletions(-) create mode 100644 contracts/deployments/stagenet-hoodi-rewards-info.json create mode 100644 contracts/deployments/stagenet-hoodi.json diff --git a/contracts/config/stagenet-hoodi.json b/contracts/config/stagenet-hoodi.json index 0dbd84c1..a79ac73a 100644 --- a/contracts/config/stagenet-hoodi.json +++ b/contracts/config/stagenet-hoodi.json @@ -43,16 +43,16 @@ "randaoCommitDelay": 4, "randaoCommitExpiration": 24, "minNumRequiredSignatures": 3, - "startBlock": 1299215, + "startBlock": 1303065, "rewardsMessageOrigin": "0x56490bd3f367447bfaf57bb18e7a45e1b2db7d538fe42098e87d2aa106c6afdd", - "initialValidatorSetId": 2179, + "initialValidatorSetId": 2186, "initialValidatorHashes": [ "0x07ce4f2cd558f4d4b529a3362b6ff7d616ca0893b53252dc62829b8218ea5c10", "0xaea5344f086d3be7c94cf3a47436bcbb98de23cf1ee773a9180cfecab0453a50", "0xcd3a33755b27fe810dfb780b3f1df1c25efa1bb826ca618e41022fa900876087", "0x4f4ce8cad711a4b33d15095091f8a98eaf9bfd1b39a9159e605cf5d6783cc667" ], - "nextValidatorSetId": 2180, + "nextValidatorSetId": 2187, "nextValidatorHashes": [ "0x07ce4f2cd558f4d4b529a3362b6ff7d616ca0893b53252dc62829b8218ea5c10", "0xaea5344f086d3be7c94cf3a47436bcbb98de23cf1ee773a9180cfecab0453a50", diff --git a/contracts/deployments/stagenet-hoodi-rewards-info.json b/contracts/deployments/stagenet-hoodi-rewards-info.json new file mode 100644 index 00000000..32bba16a --- /dev/null +++ b/contracts/deployments/stagenet-hoodi-rewards-info.json @@ -0,0 +1 @@ +{"RewardsAgent": "0x2E039a88838241d1Ac738cf2e3C5763ba12571e7","RewardsAgentOrigin": "0x56490bd3f367447bfaf57bb18e7a45e1b2db7d538fe42098e87d2aa106c6afdd"} \ No newline at end of file diff --git a/contracts/deployments/stagenet-hoodi.json b/contracts/deployments/stagenet-hoodi.json new file mode 100644 index 00000000..72b70a20 --- /dev/null +++ b/contracts/deployments/stagenet-hoodi.json @@ -0,0 +1 @@ +{"network": "stagenet-hoodi","BeefyClient": "0xE65dc4eCA2Fd428361076e1f204731224CeB4292","AgentExecutor": "0x35d3FdCB19A246a1763421168dF69dA3dE207063","Gateway": "0xE9352f1488F12bFEd722c133C129ca5F467463d1","ServiceManager": "0xED73cCaF067cebC706B2B3a6cf2b9af2c696c6d3","ServiceManagerImplementation": "0x5E1DA2eE025Dac2F8c391Ac86ebA20bd34c32465","RewardsAgent": "0x2E039a88838241d1Ac738cf2e3C5763ba12571e7","DelegationManager": "0x867837a9722C512e0862d8c2E15b8bE220E8b87d","StrategyManager": "0xeE45e76ddbEDdA2918b8C7E3035cd37Eab3b5D41","AVSDirectory": "0xD58f6844f79eB1fbd9f7091d05f7cb30d3363926","RewardsCoordinator": "0x29e8572678e0c272350aa0b4B8f304E47EBcd5e7","AllocationManager": "0x95a7431400F362F3647a69535C5666cA0133CAA0","PermissionController": "0xdcCF401fD121d8C542E96BC1d0078884422aFAD2"} \ No newline at end of file diff --git a/test/cli/handlers/contracts/update-metadata.ts b/test/cli/handlers/contracts/update-metadata.ts index d6f49cba..5c7e50b2 100644 --- a/test/cli/handlers/contracts/update-metadata.ts +++ b/test/cli/handlers/contracts/update-metadata.ts @@ -1,7 +1,7 @@ import { logger, parseDeploymentsFile, printDivider } from "utils"; import { createPublicClient, createWalletClient, encodeFunctionData, http } from "viem"; import { privateKeyToAccount } from "viem/accounts"; -import { getChainDeploymentParams } from "../../../configs/contracts/config"; +import { buildNetworkId, getChainDeploymentParams } from "../../../configs/contracts/config"; import { dataHavenServiceManagerAbi } from "../../../contract-bindings/generated"; /** @@ -10,7 +10,7 @@ import { dataHavenServiceManagerAbi } from "../../../contract-bindings/generated export const updateAVSMetadataURI = async ( chain: string, uri: string, - opts: { execute?: boolean; avsOwnerKey?: string } = {} + opts: { execute?: boolean; avsOwnerKey?: string; environment?: string } = {} ) => { try { const execute = opts.execute ?? false; @@ -22,14 +22,15 @@ export const updateAVSMetadataURI = async ( throw new Error("AVS owner private key is required to execute this transaction"); } - // Get chain configuration + // Get chain configuration using base chain name, and build networkId for deployment file lookup + const networkId = buildNetworkId(chain, opts.environment); const deploymentParams = getChainDeploymentParams(chain); - logger.info(`šŸ«Ž Updating AVS metadata URI on ${chain} chain`); + logger.info(`šŸ«Ž Updating AVS metadata URI on ${networkId}`); logger.info(`Network: ${deploymentParams.network} (Chain ID: ${deploymentParams.chainId})`); logger.info(`RPC URL: ${deploymentParams.rpcUrl}`); logger.info(`New URI: ${uri}`); - const deployments = await parseDeploymentsFile(chain); + const deployments = await parseDeploymentsFile(networkId); const serviceManagerAddress = deployments.ServiceManager; if (!serviceManagerAddress) { diff --git a/test/cli/handlers/contracts/verify.ts b/test/cli/handlers/contracts/verify.ts index 5f369b43..5d6bf98b 100644 --- a/test/cli/handlers/contracts/verify.ts +++ b/test/cli/handlers/contracts/verify.ts @@ -14,8 +14,12 @@ interface ContractToVerify { name: string; address: string; artifactName: string; + /** Path to the contract source file relative to the contracts directory (e.g. "src/Foo.sol" or "lib/snowbridge/contracts/src/Bar.sol") */ + contractPath: string; constructorArgs: string[]; constructorArgTypes: string[]; + /** When true, uses forge's --guess-constructor-args instead of explicit args (useful for proxies with complex init data) */ + guessConstructorArgs?: boolean; } /** @@ -41,11 +45,17 @@ export const verifyContracts = async (options: ContractsVerifyOptions) => { const deployments = await parseDeploymentsFile(networkId); + // Resolve the Gateway implementation address from the ERC1967 proxy storage slot + const chainConfig = CHAIN_CONFIGS[options.chain as keyof typeof CHAIN_CONFIGS]; + const rpcUrl = options.rpcUrl || chainConfig.RPC_URL; + const gatewayImplAddress = await getProxyImplementation(deployments.Gateway, rpcUrl); + const contractsToVerify: ContractToVerify[] = [ { name: "ServiceManager Implementation", address: deployments.ServiceManagerImplementation, artifactName: "DataHavenServiceManager", + contractPath: "src/DataHavenServiceManager.sol", constructorArgs: [ deployments.RewardsCoordinator, deployments.PermissionController, @@ -54,16 +64,41 @@ export const verifyContracts = async (options: ContractsVerifyOptions) => { constructorArgTypes: ["address", "address", "address"] }, { - name: "Gateway", - address: deployments.Gateway, - artifactName: "Gateway", + name: "ServiceManager Proxy", + address: deployments.ServiceManager, + artifactName: "TransparentUpgradeableProxy", + contractPath: + "lib/eigenlayer-contracts/lib/openzeppelin-contracts-v4.9.0/contracts/proxy/transparent/TransparentUpgradeableProxy.sol", constructorArgs: [], - constructorArgTypes: [] + constructorArgTypes: [], + guessConstructorArgs: true + }, + ...(gatewayImplAddress + ? [ + { + name: "Gateway Implementation", + address: gatewayImplAddress, + artifactName: "Gateway", + contractPath: "lib/snowbridge/contracts/src/Gateway.sol", + constructorArgs: [deployments.BeefyClient, deployments.AgentExecutor], + constructorArgTypes: ["address", "address"] + } + ] + : []), + { + name: "Gateway Proxy", + address: deployments.Gateway, + artifactName: "GatewayProxy", + contractPath: "lib/snowbridge/contracts/src/GatewayProxy.sol", + constructorArgs: [], + constructorArgTypes: [], + guessConstructorArgs: true }, { name: "BeefyClient", address: deployments.BeefyClient, artifactName: "BeefyClient", + contractPath: "lib/snowbridge/contracts/src/BeefyClient.sol", constructorArgs: [], constructorArgTypes: [] }, @@ -71,11 +106,18 @@ export const verifyContracts = async (options: ContractsVerifyOptions) => { name: "AgentExecutor", address: deployments.AgentExecutor, artifactName: "AgentExecutor", + contractPath: "lib/snowbridge/contracts/src/AgentExecutor.sol", constructorArgs: [], constructorArgTypes: [] } ]; + if (!gatewayImplAddress) { + logger.warn( + "āš ļø Could not resolve Gateway implementation address from proxy, skipping Gateway implementation verification" + ); + } + try { logger.info("šŸ“‹ Contracts to verify:"); contractsToVerify.forEach((contract) => { @@ -109,17 +151,29 @@ export const verifyContracts = async (options: ContractsVerifyOptions) => { async function verifySingleContract(contract: ContractToVerify, options: ContractsVerifyOptions) { logger.info(`\nšŸ” Verifying ${contract.name} (${contract.address})...`); - const { address, artifactName, constructorArgs: args, constructorArgTypes: types } = contract; + const { + address, + artifactName, + contractPath, + constructorArgs: args, + constructorArgTypes: types, + guessConstructorArgs + } = contract; - const abiEncodedArgs = getEncodedConstructorArgs(args, types); - const constructorArgsStr = abiEncodedArgs ? `--constructor-args ${abiEncodedArgs}` : ""; + let constructorArgsStr: string; + if (guessConstructorArgs) { + constructorArgsStr = "--guess-constructor-args"; + } else { + const abiEncodedArgs = getEncodedConstructorArgs(args, types); + constructorArgsStr = abiEncodedArgs ? `--constructor-args ${abiEncodedArgs}` : ""; + } try { const chainConfig = CHAIN_CONFIGS[options.chain as keyof typeof CHAIN_CONFIGS]; const rpcUrl = options.rpcUrl || chainConfig.RPC_URL; const chainParameter = options.chain === "hoodi" ? "--chain-id 560048" : `--chain ${options.chain}`; - const verifyCommand = `forge verify-contract ${address} src/${artifactName}.sol:${artifactName} --rpc-url ${rpcUrl} ${chainParameter} ${constructorArgsStr} --watch`; + const verifyCommand = `forge verify-contract ${address} ${contractPath}:${artifactName} --rpc-url ${rpcUrl} ${chainParameter} ${constructorArgsStr} --watch`; logger.info(`Running: ${verifyCommand}`); @@ -142,7 +196,7 @@ async function verifySingleContract(contract: ContractToVerify, options: Contrac logger.info(`Check manually at: ${chainConfig.BLOCK_EXPLORER}address/${contract.address}`); logger.info("You can also try running the command manually from the contracts directory:"); const rpcUrl = options.rpcUrl || chainConfig.RPC_URL; - const manualCommand = `forge verify-contract ${contract.address} src/${contract.artifactName}.sol:${contract.artifactName} --rpc-url ${rpcUrl} --chain ${options.chain} ${constructorArgsStr}`; + const manualCommand = `forge verify-contract ${contract.address} ${contract.contractPath}:${contract.artifactName} --rpc-url ${rpcUrl} --chain ${options.chain} ${constructorArgsStr}`; logger.info(`cd ../contracts && ${manualCommand}`); } } diff --git a/test/cli/index.ts b/test/cli/index.ts index a9752657..71ad8781 100644 --- a/test/cli/index.ts +++ b/test/cli/index.ts @@ -360,12 +360,14 @@ contractsCommand if (!chain) { throw new Error("--chain parameter is required"); } - // Build network identifier with environment prefix if specified - const environment = options.environment; - const networkId = environment ? `${environment}-${chain}` : chain; - await updateAVSMetadataURI(networkId, options.uri, { + let environment = options.environment; + if (!environment && command.parent) { + environment = command.parent.getOptionValue("environment"); + } + await updateAVSMetadataURI(chain, options.uri, { execute: options.execute, - avsOwnerKey: options.avsOwnerKey + avsOwnerKey: options.avsOwnerKey, + environment }); });