From cb81164f22cd6b1e0f49b399aa136de4af7e1cf7 Mon Sep 17 00:00:00 2001 From: Gonza Montiel Date: Wed, 10 Dec 2025 17:38:21 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20=E2=9C=A8=20=20enable=20AVS=20owner=20w?= =?UTF-8?q?orkflow=20(#332)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Enable AVS owner workflow Until now, the deployer of the contracts and the owner of the deployed contracts where the same account. Even if we allowed a different owner to be specified, we were using the same. For this reason, a private key was required, so after the deployment we could execute owned transactions needed for the CLI. In this PR we: - Add a mechanism to the CLI to specify a different owner account other than the deployer via `--avs-owner-address` - Add CLI flags `--avs-owner-key` and`--execute-owner-transactions` so account ownership vs. immediate execution is explicit and deferred. If both previous parameters are provided, the CLI will execute the transactions using the private key provided. - Allow DataHaven AVS deploy scripts to toggle owner-call execution via an env flag `TX_EXECUTION` - Add documentation on how the new parameters work in `test/README.md` and `test/docs/deployment.md`. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> --- contracts/script/deploy/DeployBase.s.sol | 47 +++-- contracts/script/deploy/DeployParams.s.sol | 8 +- contracts/script/deploy/DeployTestnet.s.sol | 6 + test/README.md | 32 ++++ test/cli/handlers/contracts/deploy.ts | 7 +- .../cli/handlers/contracts/update-metadata.ts | 55 ++++-- test/cli/handlers/deploy/contracts.ts | 6 +- test/cli/handlers/launch/contracts.ts | 3 +- test/cli/index.ts | 13 +- test/docs/deployment.md | 21 +++ test/launcher/contracts.ts | 13 +- test/scripts/deploy-contracts.ts | 161 +++++++++++++++++- 12 files changed, 336 insertions(+), 36 deletions(-) diff --git a/contracts/script/deploy/DeployBase.s.sol b/contracts/script/deploy/DeployBase.s.sol index 96d4b307..664b62ff 100644 --- a/contracts/script/deploy/DeployBase.s.sol +++ b/contracts/script/deploy/DeployBase.s.sol @@ -79,6 +79,8 @@ abstract contract DeployBase is Script, DeployParams, Accounts { EigenPodManager public eigenPodManager; IETHPOSDeposit public ethPOSDeposit; + bool internal _txExecutionEnabled; + function _logProgress() internal { deploymentStep++; Logging.logProgress(deploymentStep, totalSteps); @@ -95,6 +97,8 @@ abstract contract DeployBase is Script, DeployParams, Accounts { * @notice Shared deployment flow for both local and testnet deployments */ function _executeSharedDeployment() internal { + _txExecutionEnabled = vm.envOr("TX_EXECUTION", true); + string memory networkName = _getNetworkName(); string memory deploymentMode = _getDeploymentMode(); @@ -102,6 +106,9 @@ abstract contract DeployBase is Script, DeployParams, Accounts { console.log("| Network: %s", networkName); console.log("| Mode: %s", deploymentMode); console.log("| Timestamp: %s", vm.toString(block.timestamp)); + if (!_txExecutionEnabled) { + Logging.logInfo("TX EXECUTION DISABLED: owner transactions must be executed manually"); + } Logging.logFooter(); // Load configurations @@ -137,9 +144,13 @@ abstract contract DeployBase is Script, DeployParams, Accounts { // Final configuration (same for both modes) Logging.logHeader("FINAL CONFIGURATION"); - vm.broadcast(_avsOwnerPrivateKey); - serviceManager.setRewardsAgent(0, address(rewardsAgentAddress)); - Logging.logStep("Agent set in RewardsRegistry"); + if (_txExecutionEnabled) { + vm.broadcast(_avsOwnerPrivateKey); + serviceManager.setRewardsAgent(0, address(rewardsAgentAddress)); + Logging.logStep("Agent set in RewardsRegistry"); + } else { + Logging.logInfo("TX EXECUTION DISABLED: call setRewardsAgent via multisig"); + } Logging.logContractDeployed("Agent Address", rewardsAgentAddress); Logging.logFooter(); _logProgress(); @@ -301,20 +312,32 @@ abstract contract DeployBase is Script, DeployParams, Accounts { Logging.logSection("Configuring Service Manager"); // Register the DataHaven service in the AllocationManager - vm.broadcast(_avsOwnerPrivateKey); - serviceManager.updateAVSMetadataURI(""); - Logging.logStep("DataHaven service registered in AllocationManager"); + if (_txExecutionEnabled) { + vm.broadcast(_avsOwnerPrivateKey); + serviceManager.updateAVSMetadataURI(""); + Logging.logStep("DataHaven service registered in AllocationManager"); + } else { + Logging.logInfo("TX EXECUTION DISABLED: call updateAVSMetadataURI via multisig"); + } // Set the slasher in the ServiceManager - vm.broadcast(_avsOwnerPrivateKey); - serviceManager.setSlasher(vetoableSlasher); - Logging.logStep("Slasher set in ServiceManager"); + if (_txExecutionEnabled) { + vm.broadcast(_avsOwnerPrivateKey); + serviceManager.setSlasher(vetoableSlasher); + Logging.logStep("Slasher set in ServiceManager"); + } else { + Logging.logInfo("TX EXECUTION DISABLED: call setSlasher via multisig"); + } // Set the RewardsRegistry in the ServiceManager uint32 validatorsSetId = serviceManager.VALIDATORS_SET_ID(); - vm.broadcast(_avsOwnerPrivateKey); - serviceManager.setRewardsRegistry(validatorsSetId, rewardsRegistry); - Logging.logStep("RewardsRegistry set in ServiceManager"); + if (_txExecutionEnabled) { + vm.broadcast(_avsOwnerPrivateKey); + serviceManager.setRewardsRegistry(validatorsSetId, rewardsRegistry); + Logging.logStep("RewardsRegistry set in ServiceManager"); + } else { + Logging.logInfo("TX EXECUTION DISABLED: call setRewardsRegistry via multisig"); + } return ( serviceManager, diff --git a/contracts/script/deploy/DeployParams.s.sol b/contracts/script/deploy/DeployParams.s.sol index fa5f6fcd..368b87e1 100644 --- a/contracts/script/deploy/DeployParams.s.sol +++ b/contracts/script/deploy/DeployParams.s.sol @@ -48,8 +48,12 @@ contract DeployParams is Script, Config { ); string memory configJson = vm.readFile(configPath); - // Load from JSON config or use environment variables as fallback - config.avsOwner = vm.parseJsonAddress(configJson, ".avs.avsOwner"); + address avsOwnerOverride = vm.envOr("AVS_OWNER_ADDRESS", address(0)); + if (avsOwnerOverride != address(0)) { + config.avsOwner = avsOwnerOverride; + } else { + config.avsOwner = vm.parseJsonAddress(configJson, ".avs.avsOwner"); + } config.rewardsInitiator = vm.parseJsonAddress(configJson, ".avs.rewardsInitiator"); config.vetoCommitteeMember = vm.parseJsonAddress(configJson, ".avs.vetoCommitteeMember"); config.vetoWindowBlocks = uint32(vm.parseJsonUint(configJson, ".avs.vetoWindowBlocks")); diff --git a/contracts/script/deploy/DeployTestnet.s.sol b/contracts/script/deploy/DeployTestnet.s.sol index 873740aa..d1ec6c45 100644 --- a/contracts/script/deploy/DeployTestnet.s.sol +++ b/contracts/script/deploy/DeployTestnet.s.sol @@ -51,6 +51,12 @@ contract DeployTestnet is DeployBase { _validateNetwork(networkName); totalSteps = 4; + address avsOwnerEnv = vm.envOr("AVS_OWNER_ADDRESS", address(0)); + require( + avsOwnerEnv != address(0), + "AVS_OWNER_ADDRESS env variable required for testnet deployments" + ); + _executeSharedDeployment(); } diff --git a/test/README.md b/test/README.md index e401093d..8a6750b0 100644 --- a/test/README.md +++ b/test/README.md @@ -57,6 +57,38 @@ bun test suites/some-test.test.ts NOTES: Adding the environment variable `INJECT_CONTRACTS=true` will inject the contracts when starting the tests to speed up setup. +## AVS Owner Parameters & Tx Execution + +Our deployment tooling now separates โ€œwho becomes the ServiceManager ownerโ€ from โ€œwho executes the privileged post-deployment calls.โ€ The knobs are: + +| Flag / Env | Purpose | Default | +| --- | --- | --- | +| `--avs-owner-address` / `AVS_OWNER_ADDRESS` | Address set as `avsOwner` in the ServiceManager initializer. **Required** when targeting testnet/mainnet (Safe multisig). Falls back to `config/.json` only for local/anvil. | Local uses config value; non-local must supply. | +| `--avs-owner-key` / `AVS_OWNER_PRIVATE_KEY` | Private key used to sign owner-only calls the script performs (e.g. `setSlasher`). Only read when tx execution is enabled. | Anvil default key if unset. | +| `--execute-owner-transactions` (CLI) / `TX_EXECUTION=true|false` (env) | Controls whether the script actually broadcasts owner calls. When disabled, we skip sending transactions and instead print ABI-encoded payloads that a Safe can execute. | Enabled automatically for local flows and CI helpers; disabled by default on `hoodi/holesky/mainnet`. | + +### Examples + +```bash +# Local/anvil developer run (executes owner txs immediately) +bun cli contracts deploy --chain anvil --avs-owner-key $LOCAL_OWNER_KEY --execute-owner-transactions + +# Testnet deployment where ownership is a Safe (prints multisig payloads) +AVS_OWNER_ADDRESS=0x... bun cli contracts deploy --chain hoodi + +# Force execution during launch/deploy automation (already the default) +bun cli launch --deploy-contracts --execute-owner-transactions +``` + +When tx execution is off, the CLI prints a list of `{to, data, value}` objects for: + +1. `updateAVSMetadataURI("")` +2. `setSlasher(vetoableSlasher)` +3. `setRewardsRegistry(validatorsSetId, rewardsRegistry)` +4. `setRewardsAgent(validatorsSetId, rewardsAgent)` + +Copy each object into your safe transaction builder (or preferred multisig workflow) to finalize the deployment. + ## Generating Ethereum state To avoid deploying contracts everytime for each tests, you can generate and then inject state in the Ethereum client. diff --git a/test/cli/handlers/contracts/deploy.ts b/test/cli/handlers/contracts/deploy.ts index 4792fb0c..b8dc71f4 100644 --- a/test/cli/handlers/contracts/deploy.ts +++ b/test/cli/handlers/contracts/deploy.ts @@ -15,6 +15,8 @@ export const contractsDeploy = async (options: any, command: any) => { printHeader(`Deploying DataHaven Contracts to ${chain}`); + const txExecutionOverride = options.executeOwnerTransactions ? true : undefined; + try { logger.info("๐Ÿš€ Starting deployment..."); logger.info(`๐Ÿ“ก Using chain: ${chain}`); @@ -25,7 +27,10 @@ export const contractsDeploy = async (options: any, command: any) => { await deployContracts({ chain: chain, rpcUrl: options.rpcUrl, - privateKey: options.privateKey + privateKey: options.privateKey, + avsOwnerKey: options.avsOwnerKey, + avsOwnerAddress: options.avsOwnerAddress, + txExecution: txExecutionOverride }); printDivider(); diff --git a/test/cli/handlers/contracts/update-metadata.ts b/test/cli/handlers/contracts/update-metadata.ts index 03ae6544..d6f49cba 100644 --- a/test/cli/handlers/contracts/update-metadata.ts +++ b/test/cli/handlers/contracts/update-metadata.ts @@ -1,5 +1,5 @@ import { logger, parseDeploymentsFile, printDivider } from "utils"; -import { createPublicClient, createWalletClient, http } from "viem"; +import { createPublicClient, createWalletClient, encodeFunctionData, http } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { getChainDeploymentParams } from "../../../configs/contracts/config"; import { dataHavenServiceManagerAbi } from "../../../contract-bindings/generated"; @@ -7,12 +7,19 @@ import { dataHavenServiceManagerAbi } from "../../../contract-bindings/generated /** * Updates the AVS metadata URI for the DataHaven Service Manager */ -export const updateAVSMetadataURI = async (chain: string, uri: string) => { +export const updateAVSMetadataURI = async ( + chain: string, + uri: string, + opts: { execute?: boolean; avsOwnerKey?: string } = {} +) => { try { - // Load environment variables - const avsOwnerPrivateKey = process.env.AVS_OWNER_PRIVATE_KEY; - if (!avsOwnerPrivateKey) { - throw new Error("AVS_OWNER_PRIVATE_KEY environment variable is required"); + const execute = opts.execute ?? false; + const avsOwnerPrivateKey = normalizePrivateKey( + opts.avsOwnerKey || process.env.AVS_OWNER_PRIVATE_KEY + ); + + if (execute && !avsOwnerPrivateKey) { + throw new Error("AVS owner private key is required to execute this transaction"); } // Get chain configuration @@ -22,6 +29,31 @@ export const updateAVSMetadataURI = async (chain: string, uri: string) => { logger.info(`RPC URL: ${deploymentParams.rpcUrl}`); logger.info(`New URI: ${uri}`); + const deployments = await parseDeploymentsFile(chain); + const serviceManagerAddress = deployments.ServiceManager; + + if (!serviceManagerAddress) { + throw new Error("ServiceManager address not found in deployments file"); + } + + const calldata = encodeFunctionData({ + abi: dataHavenServiceManagerAbi, + functionName: "updateAVSMetadataURI", + args: [uri] + }); + + if (!execute) { + logger.info("๐Ÿ” Tx execution disabled: submit the following transaction via your multisig"); + const payload = { + to: serviceManagerAddress, + value: "0", + data: calldata + }; + logger.info(JSON.stringify(payload, null, 2)); + printDivider(); + return payload; + } + // Create wallet client for the AVS owner const account = privateKeyToAccount(avsOwnerPrivateKey as `0x${string}`); const walletClient = createWalletClient({ @@ -35,10 +67,6 @@ export const updateAVSMetadataURI = async (chain: string, uri: string) => { }); logger.info(`Using account: ${account.address}`); - - const deployments = await parseDeploymentsFile(chain); - const serviceManagerAddress = deployments.ServiceManager; - logger.info(`ServiceManager contract address: ${serviceManagerAddress}`); // Call the updateAVSMetadataURI function @@ -73,3 +101,10 @@ export const updateAVSMetadataURI = async (chain: string, uri: string) => { throw error; } }; + +const normalizePrivateKey = (key?: string): `0x${string}` | undefined => { + if (!key) { + return undefined; + } + return (key.startsWith("0x") ? key : `0x${key}`) as `0x${string}`; +}; diff --git a/test/cli/handlers/deploy/contracts.ts b/test/cli/handlers/deploy/contracts.ts index 73248624..92800f04 100644 --- a/test/cli/handlers/deploy/contracts.ts +++ b/test/cli/handlers/deploy/contracts.ts @@ -44,7 +44,11 @@ export const deployContracts = async (options: DeployContractsOptions) => { // Construct and execute deployment const deployCommand = constructDeployCommand(options); - await executeDeployment(deployCommand, options.parameterCollection, options.chain); + const env: Record = { TX_EXECUTION: "true" }; + if (options.privateKey) { + env.DEPLOYER_PRIVATE_KEY = options.privateKey; + } + await executeDeployment(deployCommand, options.parameterCollection, options.chain, env); printDivider(); }; diff --git a/test/cli/handlers/launch/contracts.ts b/test/cli/handlers/launch/contracts.ts index b6c5d049..dd0fd5c6 100644 --- a/test/cli/handlers/launch/contracts.ts +++ b/test/cli/handlers/launch/contracts.ts @@ -51,7 +51,8 @@ export const deployContracts = async (options: DeployContractsOptions): Promise< rpcUrl: options.rpcUrl, verified: options.verified, blockscoutBackendUrl: options.blockscoutBackendUrl, - parameterCollection: options.parameterCollection + parameterCollection: options.parameterCollection, + txExecution: true }); printDivider(); diff --git a/test/cli/index.ts b/test/cli/index.ts index 79d31ff3..a3a5aaf1 100644 --- a/test/cli/index.ts +++ b/test/cli/index.ts @@ -232,6 +232,12 @@ contractsCommand "Private key for deployment", process.env.DEPLOYER_PRIVATE_KEY || "" ) + .option("--avs-owner-address ", "Address to set as AVS owner (required for non-local)") + .option("--avs-owner-key ", "Private key for the AVS owner (hex string)") + .option( + "--execute-owner-transactions", + "Execute AVS owner transactions immediately (tx execution on)" + ) .option("--skip-verification", "Skip contract verification", false) .hook("preAction", contractsPreActionHook) .action(contractsDeploy); @@ -254,6 +260,8 @@ contractsCommand .option("--uri ", "New metadata URI (required)") .option("--reset", "Use if you want to reset the metadata URI") .option("--rpc-url ", "Chain RPC URL (optional, defaults based on chain)") + .option("--avs-owner-key ", "Private key for the AVS owner (hex string)") + .option("--execute", "Execute transaction immediately instead of emitting calldata", false) .action(async (options: any, command: any) => { // Try to get chain from options or command let chain = options.chain; @@ -272,7 +280,10 @@ contractsCommand if (!chain) { throw new Error("--chain parameter is required"); } - await updateAVSMetadataURI(chain, options.uri); + await updateAVSMetadataURI(chain, options.uri, { + execute: options.execute, + avsOwnerKey: options.avsOwnerKey + }); }); // Default Contracts command (runs check) diff --git a/test/docs/deployment.md b/test/docs/deployment.md index 16faf3db..95aaba1a 100644 --- a/test/docs/deployment.md +++ b/test/docs/deployment.md @@ -318,6 +318,27 @@ If everything went well, you will see something like: bun cli deploy --docker-username= --docker-password= --docker-email= --e local ``` +### AVS owner & tx execution flags + +When invoking `bun cli deploy`/`bun cli contracts deploy` in non-local environments you must: + +- Provide the multisig address that should own the ServiceManager: `--avs-owner-address 0x...` (or set `AVS_OWNER_ADDRESS`). Local deployments can still fall back to the value in `contracts/config/anvil.json`. +- Decide whether the script should broadcast owner-only calls immediately: + - Default (recommended for testnet/mainnet) is leaving tx execution **disabled**, which prints the ABI payloads you can hand off to a Safe. + - To execute immediately (e.g. for local/dev or CI), pass `--execute-owner-transactions` or set `TX_EXECUTION=true`. If you do so, a signing key must be provided via `--avs-owner-key` / `AVS_OWNER_PRIVATE_KEY`. + +Example (testnet Safe ownership, no immediate execution): + +```bash +AVS_OWNER_ADDRESS=0x... bun cli contracts deploy --chain hoodi +``` + +Example (local dev, execute owner calls right away): + +```bash +bun cli contracts deploy --chain anvil --avs-owner-key $LOCAL_OWNER_KEY --execute-owner-transactions +``` + ## Access Kubernetes dashboard: k9s ```bash diff --git a/test/launcher/contracts.ts b/test/launcher/contracts.ts index 63548093..a9a85410 100644 --- a/test/launcher/contracts.ts +++ b/test/launcher/contracts.ts @@ -18,6 +18,7 @@ export interface ContractsOptions { verified?: boolean; blockscoutBackendUrl?: string; parameterCollection?: ParameterCollection; + txExecution?: boolean; } /** @@ -54,14 +55,22 @@ export const deployContracts = async (options: ContractsOptions): Promise // Construct and execute deployment with parameter collection const deployCommand = constructDeployCommand(options); - await executeDeployment(deployCommand, options.parameterCollection, options.chain); + const env: Record = {}; + if (options.privateKey) { + env.DEPLOYER_PRIVATE_KEY = options.privateKey; + } + if (typeof options.txExecution === "boolean") { + env.TX_EXECUTION = options.txExecution ? "true" : "false"; + } + await executeDeployment(deployCommand, options.parameterCollection, options.chain, env); } else { await deployContractsCore({ chain: options.chain || "anvil", rpcUrl: options.rpcUrl, privateKey: options.privateKey, verified: options.verified, - blockscoutBackendUrl: options.blockscoutBackendUrl + blockscoutBackendUrl: options.blockscoutBackendUrl, + txExecution: options.txExecution }); } diff --git a/test/scripts/deploy-contracts.ts b/test/scripts/deploy-contracts.ts index 83935da7..c20461be 100644 --- a/test/scripts/deploy-contracts.ts +++ b/test/scripts/deploy-contracts.ts @@ -1,5 +1,5 @@ import { $ } from "bun"; -import { CHAIN_CONFIGS } from "configs/contracts/config"; +import { CHAIN_CONFIGS, loadChainConfig } from "configs/contracts/config"; import invariant from "tiny-invariant"; import { logger, @@ -8,6 +8,9 @@ import { runShellCommandWithLogger } from "utils"; import type { ParameterCollection } from "utils/parameters"; +import { encodeFunctionData } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import { dataHavenServiceManagerAbi } from "../contract-bindings/generated"; interface ContractDeploymentOptions { chain?: string; @@ -15,6 +18,9 @@ interface ContractDeploymentOptions { privateKey?: string | undefined; verified?: boolean; blockscoutBackendUrl?: string; + avsOwnerAddress?: string; + avsOwnerKey?: string; + txExecution?: boolean; } /** @@ -84,14 +90,14 @@ export const executeDeployment = async ( deployCommand: string, parameterCollection?: ParameterCollection, chain?: string, - privateKey?: string + env?: Record ) => { logger.info("โŒ›๏ธ Deploying contracts (this might take a few minutes)..."); // Using custom shell command to improve logging with forge's stdoutput await runShellCommandWithLogger(deployCommand, { cwd: "../contracts", - env: privateKey ? { DEPLOYER_PRIVATE_KEY: privateKey } : undefined + env }); // After deployment, read the: @@ -177,6 +183,9 @@ export const deployContracts = async (options: { privateKey?: string | undefined; verified?: boolean; blockscoutBackendUrl?: string; + avsOwnerKey?: string; + avsOwnerAddress?: string; + txExecution?: boolean; }) => { const chainConfig = CHAIN_CONFIGS[options.chain as keyof typeof CHAIN_CONFIGS]; @@ -185,13 +194,43 @@ export const deployContracts = async (options: { } const finalRpcUrl = options.rpcUrl || chainConfig.RPC_URL; + const isLocalChain = options.chain === "anvil"; + const txExecutionEnabled = options.txExecution ?? isLocalChain; + const normalizedOwnerKey = normalizePrivateKey( + options.avsOwnerKey || process.env.AVS_OWNER_PRIVATE_KEY + ); + + let resolvedAvsOwnerAddress = options.avsOwnerAddress; + if (!resolvedAvsOwnerAddress && normalizedOwnerKey) { + resolvedAvsOwnerAddress = privateKeyToAccount(normalizedOwnerKey).address; + } + + if (!resolvedAvsOwnerAddress && isLocalChain) { + const config = await loadChainConfig(options.chain); + resolvedAvsOwnerAddress = config?.avs?.avsOwner; + } + + if (!resolvedAvsOwnerAddress) { + throw new Error( + "AVS owner address is required. Provide --avs-owner-address, --avs-owner-key, or AVS_OWNER_ADDRESS." + ); + } + + if (txExecutionEnabled && !normalizedOwnerKey) { + throw new Error( + "Executing AVS owner transactions requires --avs-owner-key or AVS_OWNER_PRIVATE_KEY to be set." + ); + } const deploymentOptions: ContractDeploymentOptions = { chain: options.chain, rpcUrl: finalRpcUrl, privateKey: options.privateKey, verified: options.verified, - blockscoutBackendUrl: options.blockscoutBackendUrl + blockscoutBackendUrl: options.blockscoutBackendUrl, + avsOwnerAddress: resolvedAvsOwnerAddress, + avsOwnerKey: normalizedOwnerKey, + txExecution: txExecutionEnabled }; // Validate parameters @@ -202,11 +241,120 @@ export const deployContracts = async (options: { // Construct and execute deployment const deployCommand = constructDeployCommand(deploymentOptions); - await executeDeployment(deployCommand, undefined, options.chain, options.privateKey); + const env = buildDeploymentEnv(deploymentOptions); + await executeDeployment(deployCommand, undefined, options.chain, env); + + if (!txExecutionEnabled) { + await emitOwnerTransactionCalldata(options.chain); + } logger.success(`DataHaven contracts deployed successfully to ${options.chain}`); }; +const normalizePrivateKey = (key?: string): `0x${string}` | undefined => { + if (!key) { + return undefined; + } + return (key.startsWith("0x") ? key : `0x${key}`) as `0x${string}`; +}; + +const buildDeploymentEnv = (options: ContractDeploymentOptions) => { + const env: Record = {}; + + if (options.privateKey) { + env.DEPLOYER_PRIVATE_KEY = options.privateKey; + } + + if (options.avsOwnerKey) { + env.AVS_OWNER_PRIVATE_KEY = options.avsOwnerKey; + } + + if (options.avsOwnerAddress) { + env.AVS_OWNER_ADDRESS = options.avsOwnerAddress; + } + + if (typeof options.txExecution === "boolean") { + env.TX_EXECUTION = options.txExecution ? "true" : "false"; + } + + return env; +}; + +const emitOwnerTransactionCalldata = async (chain?: string) => { + try { + const deployments = await parseDeploymentsFile(chain); + const rewardsInfo = await parseRewardsInfoFile(chain); + + const serviceManager = deployments.ServiceManager; + const vetoableSlasher = deployments.VetoableSlasher; + const rewardsRegistry = deployments.RewardsRegistry; + const rewardsAgent = rewardsInfo.RewardsAgent; + + if (!serviceManager || !vetoableSlasher || !rewardsRegistry || !rewardsAgent) { + logger.warn("โš ๏ธ Missing deployment artifacts; cannot produce multisig calldata."); + return; + } + + const calls = [ + { + label: "Set metadata URI", + description: 'DataHavenServiceManager.updateAVSMetadataURI("")', + to: serviceManager, + value: "0", + data: encodeFunctionData({ + abi: dataHavenServiceManagerAbi, + functionName: "updateAVSMetadataURI", + args: [""] + }) + }, + { + label: "Set slasher", + description: "DataHavenServiceManager.setSlasher(address)", + to: serviceManager, + value: "0", + data: encodeFunctionData({ + abi: dataHavenServiceManagerAbi, + functionName: "setSlasher", + args: [vetoableSlasher] + }) + }, + { + label: "Attach RewardsRegistry", + description: "DataHavenServiceManager.setRewardsRegistry(VALIDATORS_SET_ID, address)", + to: serviceManager, + value: "0", + data: encodeFunctionData({ + abi: dataHavenServiceManagerAbi, + functionName: "setRewardsRegistry", + args: [0, rewardsRegistry] + }) + }, + { + label: "Set Rewards Agent", + description: "DataHavenServiceManager.setRewardsAgent(VALIDATORS_SET_ID, address)", + to: serviceManager, + value: "0", + data: encodeFunctionData({ + abi: dataHavenServiceManagerAbi, + functionName: "setRewardsAgent", + args: [0, rewardsAgent] + }) + } + ]; + + logger.info( + "๐Ÿ” On-chain owner transactions were deferred. Submit the following calls via your multisig:" + ); + calls.forEach((call, index) => { + logger.info(`\n#${index + 1} ${call.label}`); + logger.info(call.description); + logger.info(JSON.stringify(call, null, 2)); + }); + } catch (error) { + logger.warn(`โš ๏ธ Failed to build multisig calldata: ${error}`); + } +}; + // Allow script to be run directly with CLI arguments if (import.meta.main) { const args = process.argv.slice(2); @@ -255,5 +403,6 @@ if (import.meta.main) { await buildContracts(); const deployCommand = constructDeployCommand(options); - await executeDeployment(deployCommand, undefined, undefined, options.privateKey); + const directEnv = options.privateKey ? { DEPLOYER_PRIVATE_KEY: options.privateKey } : undefined; + await executeDeployment(deployCommand, undefined, undefined, directEnv); }