mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-24 09:50:01 +00:00
feat: ✨ enable AVS owner workflow (#332)
# 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>
This commit is contained in:
parent
ef3ddaaf69
commit
cb81164f22
12 changed files with 336 additions and 36 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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/<network>.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.
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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}`;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<string, string> = { TX_EXECUTION: "true" };
|
||||
if (options.privateKey) {
|
||||
env.DEPLOYER_PRIVATE_KEY = options.privateKey;
|
||||
}
|
||||
await executeDeployment(deployCommand, options.parameterCollection, options.chain, env);
|
||||
|
||||
printDivider();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -232,6 +232,12 @@ contractsCommand
|
|||
"Private key for deployment",
|
||||
process.env.DEPLOYER_PRIVATE_KEY || ""
|
||||
)
|
||||
.option("--avs-owner-address <value>", "Address to set as AVS owner (required for non-local)")
|
||||
.option("--avs-owner-key <value>", "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 <value>", "New metadata URI (required)")
|
||||
.option("--reset", "Use if you want to reset the metadata URI")
|
||||
.option("--rpc-url <value>", "Chain RPC URL (optional, defaults based on chain)")
|
||||
.option("--avs-owner-key <value>", "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)
|
||||
|
|
|
|||
|
|
@ -318,6 +318,27 @@ If everything went well, you will see something like:
|
|||
bun cli deploy --docker-username=<username> --docker-password=<pass> --docker-email=<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
|
||||
|
|
|
|||
|
|
@ -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<void>
|
|||
|
||||
// Construct and execute deployment with parameter collection
|
||||
const deployCommand = constructDeployCommand(options);
|
||||
await executeDeployment(deployCommand, options.parameterCollection, options.chain);
|
||||
const env: Record<string, string> = {};
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string, string>
|
||||
) => {
|
||||
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<string, string> = {};
|
||||
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue