feat(contracts): deploy stagenet-hoodi AVS contracts, fix verification and update-metadata CLI (#439)

## 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/<Name>.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 <noreply@anthropic.com>
This commit is contained in:
Steve Degosserie 2026-02-12 09:22:37 +01:00 committed by GitHub
parent a2aec42254
commit eaeb06dbbb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 81 additions and 22 deletions

View file

@ -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",

View file

@ -0,0 +1 @@
{"RewardsAgent": "0x2E039a88838241d1Ac738cf2e3C5763ba12571e7","RewardsAgentOrigin": "0x56490bd3f367447bfaf57bb18e7a45e1b2db7d538fe42098e87d2aa106c6afdd"}

View file

@ -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"}

View file

@ -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) {

View file

@ -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}`);
}
}

View file

@ -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
});
});