mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-24 09:50:01 +00:00
fix: contracts upgrade CLI environment support and deploy fixes
- Add --environment option to the contracts upgrade command, aligning it with deploy, verify, and other contracts subcommands. All internal functions now use buildNetworkId(chain, environment) for deployment file lookups (e.g. stagenet-hoodi.json). - Fix ProxyAdmin address not recorded in deployment JSON for live deployments. _createServiceManagerProxy now returns the ProxyAdmin it creates so it propagates back through _deployDataHavenContracts and _executeSharedDeployment to _outputDeployedAddresses, instead of writing address(0). - Fix ProxyAdmin ownership transfer in DeployLive using wrong address. transferOwnership now uses params.avsOwner (from AVS_OWNER_ADDRESS env var) instead of _avsOwner (from AVS_OWNER_PRIVATE_KEY with Anvil fallback), ensuring consistency with the ServiceManager initialisation. - Add ProxyAdmin to the contracts verify list so it is verified on block explorers alongside the other deployed contracts. - Log the AVS owner address before the proxy upgrade transaction so the signer can be verified. - Handle forge receipt-fetch failures gracefully during proxy upgrades. When the RPC returns null for a receipt (tx was broadcast but forge cannot confirm), downgrade to a warning with the tx hash instead of throwing a hard error. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b08570b645
commit
4b7d91efc5
7 changed files with 86 additions and 35 deletions
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue