mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-24 01:38:32 +00:00
## 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>
284 lines
9.7 KiB
TypeScript
284 lines
9.7 KiB
TypeScript
import { execSync } from "node:child_process";
|
|
import { logger } from "utils";
|
|
import { parseDeploymentsFile } from "utils/contracts";
|
|
import { buildNetworkId, CHAIN_CONFIGS, getChainConfig } from "../../../configs/contracts/config";
|
|
|
|
interface ContractsVerifyOptions {
|
|
chain: string;
|
|
environment?: string;
|
|
rpcUrl?: string;
|
|
skipVerification: boolean;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Handles contract verification on block explorer using Foundry's built-in verification
|
|
*/
|
|
export const verifyContracts = async (options: ContractsVerifyOptions) => {
|
|
if (options.skipVerification) {
|
|
logger.info("🏳️ Skipping contract verification");
|
|
return;
|
|
}
|
|
|
|
// Build network identifier for deployment file lookup
|
|
const networkId = buildNetworkId(options.chain, options.environment);
|
|
|
|
logger.info(`🔍 Verifying contracts on ${networkId} block explorer using Foundry...`);
|
|
|
|
const etherscanApiKey = process.env.ETHERSCAN_API_KEY;
|
|
if (!etherscanApiKey) {
|
|
logger.warn("⚠️ ETHERSCAN_API_KEY not found, skipping verification");
|
|
logger.info("💡 Set ETHERSCAN_API_KEY environment variable to enable verification");
|
|
return;
|
|
}
|
|
|
|
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,
|
|
deployments.AllocationManager
|
|
],
|
|
constructorArgTypes: ["address", "address", "address"]
|
|
},
|
|
{
|
|
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: [],
|
|
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: []
|
|
},
|
|
{
|
|
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) => {
|
|
logger.info(` • ${contract.name}: ${contract.address}`);
|
|
});
|
|
logger.info(`🔗 View contracts on ${options.chain} block explorer:`);
|
|
logger.info(` • ${getChainConfig(options.chain).BLOCK_EXPLORER}`);
|
|
|
|
// Verify each contract with delay to respect rate limits
|
|
for (const contract of contractsToVerify) {
|
|
await verifySingleContract(contract, options);
|
|
|
|
// Add delay between requests to respect rate limits
|
|
if (contract !== contractsToVerify[contractsToVerify.length - 1]) {
|
|
logger.info("⏳ Waiting 1 second before next verification...");
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
}
|
|
}
|
|
|
|
logger.success("Contract verification completed");
|
|
logger.info(" - Check the block explorer for verification status");
|
|
} catch (error) {
|
|
logger.error(`❌ Contract verification failed: ${error}`);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Verify a single contract using Foundry's built-in verification
|
|
*/
|
|
async function verifySingleContract(contract: ContractToVerify, options: ContractsVerifyOptions) {
|
|
logger.info(`\n🔍 Verifying ${contract.name} (${contract.address})...`);
|
|
|
|
const {
|
|
address,
|
|
artifactName,
|
|
contractPath,
|
|
constructorArgs: args,
|
|
constructorArgTypes: types,
|
|
guessConstructorArgs
|
|
} = contract;
|
|
|
|
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} ${contractPath}:${artifactName} --rpc-url ${rpcUrl} ${chainParameter} ${constructorArgsStr} --watch`;
|
|
|
|
logger.info(`Running: ${verifyCommand}`);
|
|
|
|
// Execute forge verify-contract
|
|
const result = execSync(verifyCommand, {
|
|
encoding: "utf8",
|
|
stdio: "pipe",
|
|
cwd: "../contracts", // Run from contracts directory
|
|
env: {
|
|
...process.env,
|
|
ETHERSCAN_API_KEY: process.env.ETHERSCAN_API_KEY
|
|
}
|
|
});
|
|
|
|
logger.success(`${contract.name} verified successfully using Foundry!`);
|
|
logger.debug(result);
|
|
} catch (error) {
|
|
logger.warn(`⚠️ ${contract.name} verification failed: ${error}`);
|
|
const chainConfig = CHAIN_CONFIGS[options.chain as keyof typeof CHAIN_CONFIGS];
|
|
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} ${contract.contractPath}:${contract.artifactName} --rpc-url ${rpcUrl} --chain ${options.chain} ${constructorArgsStr}`;
|
|
logger.info(`cd ../contracts && ${manualCommand}`);
|
|
}
|
|
}
|
|
|
|
const getEncodedConstructorArgs = (args: string[], types: string[]): string => {
|
|
if (args.length > 0) {
|
|
try {
|
|
return execSync(
|
|
`cast abi-encode "constructor(${types.join(",")})" ${args.map((arg) => `"${arg}"`).join(" ")}`,
|
|
{ encoding: "utf8", stdio: "pipe", cwd: "../contracts" }
|
|
).trim();
|
|
} catch (error) {
|
|
logger.error(`Failed to ABI-encode constructor arguments: ${error}`);
|
|
throw error;
|
|
}
|
|
}
|
|
return "";
|
|
};
|
|
|
|
/**
|
|
* Checks if contracts are already verified. For proxies, checks implementation contracts.
|
|
*/
|
|
export const checkContractVerification = async (
|
|
contractAddress: string,
|
|
chain?: string,
|
|
rpcUrl?: string
|
|
): Promise<boolean> => {
|
|
try {
|
|
const apiKey = process.env.ETHERSCAN_API_KEY;
|
|
if (!apiKey) throw new Error("ETHERSCAN_API_KEY not found");
|
|
|
|
// Try to get implementation address for proxy contracts
|
|
if (rpcUrl) {
|
|
const implAddress = await getProxyImplementation(contractAddress, rpcUrl);
|
|
if (implAddress && implAddress !== contractAddress) {
|
|
const implVerified = await isVerified(implAddress, chain, apiKey);
|
|
if (implVerified) return true;
|
|
}
|
|
}
|
|
|
|
// Check the original contract
|
|
return await isVerified(contractAddress, chain, apiKey);
|
|
} catch (error) {
|
|
logger.warn(`Failed to check verification status for ${contractAddress}: ${error}`);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const getProxyImplementation = async (address: string, rpcUrl: string): Promise<string | null> => {
|
|
try {
|
|
const response = await fetch(rpcUrl, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
jsonrpc: "2.0",
|
|
method: "eth_getStorageAt",
|
|
params: [
|
|
address,
|
|
"0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc",
|
|
"latest"
|
|
],
|
|
id: 1
|
|
})
|
|
});
|
|
const data = (await response.json()) as any;
|
|
return data.result ? `0x${data.result.slice(-40)}` : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const isVerified = async (
|
|
address: string,
|
|
chain: string | undefined,
|
|
apiKey: string
|
|
): Promise<boolean> => {
|
|
if (!chain) {
|
|
return false;
|
|
}
|
|
const response = await fetch(
|
|
`https://api.etherscan.io/v2/api?module=contract&action=getsourcecode&address=${address}&chainid=${chain}&apikey=${apiKey}`
|
|
);
|
|
const data = (await response.json()) as any;
|
|
return data.result?.[0]?.SourceCode && data.result[0].SourceCode !== "";
|
|
};
|