fix: contracts upgrade environment support and deploy fixes (#473)

## 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 <noreply@anthropic.com>
Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com>
This commit is contained in:
Steve Degosserie 2026-03-16 10:55:47 +01:00 committed by GitHub
parent 2f06562467
commit e60363ecc3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 86 additions and 35 deletions

View file

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

View file

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

View file

@ -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(

View file

@ -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(

View file

@ -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<string> => {
@ -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<string> => {
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;
}
}
};

View file

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

View file

@ -289,6 +289,10 @@ contractsCommand
.command("upgrade")
.description("Upgrade DataHaven AVS contracts by deploying new implementations")
.option("--chain <value>", "Target chain (hoodi, mainnet, anvil)")
.option(
"--environment <value>",
"Deployment environment (stagenet, testnet, mainnet). Config and deployment files will be prefixed with this value."
)
.option("--rpc-url <value>", "Chain RPC URL (optional, defaults based on chain)")
.option("--private-key-file <value>", "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,