From 5121ae002ba4e1ac944d41a59d3a6d06019896c9 Mon Sep 17 00:00:00 2001 From: Gonza Montiel Date: Thu, 21 Aug 2025 12:02:31 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20=E2=9C=A8=20=20Datahaven=20contracts=20?= =?UTF-8?q?deployment=20on=20public=20testnet=20(#123)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary This PR introduces support for deploying Datahaven contracts to different chains (hoodi, holesky, mainnet), as well as a new cli command to manage this deployment separately from the regular deployment, while maintaining compatibility with it. #### New CLI command - **`bun cli contracts deploy`** - Deploy contracts to supported chains (Hoodi, Holesky, Mainnet) - **`bun cli contracts status`** - Check deployment configuration and status - **`bun cli contracts verify`** - Verify contracts on block explorers - Commands need the chain parameter: `--chain ` - Right now only `hoodi` and `holesky` are supported ### Deployment #### Hoodi & Holesky Network Support - Added **DeployBase.s.sol** as common ground for **DeployTestnet.s.sol** (also new) and **DeployLocal.s.sol** (existing). - **Hoodi configuration** (`contracts/config/hoodi.json`) with deployed EigenLayer contract addresses to reference. - **Holesky configuration** (`contracts/config/hoodi.json`) with deployed EigenLayer contract addresses to reference. #### Contracts being deployed - **DataHaven**: ServiceManager, VetoableSlasher, RewardsRegistry - **Snowbridge**: BeefyClient, AgentExecutor, Gateway, RewardsAgent - **EigenLayer**: References existing deployed contracts (not re-deployed) #### Deployment files When the deployment is done, a new file under `contracts/deployments` is generated with the addresses of the deployed contracts, for each chain (it will be overriden per chain if run multiple times). So we would have one `anvil.json`, `hoodi.json`, `holesky.json`, etc, with the addresses of the deployed contracts for reference and for later verification. #### Todo - [x] Test compatibility with existing `bun cli launch` and `bun cli deploy` commands #### For follow-up PRs - Fix verification issue with `foundry verify-contracts` when specifying the `chain` or `chain-id` parameter, needed for hoodi (https://github.com/foundry-rs/foundry/issues/7466). - Add `redeploy` feature to only override implementation contract and leave the proxy address untouched ## Usage Examples ```bash # Deploy to Hoodi network bun cli contracts deploy --chain hoodi # Check deployment status bun cli contracts status --chain hoodi # Verify contracts on block explorer bun cli contracts verify --chain hoodi ``` ## Summary by CodeRabbit * **New Features** * Added deployment and configuration support for new networks "hoodi" and "holesky", including new configuration and deployment files. * Introduced a CLI tool for managing contract deployments, status checks, and verification across supported chains. * Added example environment configuration and comprehensive deployment documentation. * Enabled contract verification and status reporting via the CLI with support for block explorer integration. * **Improvements** * Refactored deployment scripts for modularity, supporting both local and testnet environments. * Centralized and extended configuration loading to support additional contract addresses and network parameters. * Enhanced deployment utilities and typings to support multi-network deployments. * **Bug Fixes** * Improved input validation and error handling in CLI commands and deployment scripts. * Added explicit handling for zero address in operator strategy retrieval. * **Chores** * Updated documentation and configuration templates for easier onboarding and deployment management. * Improved logging and output formatting for deployment and verification processes. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> --- contracts/config/example.jsonc | 6 +- contracts/config/holesky.json | 53 ++ contracts/config/hoodi.json | 55 ++ contracts/deployments/holesky.json | 1 + contracts/deployments/hoodi.json | 1 + contracts/foundry.toml | 1 + contracts/script/deploy/Config.sol | 7 + contracts/script/deploy/DeployBase.s.sol | 403 +++++++++++ contracts/script/deploy/DeployLocal.s.sol | 661 ++++++------------ contracts/script/deploy/DeployParams.s.sol | 44 +- contracts/script/deploy/DeployTestnet.s.sol | 289 ++++++++ .../src/middleware/ServiceManagerBase.sol | 5 +- test/cli/handlers/contracts/.env.example | 13 + test/cli/handlers/contracts/README.md | 72 ++ test/cli/handlers/contracts/deploy.ts | 107 +++ test/cli/handlers/contracts/index.ts | 3 + test/cli/handlers/contracts/status.ts | 143 ++++ test/cli/handlers/contracts/verify.ts | 245 +++++++ test/cli/handlers/deploy/contracts.ts | 4 +- test/cli/handlers/index.ts | 1 + test/cli/handlers/launch/contracts.ts | 1 + test/cli/index.ts | 67 ++ test/configs/contracts/config.ts | 85 +++ test/launcher/contracts.ts | 30 +- test/launcher/relayers.ts | 8 +- test/scripts/deploy-contracts.ts | 76 +- test/utils/contracts.ts | 58 +- 27 files changed, 1931 insertions(+), 508 deletions(-) create mode 100644 contracts/config/holesky.json create mode 100644 contracts/config/hoodi.json create mode 100644 contracts/deployments/holesky.json create mode 100644 contracts/deployments/hoodi.json create mode 100644 contracts/script/deploy/DeployBase.s.sol create mode 100644 contracts/script/deploy/DeployTestnet.s.sol create mode 100644 test/cli/handlers/contracts/.env.example create mode 100644 test/cli/handlers/contracts/README.md create mode 100644 test/cli/handlers/contracts/deploy.ts create mode 100644 test/cli/handlers/contracts/index.ts create mode 100644 test/cli/handlers/contracts/status.ts create mode 100644 test/cli/handlers/contracts/verify.ts create mode 100644 test/configs/contracts/config.ts diff --git a/contracts/config/example.jsonc b/contracts/config/example.jsonc index 9c00b8fb..1a8b6788 100644 --- a/contracts/config/example.jsonc +++ b/contracts/config/example.jsonc @@ -75,11 +75,7 @@ /// The number of blocks that the Veto Committee will have to submit a veto. "vetoWindowBlocks": 100, /// The EigenLayer strategy addresses for the Validators to stake into. - "validatorsStrategies": [], - /// The EigenLayer strategy addresses for the Backup Storage Providers to stake into. - "bspsStrategies": [], - /// The EigenLayer strategy addresses for the Main Storage Providers to stake into. - "mspsStrategies": [] + "validatorsStrategies": [] }, "snowbridge": { diff --git a/contracts/config/holesky.json b/contracts/config/holesky.json new file mode 100644 index 00000000..eacb3289 --- /dev/null +++ b/contracts/config/holesky.json @@ -0,0 +1,53 @@ +{ + "eigenLayer": { + "pausers": ["0x53410249ec7d3a3F9F1ba3912D50D6A3Df6d10A7"], + "unpauser": "0xE3328cb5068924119d6170496c4AB2dD12c12d15", + "rewardsUpdater": "0xe30a38ac89ffE5A86D5389Bfbf70C7EC766FbB6e", + "calculationIntervalSeconds": 86400, + "maxRewardsDuration": 6048000, + "maxRetroactiveLength": 7776000, + "maxFutureLength": 2592000, + "genesisRewardsTimestamp": 1710979200, + "activationDelay": 7200, + "globalCommissionBips": 1000, + "executorMultisig": "0x28Ade60640fdBDb2609D8d8734D1b5cBeFc0C348", + "operationsMultisig": "0xfaEF7338b7490b9E272d80A1a39f4657cAf2b97d", + "minWithdrawalDelayBlocks": 50, + "delegationInitPausedStatus": 0, + "eigenPodManagerInitPausedStatus": 0, + "rewardsCoordinatorInitPausedStatus": 0, + "allocationManagerInitPausedStatus": 0, + "deallocationDelay": 50, + "allocationConfigurationDelay": 75, + "beaconChainGenesisTimestamp": 1710666600, + "delegationManager": "0xA44151489861Fe9e3055d95adC98FbD462B948e7", + "strategyManager": "0xdfB5f6CE42aAA7830E94ECFCcAd411beF4d4D5b6", + "eigenPodManager": "0x30770d7E3e71112d7A6b7259542D1f680a70e315", + "avsDirectory": "0x055733000064333CaDDbC92763c58BF0192fFeBf", + "rewardsCoordinator": "0xAcc1fb458a1317E886dB376Fc8141540537E68fE", + "allocationManager": "0x78469728304326CBc65f8f95FA756B0B73164462", + "permissionController": "0x598cb226B591155F767dA17AfE7A2241a68C5C10" + }, + "avs": { + "avsOwner": "0xe30a38ac89ffE5A86D5389Bfbf70C7EC766FbB6e", + "rewardsInitiator": "0xe30a38ac89ffE5A86D5389Bfbf70C7EC766FbB6e", + "vetoCommitteeMember": "0xe30a38ac89ffE5A86D5389Bfbf70C7EC766FbB6e", + "vetoWindowBlocks": 100, + "validatorsStrategies": [] + }, + "snowbridge": { + "randaoCommitDelay": 4, + "randaoCommitExpiration": 24, + "minNumRequiredSignatures": 2, + "startBlock": 1, + "rewardsMessageOrigin": "0x0000000000000000000000000000000000000000000000000000000000000000", + "initialValidatorHashes": [ + "0xaeb47a269393297f4b0a3c9c9cfd00c7a4195255274cf39d83dabc2fcc9ff3d7", + "0xf68aec7304bf37f340dae2ea20fb5271ee28a3128812b84a615da4789e458bde" + ], + "nextValidatorHashes": [ + "0xaeb47a269393297f4b0a3c9c9cfd00c7a4195255274cf39d83dabc2fcc9ff3d7", + "0xf68aec7304bf37f340dae2ea20fb5271ee28a3128812b84a615da4789e458bde" + ] + } +} diff --git a/contracts/config/hoodi.json b/contracts/config/hoodi.json new file mode 100644 index 00000000..02882a2d --- /dev/null +++ b/contracts/config/hoodi.json @@ -0,0 +1,55 @@ +{ + "eigenLayer": { + "pausers": [ + "0x64D78399B0fa32EA72959f33edCF313159F3c13D" + ], + "unpauser": "0xE3328cb5068924119d6170496c4AB2dD12c12d15", + "rewardsUpdater": "0xe30a38ac89ffE5A86D5389Bfbf70C7EC766FbB6e", + "calculationIntervalSeconds": 86400, + "maxRewardsDuration": 6048000, + "maxRetroactiveLength": 7776000, + "maxFutureLength": 2592000, + "genesisRewardsTimestamp": 1710979200, + "activationDelay": 7200, + "globalCommissionBips": 1000, + "executorMultisig": "0xE3328cb5068924119d6170496c4AB2dD12c12d15", + "operationsMultisig": "0xE7f4E30D2619273468afe9EC0Acf805E55532257", + "minWithdrawalDelayBlocks": 50, + "delegationInitPausedStatus": 0, + "eigenPodManagerInitPausedStatus": 0, + "rewardsCoordinatorInitPausedStatus": 0, + "allocationManagerInitPausedStatus": 0, + "deallocationDelay": 50, + "allocationConfigurationDelay": 75, + "beaconChainGenesisTimestamp": 1710666600, + "delegationManager": "0x867837a9722C512e0862d8c2E15b8bE220E8b87d", + "strategyManager": "0xeE45e76ddbEDdA2918b8C7E3035cd37Eab3b5D41", + "eigenPodManager": "0xcd1442415Fc5C29Aa848A49d2e232720BE07976c", + "avsDirectory": "0xD58f6844f79eB1fbd9f7091d05f7cb30d3363926", + "rewardsCoordinator": "0x29e8572678e0c272350aa0b4B8f304E47EBcd5e7", + "allocationManager": "0x95a7431400F362F3647a69535C5666cA0133CAA0", + "permissionController": "0xdcCF401fD121d8C542E96BC1d0078884422aFAD2" + }, + "avs": { + "avsOwner": "0xe30a38ac89ffE5A86D5389Bfbf70C7EC766FbB6e", + "rewardsInitiator": "0xe30a38ac89ffE5A86D5389Bfbf70C7EC766FbB6e", + "vetoCommitteeMember": "0xe30a38ac89ffE5A86D5389Bfbf70C7EC766FbB6e", + "vetoWindowBlocks": 100, + "validatorsStrategies": [] + }, + "snowbridge": { + "randaoCommitDelay": 4, + "randaoCommitExpiration": 24, + "minNumRequiredSignatures": 2, + "startBlock": 1, + "rewardsMessageOrigin": "0x0000000000000000000000000000000000000000000000000000000000000000", + "initialValidatorHashes": [ + "0xaeb47a269393297f4b0a3c9c9cfd00c7a4195255274cf39d83dabc2fcc9ff3d7", + "0xf68aec7304bf37f340dae2ea20fb5271ee28a3128812b84a615da4789e458bde" + ], + "nextValidatorHashes": [ + "0xaeb47a269393297f4b0a3c9c9cfd00c7a4195255274cf39d83dabc2fcc9ff3d7", + "0xf68aec7304bf37f340dae2ea20fb5271ee28a3128812b84a615da4789e458bde" + ] + } +} \ No newline at end of file diff --git a/contracts/deployments/holesky.json b/contracts/deployments/holesky.json new file mode 100644 index 00000000..7212da30 --- /dev/null +++ b/contracts/deployments/holesky.json @@ -0,0 +1 @@ +{"network": "holesky","BeefyClient": "0xcBfD943fC9385041Fc8BCfe9705e6F803A735F09","AgentExecutor": "0x545bC12764770C4F00b05a42F81C6eF5e638B9d3","Gateway": "0xa3C18F4D07Bf97BA5B1405a3e19d59fdAe24913F","ServiceManager": "0xaCF6110009a790eC487Ed362A43FC36cCAE49bC6","ServiceManagerImplementation": "0xE9cf23E6c3d2f46F8DE93d7EFaF6222bD00C5092","VetoableSlasher": "0xDbDD8EcB2725b2720a271f98dB1A7AD31D6A5224","RewardsRegistry": "0x88F243feF1EC11426b743a98cae9Dd7A6704cdf1","RewardsAgent": "0x73C3BF5Da6E0F81423c71e3699Ec4520D020fDfF","DelegationManager": "0xA44151489861Fe9e3055d95adC98FbD462B948e7","StrategyManager": "0xdfB5f6CE42aAA7830E94ECFCcAd411beF4d4D5b6","AVSDirectory": "0x055733000064333CaDDbC92763c58BF0192fFeBf","RewardsCoordinator": "0xAcc1fb458a1317E886dB376Fc8141540537E68fE","AllocationManager": "0x78469728304326CBc65f8f95FA756B0B73164462","PermissionController": "0x598cb226B591155F767dA17AfE7A2241a68C5C10"} \ No newline at end of file diff --git a/contracts/deployments/hoodi.json b/contracts/deployments/hoodi.json new file mode 100644 index 00000000..42ba83d5 --- /dev/null +++ b/contracts/deployments/hoodi.json @@ -0,0 +1 @@ +{"network": "hoodi","BeefyClient": "0x109F9D0064D68639552d9aE037D67186EC870a1f","AgentExecutor": "0xfd44dC7B88d1C5186f5b60A0576245055F9dBEeB","Gateway": "0x0B13aAD3f9bD6bEFB9a4B678E6804b172f320C25","ServiceManager": "0xd69a0181D5d89827648E681cA6a4Cd517dEE8f1B","ServiceManagerImplementation": "0x9F4Fbc2A95d21d58BE029C8F6a656856E16833D6","VetoableSlasher": "0x66E9b408A45C6b7532fa6F7a992719aBAE1039D8","RewardsRegistry": "0x0a9C6901A3a23756BC97d40F44BfA611241a70D5","RewardsAgent": "0xeAd1BB0eA0e203f88d6D332F19910dcdF4A3B1A8","DelegationManager": "0x867837a9722C512e0862d8c2E15b8bE220E8b87d","StrategyManager": "0xeE45e76ddbEDdA2918b8C7E3035cd37Eab3b5D41","AVSDirectory": "0xD58f6844f79eB1fbd9f7091d05f7cb30d3363926","RewardsCoordinator": "0x29e8572678e0c272350aa0b4B8f304E47EBcd5e7","AllocationManager": "0x95a7431400F362F3647a69535C5666cA0133CAA0","PermissionController": "0xdcCF401fD121d8C542E96BC1d0078884422aFAD2"} \ No newline at end of file diff --git a/contracts/foundry.toml b/contracts/foundry.toml index c2830915..bfadf383 100644 --- a/contracts/foundry.toml +++ b/contracts/foundry.toml @@ -111,6 +111,7 @@ [rpc_endpoints] mainnet = "${RPC_MAINNET}" holesky = "${RPC_HOLESKY}" + hoodi = "https://rpc.hoodi.ethpandaops.io" anvil = "http://localhost:8545" # [etherscan] diff --git a/contracts/script/deploy/Config.sol b/contracts/script/deploy/Config.sol index 68c03f2d..61fe9208 100644 --- a/contracts/script/deploy/Config.sol +++ b/contracts/script/deploy/Config.sol @@ -48,5 +48,12 @@ contract Config { uint32 deallocationDelay; uint32 allocationConfigurationDelay; uint64 beaconChainGenesisTimestamp; + // Hoodi-specific contract addresses (existing deployed contracts) + address delegationManager; + address strategyManager; + address avsDirectory; + address rewardsCoordinator; + address allocationManager; + address permissionController; } } diff --git a/contracts/script/deploy/DeployBase.s.sol b/contracts/script/deploy/DeployBase.s.sol new file mode 100644 index 00000000..22ad3443 --- /dev/null +++ b/contracts/script/deploy/DeployBase.s.sol @@ -0,0 +1,403 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.27; + +// Testing imports +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; +import {DeployParams} from "./DeployParams.s.sol"; +import {Logging} from "../utils/Logging.sol"; +import {Accounts} from "../utils/Accounts.sol"; + +// Snowbridge imports +import {Gateway} from "snowbridge/src/Gateway.sol"; +import {IGatewayV2} from "snowbridge/src/v2/IGateway.sol"; +import {GatewayProxy} from "snowbridge/src/GatewayProxy.sol"; +import {AgentExecutor} from "snowbridge/src/AgentExecutor.sol"; +import {Agent} from "snowbridge/src/Agent.sol"; +import {Initializer} from "snowbridge/src/Initializer.sol"; +import {OperatingMode} from "snowbridge/src/types/Common.sol"; +import {ud60x18} from "snowbridge/lib/prb-math/src/UD60x18.sol"; +import {BeefyClient} from "snowbridge/src/BeefyClient.sol"; + +// OpenZeppelin imports +import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +import {TransparentUpgradeableProxy} from + "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +// EigenLayer imports +import {AllocationManager} from "eigenlayer-contracts/src/contracts/core/AllocationManager.sol"; +import {AVSDirectory} from "eigenlayer-contracts/src/contracts/core/AVSDirectory.sol"; +import {DelegationManager} from "eigenlayer-contracts/src/contracts/core/DelegationManager.sol"; +import {RewardsCoordinator} from "eigenlayer-contracts/src/contracts/core/RewardsCoordinator.sol"; +import {StrategyManager} from "eigenlayer-contracts/src/contracts/core/StrategyManager.sol"; +import {PermissionController} from + "eigenlayer-contracts/src/contracts/permissions/PermissionController.sol"; +import {EigenPodManager} from "eigenlayer-contracts/src/contracts/pods/EigenPodManager.sol"; +import {IETHPOSDeposit} from "eigenlayer-contracts/src/contracts/interfaces/IETHPOSDeposit.sol"; + +// DataHaven imports +import {DataHavenServiceManager} from "../../src/DataHavenServiceManager.sol"; +import {MerkleUtils} from "../../src/libraries/MerkleUtils.sol"; +import {VetoableSlasher} from "../../src/middleware/VetoableSlasher.sol"; +import {RewardsRegistry} from "../../src/middleware/RewardsRegistry.sol"; +import {IRewardsRegistry} from "../../src/interfaces/IRewardsRegistry.sol"; +import {ValidatorsUtils} from "../../script/utils/ValidatorsUtils.sol"; + +// Shared structs +struct ServiceManagerInitParams { + address avsOwner; + address rewardsInitiator; + address[] validatorsStrategies; + address gateway; +} + +// Struct to store more detailed strategy information +struct StrategyInfo { + address address_; + address underlyingToken; + address tokenCreator; +} + +/** + * @title DeployBase + * @notice Base contract containing all shared deployment logic between local and testnet deployments + */ +abstract contract DeployBase is Script, DeployParams, Accounts { + // Progress indicator + uint16 public deploymentStep = 0; + uint16 public totalSteps; + + // Shared EigenLayer Contract references + DelegationManager public delegation; + StrategyManager public strategyManager; + AVSDirectory public avsDirectory; + RewardsCoordinator public rewardsCoordinator; + AllocationManager public allocationManager; + PermissionController public permissionController; + EigenPodManager public eigenPodManager; + IETHPOSDeposit public ethPOSDeposit; + + function _logProgress() internal { + deploymentStep++; + Logging.logProgress(deploymentStep, totalSteps); + } + + // Abstract functions that must be implemented by inheriting contracts + function _setupEigenLayerContracts( + EigenLayerConfig memory config + ) internal virtual returns (ProxyAdmin); + function _getNetworkName() internal virtual returns (string memory); + function _getDeploymentMode() internal virtual returns (string memory); + + /** + * @notice Shared deployment flow for both local and testnet deployments + */ + function _executeSharedDeployment() internal { + string memory networkName = _getNetworkName(); + string memory deploymentMode = _getDeploymentMode(); + + Logging.logHeader("DATAHAVEN DEPLOYMENT SCRIPT"); + console.log("| Network: %s", networkName); + console.log("| Mode: %s", deploymentMode); + console.log("| Timestamp: %s", vm.toString(block.timestamp)); + Logging.logFooter(); + + // Load configurations + SnowbridgeConfig memory snowbridgeConfig = getSnowbridgeConfig(); + AVSConfig memory avsConfig = getAVSConfig(); + EigenLayerConfig memory eigenLayerConfig = getEigenLayerConfig(); + + // Setup EigenLayer contracts (implementation varies by deployment type) + ProxyAdmin proxyAdmin = _setupEigenLayerContracts(eigenLayerConfig); + _logProgress(); + + // Deploy Snowbridge (same for both modes) + Logging.logHeader("SNOWBRIDGE DEPLOYMENT"); + ( + BeefyClient beefyClient, + AgentExecutor agentExecutor, + IGatewayV2 gateway, + address payable rewardsAgentAddress + ) = _deploySnowbridge(snowbridgeConfig); + Logging.logFooter(); + _logProgress(); + + // Deploy DataHaven contracts (same for both modes) + ( + DataHavenServiceManager serviceManager, + DataHavenServiceManager serviceManagerImplementation, + VetoableSlasher vetoableSlasher, + RewardsRegistry rewardsRegistry, + bytes4 updateRewardsMerkleRootSelector + ) = _deployDataHavenContracts(avsConfig, proxyAdmin, gateway); + + Logging.logFooter(); + _logProgress(); + + // Final configuration (same for both modes) + Logging.logHeader("FINAL CONFIGURATION"); + vm.broadcast(_avsOwnerPrivateKey); + serviceManager.setRewardsAgent(0, address(rewardsAgentAddress)); + Logging.logStep("Agent set in RewardsRegistry"); + Logging.logContractDeployed("Agent Address", rewardsAgentAddress); + Logging.logFooter(); + _logProgress(); + + // Output deployment info + _outputDeployedAddresses( + beefyClient, + agentExecutor, + gateway, + serviceManager, + serviceManagerImplementation, + vetoableSlasher, + rewardsRegistry, + rewardsAgentAddress + ); + + _outputRewardsInfo( + rewardsAgentAddress, + snowbridgeConfig.rewardsMessageOrigin, + updateRewardsMerkleRootSelector + ); + } + + /** + * @notice Deploy Snowbridge components (shared across all deployment types) + */ + function _deploySnowbridge( + SnowbridgeConfig memory config + ) internal returns (BeefyClient, AgentExecutor, IGatewayV2, address payable) { + Logging.logSection("Deploying Snowbridge Core Components"); + + BeefyClient beefyClient = _deployBeefyClient(config); + Logging.logContractDeployed("BeefyClient", address(beefyClient)); + + vm.broadcast(_deployerPrivateKey); + AgentExecutor agentExecutor = new AgentExecutor(); + Logging.logContractDeployed("AgentExecutor", address(agentExecutor)); + + vm.broadcast(_deployerPrivateKey); + Gateway gatewayImplementation = new Gateway(address(beefyClient), address(agentExecutor)); + Logging.logContractDeployed("Gateway Implementation", address(gatewayImplementation)); + + // Configure and deploy Gateway proxy + OperatingMode defaultOperatingMode = OperatingMode.Normal; + Initializer.Config memory gatewayConfig = Initializer.Config({ + mode: defaultOperatingMode, + deliveryCost: 1, + registerTokenFee: 1, + assetHubCreateAssetFee: 1, + assetHubReserveTransferFee: 1, + exchangeRate: ud60x18(1), + multiplier: ud60x18(1), + foreignTokenDecimals: 18, + maxDestinationFee: 1 + }); + + vm.broadcast(_deployerPrivateKey); + IGatewayV2 gateway = IGatewayV2( + address(new GatewayProxy(address(gatewayImplementation), abi.encode(gatewayConfig))) + ); + Logging.logContractDeployed("Gateway Proxy", address(gateway)); + + // Create Agent + Logging.logSection("Creating Snowbridge Agent"); + vm.broadcast(_deployerPrivateKey); + gateway.v2_createAgent(config.rewardsMessageOrigin); + address payable rewardsAgentAddress = payable(gateway.agentOf(config.rewardsMessageOrigin)); + Logging.logContractDeployed("Rewards Agent", rewardsAgentAddress); + + return (beefyClient, agentExecutor, gateway, rewardsAgentAddress); + } + + /** + * @notice Deploy BeefyClient (shared across all deployment types) + */ + function _deployBeefyClient( + SnowbridgeConfig memory config + ) internal returns (BeefyClient) { + // Create validator sets using the MerkleUtils library + BeefyClient.ValidatorSet memory validatorSet = + ValidatorsUtils._buildValidatorSet(0, config.initialValidatorHashes); + BeefyClient.ValidatorSet memory nextValidatorSet = + ValidatorsUtils._buildValidatorSet(1, config.nextValidatorHashes); + + // Deploy BeefyClient + vm.broadcast(_deployerPrivateKey); + return new BeefyClient( + config.randaoCommitDelay, + config.randaoCommitExpiration, + config.minNumRequiredSignatures, + config.startBlock, + validatorSet, + nextValidatorSet + ); + } + + /** + * @notice Deploy DataHaven custom contracts (shared with mode-specific proxy creation) + */ + function _deployDataHavenContracts( + AVSConfig memory avsConfig, + ProxyAdmin proxyAdmin, + IGatewayV2 gateway + ) + internal + returns ( + DataHavenServiceManager, + DataHavenServiceManager, + VetoableSlasher, + RewardsRegistry, + bytes4 + ) + { + Logging.logHeader("DATAHAVEN CUSTOM CONTRACTS DEPLOYMENT"); + + // Deploy the Service Manager + vm.broadcast(_deployerPrivateKey); + DataHavenServiceManager serviceManagerImplementation = + new DataHavenServiceManager(rewardsCoordinator, permissionController, allocationManager); + Logging.logContractDeployed( + "ServiceManager Implementation", address(serviceManagerImplementation) + ); + + // Create service manager initialisation parameters struct + ServiceManagerInitParams memory initParams = ServiceManagerInitParams({ + avsOwner: avsConfig.avsOwner, + rewardsInitiator: avsConfig.rewardsInitiator, + validatorsStrategies: avsConfig.validatorsStrategies, + gateway: address(gateway) + }); + + // Create the service manager proxy (different logic for local vs testnet) + DataHavenServiceManager serviceManager = + _createServiceManagerProxy(serviceManagerImplementation, proxyAdmin, initParams); + Logging.logContractDeployed("ServiceManager Proxy", address(serviceManager)); + + // Deploy VetoableSlasher + vm.broadcast(_deployerPrivateKey); + VetoableSlasher vetoableSlasher = new VetoableSlasher( + allocationManager, + serviceManager, + avsConfig.vetoCommitteeMember, + avsConfig.vetoWindowBlocks + ); + Logging.logContractDeployed("VetoableSlasher", address(vetoableSlasher)); + + // Deploy RewardsRegistry + vm.broadcast(_deployerPrivateKey); + RewardsRegistry rewardsRegistry = new RewardsRegistry( + address(serviceManager), + address(0) // Will be set to the Agent address after creation + ); + Logging.logContractDeployed("RewardsRegistry", address(rewardsRegistry)); + bytes4 updateRewardsMerkleRootSelector = IRewardsRegistry.updateRewardsMerkleRoot.selector; + + Logging.logSection("Configuring Service Manager"); + + // Register the DataHaven service in the AllocationManager + vm.broadcast(_avsOwnerPrivateKey); + serviceManager.updateAVSMetadataURI(""); + Logging.logStep("DataHaven service registered in AllocationManager"); + + // Set the slasher in the ServiceManager + vm.broadcast(_avsOwnerPrivateKey); + serviceManager.setSlasher(vetoableSlasher); + Logging.logStep("Slasher set in ServiceManager"); + + // 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"); + + return ( + serviceManager, + serviceManagerImplementation, + vetoableSlasher, + rewardsRegistry, + updateRewardsMerkleRootSelector + ); + } + + /** + * @notice Create service manager proxy - implementation varies by deployment type + */ + function _createServiceManagerProxy( + DataHavenServiceManager implementation, + ProxyAdmin proxyAdmin, + ServiceManagerInitParams memory params + ) internal virtual returns (DataHavenServiceManager); + + /** + * @notice Output deployed addresses with mode-specific logic + */ + function _outputDeployedAddresses( + BeefyClient beefyClient, + AgentExecutor agentExecutor, + IGatewayV2 gateway, + DataHavenServiceManager serviceManager, + DataHavenServiceManager serviceManagerImplementation, + VetoableSlasher vetoableSlasher, + RewardsRegistry rewardsRegistry, + address rewardsAgent + ) internal virtual; + + /** + * @notice Output rewards info (shared across all deployment types) + */ + function _outputRewardsInfo( + address rewardsAgent, + bytes32 rewardsAgentOrigin, + bytes4 updateRewardsMerkleRootSelector + ) internal { + Logging.logHeader("REWARDS AGENT INFO"); + Logging.logContractDeployed("RewardsAgent", rewardsAgent); + Logging.logAgentOrigin("RewardsAgentOrigin", vm.toString(rewardsAgentOrigin)); + Logging.logFunctionSelector( + "updateRewardsMerkleRootSelector", vm.toString(updateRewardsMerkleRootSelector) + ); + Logging.logFooter(); + + // Write to deployment file for future reference + string memory network = _getNetworkName(); + string memory rewardsInfoPath = + string.concat(vm.projectRoot(), "/deployments/", network, "-rewards-info.json"); + + // Create directory if it doesn't exist + vm.createDir(string.concat(vm.projectRoot(), "/deployments"), true); + + // Create JSON with rewards info + string memory json = "{"; + json = string.concat(json, '"RewardsAgent": "', vm.toString(rewardsAgent), '",'); + json = string.concat(json, '"RewardsAgentOrigin": "', vm.toString(rewardsAgentOrigin), '",'); + json = string.concat( + json, + '"updateRewardsMerkleRootSelector": "', + _trimToBytes4(vm.toString(updateRewardsMerkleRootSelector)), + '"' + ); + json = string.concat(json, "}"); + + // Write to file + vm.writeFile(rewardsInfoPath, json); + Logging.logInfo(string.concat("Rewards info saved to: ", rewardsInfoPath)); + } + + /** + * @notice Helper function to trim a padded hex string to only the first 4 bytes + */ + function _trimToBytes4( + string memory paddedHex + ) internal pure returns (string memory) { + bytes memory data = bytes(paddedHex); + bytes memory trimmed = new bytes(10); // 0x + 8 hex chars = 10 total chars + + for (uint256 i = 0; i < 10; i++) { + trimmed[i] = data[i]; + } + + return string(trimmed); + } +} diff --git a/contracts/script/deploy/DeployLocal.s.sol b/contracts/script/deploy/DeployLocal.s.sol index c1e09b31..c96cee7b 100644 --- a/contracts/script/deploy/DeployLocal.s.sol +++ b/contracts/script/deploy/DeployLocal.s.sol @@ -1,10 +1,14 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.27; -// Testing imports -import {Script} from "forge-std/Script.sol"; -import {console} from "forge-std/console.sol"; -import {DeployParams} from "./DeployParams.s.sol"; +import {DeployBase, StrategyInfo, ServiceManagerInitParams} from "./DeployBase.s.sol"; + +// Snowbridge imports for function signatures +import {BeefyClient} from "snowbridge/src/BeefyClient.sol"; +import {AgentExecutor} from "snowbridge/src/AgentExecutor.sol"; +import {IGatewayV2} from "snowbridge/src/v2/IGateway.sol"; + +// Logging import import {Logging} from "../utils/Logging.sol"; import {Accounts} from "../utils/Accounts.sol"; import {ValidatorsUtils} from "../utils/ValidatorsUtils.sol"; @@ -20,7 +24,11 @@ import {OperatingMode} from "snowbridge/src/types/Common.sol"; import {ud60x18} from "snowbridge/lib/prb-math/src/UD60x18.sol"; import {BeefyClient} from "snowbridge/src/BeefyClient.sol"; -// OpenZeppelin imports +// DataHaven imports for function signatures +import {VetoableSlasher} from "../../src/middleware/VetoableSlasher.sol"; +import {RewardsRegistry} from "../../src/middleware/RewardsRegistry.sol"; + +// Additional imports specific to local deployment import {ERC20PresetFixedSupply} from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -31,12 +39,15 @@ import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; -// EigenLayer imports +// EigenLayer core contract imports for implementation declarations import {AllocationManager} from "eigenlayer-contracts/src/contracts/core/AllocationManager.sol"; import {AVSDirectory} from "eigenlayer-contracts/src/contracts/core/AVSDirectory.sol"; import {DelegationManager} from "eigenlayer-contracts/src/contracts/core/DelegationManager.sol"; import {RewardsCoordinator} from "eigenlayer-contracts/src/contracts/core/RewardsCoordinator.sol"; import {StrategyManager} from "eigenlayer-contracts/src/contracts/core/StrategyManager.sol"; +import {PermissionController} from + "eigenlayer-contracts/src/contracts/permissions/PermissionController.sol"; + import {IAllocationManagerTypes} from "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; import {IETHPOSDeposit} from "eigenlayer-contracts/src/contracts/interfaces/IETHPOSDeposit.sol"; @@ -45,8 +56,6 @@ import { IRewardsCoordinatorTypes } from "eigenlayer-contracts/src/contracts/interfaces/IRewardsCoordinator.sol"; import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; -import {PermissionController} from - "eigenlayer-contracts/src/contracts/permissions/PermissionController.sol"; import {PauserRegistry} from "eigenlayer-contracts/src/contracts/permissions/PauserRegistry.sol"; import {EigenPod} from "eigenlayer-contracts/src/contracts/pods/EigenPod.sol"; import {EigenPodManager} from "eigenlayer-contracts/src/contracts/pods/EigenPodManager.sol"; @@ -54,75 +63,50 @@ import {StrategyBaseTVLLimits} from "eigenlayer-contracts/src/contracts/strategies/StrategyBaseTVLLimits.sol"; import {EmptyContract} from "eigenlayer-contracts/src/test/mocks/EmptyContract.sol"; -// DataHaven imports import {DataHavenServiceManager} from "../../src/DataHavenServiceManager.sol"; import {VetoableSlasher} from "../../src/middleware/VetoableSlasher.sol"; import {RewardsRegistry} from "../../src/middleware/RewardsRegistry.sol"; import {IRewardsRegistry} from "../../src/interfaces/IRewardsRegistry.sol"; -struct ServiceManagerInitParams { - address avsOwner; - address rewardsInitiator; - address[] validatorsStrategies; - address[] bspsStrategies; - address[] mspsStrategies; - address gateway; -} - -// Struct to store more detailed strategy information -struct StrategyInfo { - address address_; - address underlyingToken; - address tokenCreator; -} - -contract Deploy is Script, DeployParams, Accounts { - // Progress indicator - uint16 public deploymentStep = 0; - uint16 public totalSteps = 4; // Total major deployment steps - - // EigenLayer Contract declarations +/** + * @title DeployLocal + * @notice Deployment script for local development (anvil) - deploys full EigenLayer infrastructure + */ +contract DeployLocal is DeployBase { + // Local-specific EigenLayer Contract declarations EmptyContract public emptyContract; - RewardsCoordinator public rewardsCoordinator; RewardsCoordinator public rewardsCoordinatorImplementation; - PermissionController public permissionController; PermissionController public permissionControllerImplementation; - AllocationManager public allocationManager; AllocationManager public allocationManagerImplementation; - DelegationManager public delegation; DelegationManager public delegationImplementation; - StrategyManager public strategyManager; StrategyManager public strategyManagerImplementation; - AVSDirectory public avsDirectory; AVSDirectory public avsDirectoryImplementation; - EigenPodManager public eigenPodManager; EigenPodManager public eigenPodManagerImplementation; UpgradeableBeacon public eigenPodBeacon; EigenPod public eigenPodImplementation; StrategyBaseTVLLimits public baseStrategyImplementation; StrategyInfo[] public deployedStrategies; - IETHPOSDeposit public ethPOSDeposit; // EigenLayer required semver string public constant SEMVER = "v1.0.0"; - function _logProgress() internal { - deploymentStep++; - Logging.logProgress(deploymentStep, totalSteps); + function run() public { + totalSteps = 4; // Total major deployment steps for local + _executeSharedDeployment(); } - function run() public { - Logging.logHeader("DATAHAVEN DEPLOYMENT SCRIPT"); - console.log("| Network: %s", vm.envOr("NETWORK", string("anvil"))); - console.log("| Timestamp: %s", vm.toString(block.timestamp)); - Logging.logFooter(); + // Implementation of abstract functions from DeployBase + function _getNetworkName() internal pure override returns (string memory) { + return "anvil"; + } - // Load configurations - SnowbridgeConfig memory snowbridgeConfig = getSnowbridgeConfig(); - AVSConfig memory avsConfig = getAVSConfig(); - EigenLayerConfig memory eigenLayerConfig = getEigenLayerConfig(); + function _getDeploymentMode() internal pure override returns (string memory) { + return "LOCAL"; + } - // Deploy EigenLayer core contracts + function _setupEigenLayerContracts( + EigenLayerConfig memory eigenLayerConfig + ) internal override returns (ProxyAdmin) { Logging.logHeader("EIGENLAYER CORE CONTRACTS DEPLOYMENT"); Logging.logInfo("Deploying core infrastructure contracts"); @@ -182,106 +166,184 @@ contract Deploy is Script, DeployParams, Accounts { Logging.logStep("Ownership transferred to multisig"); Logging.logFooter(); - _logProgress(); - - // Deploy Snowbridge and configure Agent - Logging.logHeader("SNOWBRIDGE DEPLOYMENT"); - - ( - BeefyClient beefyClient, - AgentExecutor agentExecutor, - IGatewayV2 gateway, - address payable rewardsAgentAddress - ) = _deploySnowbridge(snowbridgeConfig); - - Logging.logFooter(); - _logProgress(); - - // Deploy DataHaven custom contracts - ( - DataHavenServiceManager serviceManager, - VetoableSlasher vetoableSlasher, - RewardsRegistry rewardsRegistry, - bytes4 updateRewardsMerkleRootSelector - ) = _deployDataHavenContracts(avsConfig, proxyAdmin, gateway); - - Logging.logFooter(); - _logProgress(); - - // Set the Agent in the RewardsRegistry - Logging.logHeader("FINAL CONFIGURATION"); - // This needs to be executed by the AVS owner - vm.broadcast(_avsOwnerPrivateKey); - serviceManager.setRewardsAgent(0, address(rewardsAgentAddress)); - Logging.logStep("Agent set in RewardsRegistry"); - Logging.logContractDeployed("Agent Address", rewardsAgentAddress); - - Logging.logFooter(); - _logProgress(); - - // Output all deployed contract addresses - _outputDeployedAddresses( - beefyClient, - agentExecutor, - gateway, - serviceManager, - vetoableSlasher, - rewardsRegistry, - rewardsAgentAddress - ); - - // Output rewards info (Rewards agent address and origin, updateRewardsMerkleRoot function selector) - _outputRewardsInfo( - rewardsAgentAddress, - snowbridgeConfig.rewardsMessageOrigin, - updateRewardsMerkleRootSelector - ); + return proxyAdmin; } - function _deploySnowbridge( - SnowbridgeConfig memory config - ) internal returns (BeefyClient, AgentExecutor, IGatewayV2, address payable) { - Logging.logSection("Deploying Snowbridge Core Components"); - - BeefyClient beefyClient = _deployBeefyClient(config); - Logging.logContractDeployed("BeefyClient", address(beefyClient)); + function _createServiceManagerProxy( + DataHavenServiceManager implementation, + ProxyAdmin proxyAdmin, + ServiceManagerInitParams memory params + ) internal override returns (DataHavenServiceManager) { + // Prepare strategies for service manager (local deployment has deployed strategies) + _prepareStrategiesForServiceManager(params); vm.broadcast(_deployerPrivateKey); - AgentExecutor agentExecutor = new AgentExecutor(); - Logging.logContractDeployed("AgentExecutor", address(agentExecutor)); - - vm.broadcast(_deployerPrivateKey); - Gateway gatewayImplementation = new Gateway(address(beefyClient), address(agentExecutor)); - Logging.logContractDeployed("Gateway Implementation", address(gatewayImplementation)); - - // Configure and deploy Gateway proxy - OperatingMode defaultOperatingMode = OperatingMode.Normal; - Initializer.Config memory gatewayConfig = Initializer.Config({ - mode: defaultOperatingMode, - deliveryCost: 1, - registerTokenFee: 1, - assetHubCreateAssetFee: 1, - assetHubReserveTransferFee: 1, - exchangeRate: ud60x18(1), - multiplier: ud60x18(1), - foreignTokenDecimals: 18, - maxDestinationFee: 1 - }); - - vm.broadcast(_deployerPrivateKey); - IGatewayV2 gateway = IGatewayV2( - address(new GatewayProxy(address(gatewayImplementation), abi.encode(gatewayConfig))) + bytes memory initData = abi.encodeWithSelector( + DataHavenServiceManager.initialise.selector, + params.avsOwner, + params.rewardsInitiator, + params.validatorsStrategies, + new IStrategy[](0), // FIXME remove when BSPs are removed + new IStrategy[](0), // FIXME remove when MSPs are removed + params.gateway ); - Logging.logContractDeployed("Gateway Proxy", address(gateway)); - // Create Agent - Logging.logSection("Creating Snowbridge Agent"); - vm.broadcast(_deployerPrivateKey); - gateway.v2_createAgent(config.rewardsMessageOrigin); - address payable rewardsAgentAddress = payable(gateway.agentOf(config.rewardsMessageOrigin)); - Logging.logContractDeployed("Rewards Agent", rewardsAgentAddress); + TransparentUpgradeableProxy proxy = + new TransparentUpgradeableProxy(address(implementation), address(proxyAdmin), initData); - return (beefyClient, agentExecutor, gateway, rewardsAgentAddress); + return DataHavenServiceManager(address(proxy)); + } + + function _outputDeployedAddresses( + BeefyClient beefyClient, + AgentExecutor agentExecutor, + IGatewayV2 gateway, + DataHavenServiceManager serviceManager, + DataHavenServiceManager serviceManagerImplementation, + VetoableSlasher vetoableSlasher, + RewardsRegistry rewardsRegistry, + address rewardsAgent + ) internal override { + Logging.logHeader("DEPLOYMENT SUMMARY"); + + Logging.logSection("Snowbridge Contracts + Rewards Agent"); + Logging.logContractDeployed("BeefyClient", address(beefyClient)); + Logging.logContractDeployed("AgentExecutor", address(agentExecutor)); + Logging.logContractDeployed("Gateway", address(gateway)); + Logging.logContractDeployed("RewardsAgent", rewardsAgent); + + Logging.logSection("DataHaven Contracts"); + Logging.logContractDeployed("ServiceManager", address(serviceManager)); + Logging.logContractDeployed("VetoableSlasher", address(vetoableSlasher)); + Logging.logContractDeployed("RewardsRegistry", address(rewardsRegistry)); + + Logging.logSection("EigenLayer Core Contracts"); + Logging.logContractDeployed("DelegationManager", address(delegation)); + Logging.logContractDeployed("StrategyManager", address(strategyManager)); + Logging.logContractDeployed("AVSDirectory", address(avsDirectory)); + Logging.logContractDeployed("EigenPodManager", address(eigenPodManager)); + Logging.logContractDeployed("EigenPodBeacon", address(eigenPodBeacon)); + Logging.logContractDeployed("RewardsCoordinator", address(rewardsCoordinator)); + Logging.logContractDeployed("AllocationManager", address(allocationManager)); + Logging.logContractDeployed("PermissionController", address(permissionController)); + Logging.logContractDeployed("ETHPOSDeposit", address(ethPOSDeposit)); + + Logging.logSection("Strategy Contracts"); + Logging.logContractDeployed( + "BaseStrategyImplementation", address(baseStrategyImplementation) + ); + for (uint256 i = 0; i < deployedStrategies.length; i++) { + Logging.logContractDeployed( + string.concat("DeployedStrategy", vm.toString(i)), deployedStrategies[i].address_ + ); + } + + Logging.logFooter(); + + // Write to deployment file for future reference + string memory network = _getNetworkName(); + string memory deploymentPath = + string.concat(vm.projectRoot(), "/deployments/", network, ".json"); + + // Create directory if it doesn't exist + vm.createDir(string.concat(vm.projectRoot(), "/deployments"), true); + + // Create JSON with deployed addresses + string memory json = "{"; + json = string.concat(json, '"network": "', network, '",'); + + // Snowbridge contracts + json = string.concat(json, '"BeefyClient": "', vm.toString(address(beefyClient)), '",'); + json = string.concat(json, '"AgentExecutor": "', vm.toString(address(agentExecutor)), '",'); + json = string.concat(json, '"Gateway": "', vm.toString(address(gateway)), '",'); + json = + string.concat(json, '"ServiceManager": "', vm.toString(address(serviceManager)), '",'); + json = string.concat( + json, + '"ServiceManagerImplementation": "', + vm.toString(address(serviceManagerImplementation)), + '",' + ); + json = + string.concat(json, '"VetoableSlasher": "', vm.toString(address(vetoableSlasher)), '",'); + json = + string.concat(json, '"RewardsRegistry": "', vm.toString(address(rewardsRegistry)), '",'); + json = string.concat(json, '"RewardsAgent": "', vm.toString(rewardsAgent), '",'); + + // EigenLayer contracts + json = string.concat(json, '"DelegationManager": "', vm.toString(address(delegation)), '",'); + json = + string.concat(json, '"StrategyManager": "', vm.toString(address(strategyManager)), '",'); + json = string.concat(json, '"AVSDirectory": "', vm.toString(address(avsDirectory)), '",'); + json = + string.concat(json, '"EigenPodManager": "', vm.toString(address(eigenPodManager)), '",'); + json = + string.concat(json, '"EigenPodBeacon": "', vm.toString(address(eigenPodBeacon)), '",'); + json = string.concat( + json, '"RewardsCoordinator": "', vm.toString(address(rewardsCoordinator)), '",' + ); + json = string.concat( + json, '"AllocationManager": "', vm.toString(address(allocationManager)), '",' + ); + json = string.concat( + json, '"PermissionController": "', vm.toString(address(permissionController)), '",' + ); + json = string.concat(json, '"ETHPOSDeposit": "', vm.toString(address(ethPOSDeposit)), '",'); + json = string.concat( + json, + '"BaseStrategyImplementation": "', + vm.toString(address(baseStrategyImplementation)), + '"' + ); + + // Add strategies with token information + if (deployedStrategies.length > 0) { + json = string.concat(json, ","); + json = string.concat(json, '"DeployedStrategies": ['); + + for (uint256 i = 0; i < deployedStrategies.length; i++) { + json = string.concat(json, "{"); + json = string.concat( + json, '"address": "', vm.toString(deployedStrategies[i].address_), '",' + ); + json = string.concat( + json, + '"underlyingToken": "', + vm.toString(deployedStrategies[i].underlyingToken), + '",' + ); + json = string.concat( + json, '"tokenCreator": "', vm.toString(deployedStrategies[i].tokenCreator), '"' + ); + json = string.concat(json, "}"); + + // Add comma if not the last element + if (i < deployedStrategies.length - 1) { + json = string.concat(json, ","); + } + } + + json = string.concat(json, "]"); + } + + json = string.concat(json, "}"); + + // Write to file + vm.writeFile(deploymentPath, json); + Logging.logInfo(string.concat("Deployment info saved to: ", deploymentPath)); + } + + // LOCAL-SPECIFIC FUNCTIONS + + function _prepareStrategiesForServiceManager( + ServiceManagerInitParams memory params + ) internal view { + if (params.validatorsStrategies.length == 0) { + params.validatorsStrategies = new address[](deployedStrategies.length); + for (uint256 i = 0; i < deployedStrategies.length; i++) { + params.validatorsStrategies[i] = deployedStrategies[i].address_; + } + } } function _deployProxies( @@ -579,11 +641,6 @@ contract Deploy is Script, DeployParams, Accounts { strategyManager.addStrategiesToDepositWhitelist(strategies); } - function _deployProxyAdmin() internal returns (ProxyAdmin) { - ProxyAdmin proxyAdmin = new ProxyAdmin(); - return proxyAdmin; - } - function _deployPauserRegistry( EigenLayerConfig memory config ) internal returns (PauserRegistry) { @@ -592,292 +649,6 @@ contract Deploy is Script, DeployParams, Accounts { return new PauserRegistry(config.pauserAddresses, config.unpauserAddress); } - function _deployBeefyClient( - SnowbridgeConfig memory config - ) internal returns (BeefyClient) { - // Create validator sets using the MerkleUtils library - BeefyClient.ValidatorSet memory validatorSet = - ValidatorsUtils._buildValidatorSet(0, config.initialValidatorHashes); - BeefyClient.ValidatorSet memory nextValidatorSet = - ValidatorsUtils._buildValidatorSet(1, config.nextValidatorHashes); - - // Deploy BeefyClient - vm.broadcast(_deployerPrivateKey); - return new BeefyClient( - config.randaoCommitDelay, - config.randaoCommitExpiration, - config.minNumRequiredSignatures, - config.startBlock, - validatorSet, - nextValidatorSet - ); - } - - function _outputDeployedAddresses( - BeefyClient beefyClient, - AgentExecutor agentExecutor, - IGatewayV2 gateway, - DataHavenServiceManager serviceManager, - VetoableSlasher vetoableSlasher, - RewardsRegistry rewardsRegistry, - address rewardsAgent - ) internal { - Logging.logHeader("DEPLOYMENT SUMMARY"); - - Logging.logSection("Snowbridge Contracts + Rewards Agent"); - Logging.logContractDeployed("BeefyClient", address(beefyClient)); - Logging.logContractDeployed("AgentExecutor", address(agentExecutor)); - Logging.logContractDeployed("Gateway", address(gateway)); - Logging.logContractDeployed("RewardsAgent", rewardsAgent); - - Logging.logSection("DataHaven Contracts"); - Logging.logContractDeployed("ServiceManager", address(serviceManager)); - Logging.logContractDeployed("VetoableSlasher", address(vetoableSlasher)); - Logging.logContractDeployed("RewardsRegistry", address(rewardsRegistry)); - - Logging.logSection("EigenLayer Core Contracts"); - Logging.logContractDeployed("DelegationManager", address(delegation)); - Logging.logContractDeployed("StrategyManager", address(strategyManager)); - Logging.logContractDeployed("AVSDirectory", address(avsDirectory)); - Logging.logContractDeployed("EigenPodManager", address(eigenPodManager)); - Logging.logContractDeployed("EigenPodBeacon", address(eigenPodBeacon)); - Logging.logContractDeployed("RewardsCoordinator", address(rewardsCoordinator)); - Logging.logContractDeployed("AllocationManager", address(allocationManager)); - Logging.logContractDeployed("PermissionController", address(permissionController)); - Logging.logContractDeployed("ETHPOSDeposit", address(ethPOSDeposit)); - - Logging.logSection("Strategy Contracts"); - Logging.logContractDeployed( - "BaseStrategyImplementation", address(baseStrategyImplementation) - ); - for (uint256 i = 0; i < deployedStrategies.length; i++) { - Logging.logContractDeployed( - string.concat("DeployedStrategy", vm.toString(i)), deployedStrategies[i].address_ - ); - } - - Logging.logFooter(); - - // Write to deployment file for future reference - string memory network = vm.envOr("NETWORK", string("anvil")); - string memory deploymentPath = - string.concat(vm.projectRoot(), "/deployments/", network, ".json"); - - // Create directory if it doesn't exist - vm.createDir(string.concat(vm.projectRoot(), "/deployments"), true); - - // Create JSON with deployed addresses - string memory json = "{"; - json = string.concat(json, '"network": "', network, '",'); - - // Snowbridge contracts - json = string.concat(json, '"BeefyClient": "', vm.toString(address(beefyClient)), '",'); - json = string.concat(json, '"AgentExecutor": "', vm.toString(address(agentExecutor)), '",'); - json = string.concat(json, '"Gateway": "', vm.toString(address(gateway)), '",'); - json = - string.concat(json, '"ServiceManager": "', vm.toString(address(serviceManager)), '",'); - json = - string.concat(json, '"VetoableSlasher": "', vm.toString(address(vetoableSlasher)), '",'); - json = - string.concat(json, '"RewardsRegistry": "', vm.toString(address(rewardsRegistry)), '",'); - json = string.concat(json, '"RewardsAgent": "', vm.toString(rewardsAgent), '",'); - - // EigenLayer contracts - json = string.concat(json, '"DelegationManager": "', vm.toString(address(delegation)), '",'); - json = - string.concat(json, '"StrategyManager": "', vm.toString(address(strategyManager)), '",'); - json = string.concat(json, '"AVSDirectory": "', vm.toString(address(avsDirectory)), '",'); - json = - string.concat(json, '"EigenPodManager": "', vm.toString(address(eigenPodManager)), '",'); - json = - string.concat(json, '"EigenPodBeacon": "', vm.toString(address(eigenPodBeacon)), '",'); - json = string.concat( - json, '"RewardsCoordinator": "', vm.toString(address(rewardsCoordinator)), '",' - ); - json = string.concat( - json, '"AllocationManager": "', vm.toString(address(allocationManager)), '",' - ); - json = string.concat( - json, '"PermissionController": "', vm.toString(address(permissionController)), '",' - ); - json = string.concat(json, '"ETHPOSDeposit": "', vm.toString(address(ethPOSDeposit)), '",'); - json = string.concat( - json, - '"BaseStrategyImplementation": "', - vm.toString(address(baseStrategyImplementation)), - '"' - ); - - // Add strategies with token information - if (deployedStrategies.length > 0) { - json = string.concat(json, ","); - json = string.concat(json, '"DeployedStrategies": ['); - - for (uint256 i = 0; i < deployedStrategies.length; i++) { - json = string.concat(json, "{"); - json = string.concat( - json, '"address": "', vm.toString(deployedStrategies[i].address_), '",' - ); - json = string.concat( - json, - '"underlyingToken": "', - vm.toString(deployedStrategies[i].underlyingToken), - '",' - ); - json = string.concat( - json, '"tokenCreator": "', vm.toString(deployedStrategies[i].tokenCreator), '"' - ); - json = string.concat(json, "}"); - - // Add comma if not the last element - if (i < deployedStrategies.length - 1) { - json = string.concat(json, ","); - } - } - - json = string.concat(json, "]"); - } - - json = string.concat(json, "}"); - - // Write to file - vm.writeFile(deploymentPath, json); - Logging.logInfo(string.concat("Deployment info saved to: ", deploymentPath)); - } - - function _outputRewardsInfo( - address rewardsAgent, - bytes32 rewardsAgentOrigin, - bytes4 updateRewardsMerkleRootSelector - ) internal { - Logging.logHeader("REWARDS AGENT INFO"); - Logging.logContractDeployed("RewardsAgent", rewardsAgent); - Logging.logAgentOrigin("RewardsAgentOrigin", vm.toString(rewardsAgentOrigin)); - Logging.logFunctionSelector( - "updateRewardsMerkleRootSelector", vm.toString(updateRewardsMerkleRootSelector) - ); - Logging.logFooter(); - - // Write to deployment file for future reference - string memory network = vm.envOr("NETWORK", string("anvil")); - string memory rewardsInfoPath = - string.concat(vm.projectRoot(), "/deployments/", network, "-rewards-info.json"); - - // Create directory if it doesn't exist - vm.createDir(string.concat(vm.projectRoot(), "/deployments"), true); - - // Create JSON with rewards info - string memory json = "{"; - json = string.concat(json, '"RewardsAgent": "', vm.toString(rewardsAgent), '",'); - json = string.concat(json, '"RewardsAgentOrigin": "', vm.toString(rewardsAgentOrigin), '",'); - json = string.concat( - json, - '"updateRewardsMerkleRootSelector": "', - _trimToBytes4(vm.toString(updateRewardsMerkleRootSelector)), - '"' - ); - json = string.concat(json, "}"); - - // Write to file - vm.writeFile(rewardsInfoPath, json); - Logging.logInfo(string.concat("Rewards info saved to: ", rewardsInfoPath)); - } - - function _deployDataHavenContracts( - AVSConfig memory avsConfig, - ProxyAdmin proxyAdmin, - IGatewayV2 gateway - ) internal returns (DataHavenServiceManager, VetoableSlasher, RewardsRegistry, bytes4) { - Logging.logHeader("DATAHAVEN CUSTOM CONTRACTS DEPLOYMENT"); - - // Deploy the Service Manager - vm.broadcast(_deployerPrivateKey); - DataHavenServiceManager serviceManagerImplementation = - new DataHavenServiceManager(rewardsCoordinator, permissionController, allocationManager); - Logging.logContractDeployed( - "ServiceManager Implementation", address(serviceManagerImplementation) - ); - - // Extract strategies logic to a helper function to reduce local variables - _prepareStrategiesForServiceManager(avsConfig, deployedStrategies); - - // Create service manager initialisation parameters struct to reduce stack variables - ServiceManagerInitParams memory initParams = ServiceManagerInitParams({ - avsOwner: avsConfig.avsOwner, - rewardsInitiator: avsConfig.rewardsInitiator, - validatorsStrategies: avsConfig.validatorsStrategies, - bspsStrategies: avsConfig.bspsStrategies, - mspsStrategies: avsConfig.mspsStrategies, - gateway: address(gateway) - }); - - // Create the service manager proxy - DataHavenServiceManager serviceManager = - _createServiceManagerProxy(serviceManagerImplementation, proxyAdmin, initParams); - Logging.logContractDeployed("ServiceManager Proxy", address(serviceManager)); - - // Deploy VetoableSlasher - vm.broadcast(_deployerPrivateKey); - VetoableSlasher vetoableSlasher = new VetoableSlasher( - allocationManager, - serviceManager, - avsConfig.vetoCommitteeMember, - avsConfig.vetoWindowBlocks - ); - Logging.logContractDeployed("VetoableSlasher", address(vetoableSlasher)); - - // Deploy RewardsRegistry - vm.broadcast(_deployerPrivateKey); - RewardsRegistry rewardsRegistry = new RewardsRegistry( - address(serviceManager), - address(0) // Will be set to the Agent address after creation - ); - Logging.logContractDeployed("RewardsRegistry", address(rewardsRegistry)); - bytes4 updateRewardsMerkleRootSelector = IRewardsRegistry.updateRewardsMerkleRoot.selector; - - Logging.logSection("Configuring Service Manager"); - - // Register the DataHaven service in the AllocationManager - vm.broadcast(_avsOwnerPrivateKey); - serviceManager.updateAVSMetadataURI(""); - Logging.logStep("DataHaven service registered in AllocationManager"); - - // Set the slasher in the ServiceManager - vm.broadcast(_avsOwnerPrivateKey); - serviceManager.setSlasher(vetoableSlasher); - Logging.logStep("Slasher set in ServiceManager"); - - // 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"); - - return (serviceManager, vetoableSlasher, rewardsRegistry, updateRewardsMerkleRootSelector); - } - - function _createServiceManagerProxy( - DataHavenServiceManager implementation, - ProxyAdmin proxyAdmin, - ServiceManagerInitParams memory params - ) internal returns (DataHavenServiceManager) { - vm.broadcast(_deployerPrivateKey); - bytes memory initData = abi.encodeWithSelector( - DataHavenServiceManager.initialise.selector, - params.avsOwner, - params.rewardsInitiator, - params.validatorsStrategies, - params.bspsStrategies, - params.mspsStrategies, - params.gateway - ); - - TransparentUpgradeableProxy proxy = - new TransparentUpgradeableProxy(address(implementation), address(proxyAdmin), initData); - - return DataHavenServiceManager(address(proxy)); - } - function _prepareStrategiesForServiceManager( AVSConfig memory config, StrategyInfo[] memory strategies @@ -893,22 +664,4 @@ contract Deploy is Script, DeployParams, Accounts { } } } - - /** - * @dev Helper function to trim a padded hex string to only the first 4 bytes (10 characters: 0x + 8 hex digits) - * @param paddedHex The padded hex string from vm.toString() - * @return A hex string with only the first 4 bytes (e.g., "0x12345678") - */ - function _trimToBytes4( - string memory paddedHex - ) internal pure returns (string memory) { - bytes memory data = bytes(paddedHex); - bytes memory trimmed = new bytes(10); // 0x + 8 hex chars = 10 total chars - - for (uint256 i = 0; i < 10; i++) { - trimmed[i] = data[i]; - } - - return string(trimmed); - } } diff --git a/contracts/script/deploy/DeployParams.s.sol b/contracts/script/deploy/DeployParams.s.sol index 4b4d519b..5675b199 100644 --- a/contracts/script/deploy/DeployParams.s.sol +++ b/contracts/script/deploy/DeployParams.s.sol @@ -55,8 +55,6 @@ contract DeployParams is Script, Config { config.vetoWindowBlocks = uint32(vm.parseJsonUint(configJson, ".avs.vetoWindowBlocks")); config.validatorsStrategies = vm.parseJsonAddressArray(configJson, ".avs.validatorsStrategies"); - config.bspsStrategies = vm.parseJsonAddressArray(configJson, ".avs.bspsStrategies"); - config.mspsStrategies = vm.parseJsonAddressArray(configJson, ".avs.mspsStrategies"); return config; } @@ -168,6 +166,48 @@ contract DeployParams is Script, Config { config.beaconChainGenesisTimestamp = 1616508000; // Mainnet default } + // Load EigenLayer-specific contract addresses (if they exist in config) + try vm.parseJsonAddress(configJson, ".eigenLayer.delegationManager") returns (address addr) + { + config.delegationManager = addr; + } catch { + config.delegationManager = address(0); + } + + try vm.parseJsonAddress(configJson, ".eigenLayer.strategyManager") returns (address addr) { + config.strategyManager = addr; + } catch { + config.strategyManager = address(0); + } + + try vm.parseJsonAddress(configJson, ".eigenLayer.avsDirectory") returns (address addr) { + config.avsDirectory = addr; + } catch { + config.avsDirectory = address(0); + } + + try vm.parseJsonAddress(configJson, ".eigenLayer.rewardsCoordinator") returns (address addr) + { + config.rewardsCoordinator = addr; + } catch { + config.rewardsCoordinator = address(0); + } + + try vm.parseJsonAddress(configJson, ".eigenLayer.allocationManager") returns (address addr) + { + config.allocationManager = addr; + } catch { + config.allocationManager = address(0); + } + + try vm.parseJsonAddress(configJson, ".eigenLayer.permissionController") returns ( + address addr + ) { + config.permissionController = addr; + } catch { + config.permissionController = address(0); + } + return config; } diff --git a/contracts/script/deploy/DeployTestnet.s.sol b/contracts/script/deploy/DeployTestnet.s.sol new file mode 100644 index 00000000..df0af4e9 --- /dev/null +++ b/contracts/script/deploy/DeployTestnet.s.sol @@ -0,0 +1,289 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.27; + +import {DeployBase, ServiceManagerInitParams} from "./DeployBase.s.sol"; +import {DataHavenServiceManager} from "../../src/DataHavenServiceManager.sol"; + +// Snowbridge imports for function signatures +import {BeefyClient} from "snowbridge/src/BeefyClient.sol"; +import {AgentExecutor} from "snowbridge/src/AgentExecutor.sol"; +import {IGatewayV2} from "snowbridge/src/v2/IGateway.sol"; + +// Logging import +import {Logging} from "../utils/Logging.sol"; + +// DataHaven imports for function signatures +import {VetoableSlasher} from "../../src/middleware/VetoableSlasher.sol"; +import {RewardsRegistry} from "../../src/middleware/RewardsRegistry.sol"; + +// EigenLayer core contract imports for type casting +import {AllocationManager} from "eigenlayer-contracts/src/contracts/core/AllocationManager.sol"; +import {AVSDirectory} from "eigenlayer-contracts/src/contracts/core/AVSDirectory.sol"; +import {DelegationManager} from "eigenlayer-contracts/src/contracts/core/DelegationManager.sol"; +import {RewardsCoordinator} from "eigenlayer-contracts/src/contracts/core/RewardsCoordinator.sol"; +import {StrategyManager} from "eigenlayer-contracts/src/contracts/core/StrategyManager.sol"; +import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; +import {PermissionController} from + "eigenlayer-contracts/src/contracts/permissions/PermissionController.sol"; + +// OpenZeppelin imports for proxy creation +import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +import {TransparentUpgradeableProxy} from + "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +/** + * @title DeployTestnet + * @notice Deployment script for testnets (hoodi, holesky) - references existing EigenLayer contracts + */ +contract DeployTestnet is DeployBase { + // Supported testnet chains + enum TestnetChain { + HOODI, + HOLESKY + } + + // Current testnet being deployed to + TestnetChain public currentTestnet; + string public networkName; + + function run() public { + // Network detection and validation + networkName = vm.envString("NETWORK"); + require( + bytes(networkName).length > 0, + "NETWORK environment variable required for testnet deployment" + ); + + currentTestnet = _detectAndValidateNetwork(networkName); + totalSteps = 2; // Reduced steps since we're not deploying EigenLayer + + _executeSharedDeployment(); + } + + // Implementation of abstract functions from DeployBase + function _getNetworkName() internal view override returns (string memory) { + return networkName; + } + + function _getDeploymentMode() internal view override returns (string memory) { + if (currentTestnet == TestnetChain.HOODI) { + return "HOODI_TESTNET"; + } else if (currentTestnet == TestnetChain.HOLESKY) { + return "HOLESKY_TESTNET"; + } + return "UNKNOWN_TESTNET"; + } + + function _setupEigenLayerContracts( + EigenLayerConfig memory config + ) internal override returns (ProxyAdmin) { + Logging.logHeader("REFERENCING EXISTING EIGENLAYER CONTRACTS"); + Logging.logSection( + string.concat("Referencing Existing EigenLayer Contracts on ", _getDeploymentMode()) + ); + + // Reference existing EigenLayer contracts using addresses from config + delegation = DelegationManager(config.delegationManager); + strategyManager = StrategyManager(config.strategyManager); + avsDirectory = AVSDirectory(config.avsDirectory); + rewardsCoordinator = RewardsCoordinator(config.rewardsCoordinator); + allocationManager = AllocationManager(config.allocationManager); + permissionController = PermissionController(config.permissionController); + + // Validate that contracts exist at the specified addresses + _validateContractExists(address(delegation), "DelegationManager"); + _validateContractExists(address(strategyManager), "StrategyManager"); + _validateContractExists(address(avsDirectory), "AVSDirectory"); + _validateContractExists(address(rewardsCoordinator), "RewardsCoordinator"); + _validateContractExists(address(allocationManager), "AllocationManager"); + _validateContractExists(address(permissionController), "PermissionController"); + + Logging.logContractDeployed("DelegationManager (existing)", address(delegation)); + Logging.logContractDeployed("StrategyManager (existing)", address(strategyManager)); + Logging.logContractDeployed("AVSDirectory (existing)", address(avsDirectory)); + Logging.logContractDeployed("RewardsCoordinator (existing)", address(rewardsCoordinator)); + Logging.logContractDeployed("AllocationManager (existing)", address(allocationManager)); + Logging.logContractDeployed( + "PermissionController (existing)", address(permissionController) + ); + + Logging.logStep("All EigenLayer contracts referenced successfully"); + Logging.logFooter(); + + // Testnet deployments create their own ProxyAdmin (no existing one from EigenLayer deployment) + return ProxyAdmin(address(0)); // Will be created in _createServiceManagerProxy + } + + function _createServiceManagerProxy( + DataHavenServiceManager implementation, + ProxyAdmin, // Ignored for testnet deployment + ServiceManagerInitParams memory params + ) internal override returns (DataHavenServiceManager) { + // Testnet deployment creates its own ProxyAdmin for the service manager + vm.broadcast(_deployerPrivateKey); + ProxyAdmin proxyAdmin = new ProxyAdmin(); + Logging.logContractDeployed("ProxyAdmin", address(proxyAdmin)); + + vm.broadcast(_deployerPrivateKey); + bytes memory initData = abi.encodeWithSelector( + DataHavenServiceManager.initialise.selector, + params.avsOwner, + params.rewardsInitiator, + params.validatorsStrategies, + new IStrategy[](0), // FIXME remove when BSPs and MSPs are removed + new IStrategy[](0), // FIXME remove when BSPs and MSPs are removed + params.gateway + ); + + TransparentUpgradeableProxy proxy = + new TransparentUpgradeableProxy(address(implementation), address(proxyAdmin), initData); + + return DataHavenServiceManager(address(proxy)); + } + + function _outputDeployedAddresses( + BeefyClient beefyClient, + AgentExecutor agentExecutor, + IGatewayV2 gateway, + DataHavenServiceManager serviceManager, + DataHavenServiceManager serviceManagerImplementation, + VetoableSlasher vetoableSlasher, + RewardsRegistry rewardsRegistry, + address rewardsAgent + ) internal override { + Logging.logHeader("DEPLOYMENT SUMMARY"); + + Logging.logSection("Snowbridge Contracts + Rewards Agent"); + Logging.logContractDeployed("BeefyClient", address(beefyClient)); + Logging.logContractDeployed("AgentExecutor", address(agentExecutor)); + Logging.logContractDeployed("Gateway", address(gateway)); + Logging.logContractDeployed("RewardsAgent", rewardsAgent); + + Logging.logSection("DataHaven Contracts"); + Logging.logContractDeployed("ServiceManager", address(serviceManager)); + Logging.logContractDeployed("VetoableSlasher", address(vetoableSlasher)); + Logging.logContractDeployed("RewardsRegistry", address(rewardsRegistry)); + + Logging.logSection( + string.concat("EigenLayer Core Contracts (Existing on ", _getDeploymentMode(), ")") + ); + Logging.logContractDeployed("DelegationManager", address(delegation)); + Logging.logContractDeployed("StrategyManager", address(strategyManager)); + Logging.logContractDeployed("AVSDirectory", address(avsDirectory)); + Logging.logContractDeployed("RewardsCoordinator", address(rewardsCoordinator)); + Logging.logContractDeployed("AllocationManager", address(allocationManager)); + Logging.logContractDeployed("PermissionController", address(permissionController)); + + Logging.logFooter(); + + // Write to deployment file for future reference + string memory network = _getNetworkName(); + string memory deploymentPath = + string.concat(vm.projectRoot(), "/deployments/", network, ".json"); + + // Create directory if it doesn't exist + vm.createDir(string.concat(vm.projectRoot(), "/deployments"), true); + + // Create JSON with deployed addresses + string memory json = "{"; + json = string.concat(json, '"network": "', network, '",'); + + // Snowbridge contracts + json = string.concat(json, '"BeefyClient": "', vm.toString(address(beefyClient)), '",'); + json = string.concat(json, '"AgentExecutor": "', vm.toString(address(agentExecutor)), '",'); + json = string.concat(json, '"Gateway": "', vm.toString(address(gateway)), '",'); + json = + string.concat(json, '"ServiceManager": "', vm.toString(address(serviceManager)), '",'); + json = string.concat( + json, + '"ServiceManagerImplementation": "', + vm.toString(address(serviceManagerImplementation)), + '",' + ); + json = + string.concat(json, '"VetoableSlasher": "', vm.toString(address(vetoableSlasher)), '",'); + json = + string.concat(json, '"RewardsRegistry": "', vm.toString(address(rewardsRegistry)), '",'); + json = string.concat(json, '"RewardsAgent": "', vm.toString(rewardsAgent), '",'); + + // EigenLayer contracts (existing on testnet) + json = string.concat(json, '"DelegationManager": "', vm.toString(address(delegation)), '",'); + json = + string.concat(json, '"StrategyManager": "', vm.toString(address(strategyManager)), '",'); + json = string.concat(json, '"AVSDirectory": "', vm.toString(address(avsDirectory)), '",'); + json = string.concat( + json, '"RewardsCoordinator": "', vm.toString(address(rewardsCoordinator)), '",' + ); + json = string.concat( + json, '"AllocationManager": "', vm.toString(address(allocationManager)), '",' + ); + json = string.concat( + json, '"PermissionController": "', vm.toString(address(permissionController)), '"' + ); + + json = string.concat(json, "}"); + + // Write to file + vm.writeFile(deploymentPath, json); + Logging.logInfo(string.concat("Deployment info saved to: ", deploymentPath)); + } + + // TESTNET-SPECIFIC FUNCTIONS + + /** + * @notice Detect and validate the target testnet network + */ + function _detectAndValidateNetwork( + string memory network + ) internal pure returns (TestnetChain) { + bytes32 networkHash = keccak256(abi.encodePacked(network)); + + if (networkHash == keccak256(abi.encodePacked("hoodi"))) { + return TestnetChain.HOODI; + } else if (networkHash == keccak256(abi.encodePacked("holesky"))) { + return TestnetChain.HOLESKY; + } + + revert( + string.concat( + "Unsupported testnet network: ", network, ". Supported networks: hoodi, holesky" + ) + ); + } + + /** + * @notice Validate that a contract exists at the given address + */ + function _validateContractExists( + address contractAddress, + string memory contractName + ) internal view { + require( + contractAddress != address(0), string.concat(contractName, " address cannot be zero") + ); + + uint256 codeSize; + assembly { + codeSize := extcodesize(contractAddress) + } + require( + codeSize > 0, + string.concat( + "No contract found at ", contractName, " address: ", vm.toString(contractAddress) + ) + ); + } + + /** + * @notice Get testnet-specific configuration parameters + * @dev Override this function to add testnet-specific logic in the future + */ + function _getTestnetConfig() internal view returns (string memory) { + if (currentTestnet == TestnetChain.HOODI) { + return "hoodi"; + } else if (currentTestnet == TestnetChain.HOLESKY) { + return "holesky"; + } + return "unknown"; + } +} diff --git a/contracts/src/middleware/ServiceManagerBase.sol b/contracts/src/middleware/ServiceManagerBase.sol index 8ba0e2f5..390e8591 100644 --- a/contracts/src/middleware/ServiceManagerBase.sol +++ b/contracts/src/middleware/ServiceManagerBase.sol @@ -428,7 +428,10 @@ abstract contract ServiceManagerBase is ServiceManagerBaseStorage, IAVSRegistrar function getOperatorRestakedStrategies( address operator ) external view virtual returns (address[] memory) { - // TODO: Implement this + // TODO implement + if (operator == address(0)) { + return new address[](0); + } return new address[](0); } diff --git a/test/cli/handlers/contracts/.env.example b/test/cli/handlers/contracts/.env.example new file mode 100644 index 00000000..722016ef --- /dev/null +++ b/test/cli/handlers/contracts/.env.example @@ -0,0 +1,13 @@ +# DataHaven Contrats Deployment Environment Variables +# Copy this file to .env and fill in your values + +# Private key for contract deployment (REQUIRED) +PRIVATE_KEY=0x0000000000000000000000000000000000000000000000000000000000000000 + +# AVS Owner private key (for post-deployment configuration) +AVS_OWNER_PRIVATE_KEY=0x0000000000000000000000000000000000000000000000000000000000000000 + +# Etherscan API key for contract verification (optional) +# Get your API key from: https://etherscan.io/apis +# This is used for automatic contract verification on Hoodi block explorer +# ETHERSCAN_API_KEY=your_etherscan_api_key_here \ No newline at end of file diff --git a/test/cli/handlers/contracts/README.md b/test/cli/handlers/contracts/README.md new file mode 100644 index 00000000..1055c21d --- /dev/null +++ b/test/cli/handlers/contracts/README.md @@ -0,0 +1,72 @@ +# DataHaven Contracts Deployment + +Deploy DataHaven AVS contracts to supported chains (Hoodi, Holesky, Mainnet). + +## What Gets Deployed + +- **DataHaven**: ServiceManager, VetoableSlasher, RewardsRegistry +- **Snowbridge**: BeefyClient, AgentExecutor, Gateway, RewardsAgent +- **EigenLayer**: References existing contracts (not deployed) + +## Prerequisites + +1. **Account Setup**: Create or import an account in Metamask (you'll need the private key) +2. **Funding**: Get native tokens for deployment fees: + - **Hoodi**: Use PoW Faucet at https://hoodi-faucet.pk910.de/#/mine/cc7df92c-9629-4ad8-aaa4-53b1e1c294e8 + - **Holesky**: Use public faucets or bridge from mainnet + - **Mainnet**: Purchase ETH +3. **API Key** (optional): Generate API token from block explorer for contract verification: + - Hoodi: Etherscan-compatible endpoint + - Holesky: https://holesky.etherscan.io/apis + - Mainnet: https://etherscan.io/apis + +## Setup + +```bash +cd test && cp cli/handlers/contracts/.env.example .env +``` + +Edit `.env` with your values: +```bash +# Required: Private key with deployment funds +PRIVATE_KEY=0x... + +# Required: AVS owner private key (can be same as PRIVATE_KEY) +AVS_OWNER_PRIVATE_KEY=0x... + +# Optional: For contract verification +ETHERSCAN_API_KEY=your_api_key_here +``` + +## Deployment Commands + +### Deploy to Hoodi +```bash +bun cli contracts deploy --chain hoodi +``` + +### Deploy to Holesky +```bash +bun cli contracts deploy --chain holesky +``` + +### Deploy to Mainnet +```bash +bun cli contracts deploy --chain mainnet +``` + +### Custom RPC URL +```bash +bun cli contracts deploy --chain hoodi --rpc-url https://your-rpc-url.com +``` + +## Check Deployment Status +```bash +bun cli contracts status --chain hoodi +``` + +## Deployment Files + +Successful deployments create: +- `../contracts/deployments/{chain}.json` - Contract addresses +- `../contracts/deployments/{chain}-rewards-info.json` - Rewards configuration \ No newline at end of file diff --git a/test/cli/handlers/contracts/deploy.ts b/test/cli/handlers/contracts/deploy.ts new file mode 100644 index 00000000..abddeb11 --- /dev/null +++ b/test/cli/handlers/contracts/deploy.ts @@ -0,0 +1,107 @@ +import { logger, printDivider, printHeader } from "utils"; +import { deployContracts } from "../../../scripts/deploy-contracts"; +import { showDeploymentPlanAndStatus } from "./status"; +import { verifyContracts } from "./verify"; + +export const contractsDeploy = async (options: any, command: any) => { + // Try to get chain from options or command + let chain = options.chain; + if (!chain && command.parent) { + chain = command.parent.getOptionValue("chain"); + } + if (!chain) { + chain = command.getOptionValue("chain"); + } + + printHeader(`Deploying DataHaven Contracts to ${chain}`); + + try { + logger.info("šŸš€ Starting deployment..."); + logger.info(`šŸ“” Using chain: ${chain}`); + if (options.rpcUrl) { + logger.info(`šŸ“” Using RPC URL: ${options.rpcUrl}`); + } + + await deployContracts({ + chain: chain, + rpcUrl: options.rpcUrl, + privateKey: options.privateKey + }); + + printDivider(); + } catch (error) { + logger.error(`āŒ Deployment failed: ${error}`); + } +}; + +export const contractsCheck = async (options: any, command: any) => { + // Try to get chain from options or command + let chain = options.chain; + if (!chain && command.parent) { + chain = command.parent.getOptionValue("chain"); + } + if (!chain) { + chain = command.getOptionValue("chain"); + } + + printHeader(`Checking DataHaven ${chain} Configuration and Status`); + + logger.info("šŸ” Showing deployment plan and status"); + + // Use the status function from status.ts + await showDeploymentPlanAndStatus(chain); +}; + +export const contractsVerify = async (options: any, command: any) => { + // Try to get chain from options or command + let chain = options.chain; + if (!chain && command.parent) { + chain = command.parent.getOptionValue("chain"); + } + if (!chain) { + chain = command.getOptionValue("chain"); + } + + printHeader(`Verifying DataHaven Contracts on ${chain} Block Explorer`); + + if (options.skipVerification) { + logger.info("ā­ļø Skipping verification as requested"); + return; + } + + try { + const verifyOptions = { + ...options, + chain: chain + }; + await verifyContracts(verifyOptions); + printDivider(); + } catch (error) { + logger.error(`āŒ Verification failed: ${error}`); + } +}; + +export const contractsPreActionHook = async (thisCommand: any) => { + let chain = thisCommand.getOptionValue("chain"); + + if (!chain && thisCommand.parent) { + chain = thisCommand.parent.getOptionValue("chain"); + } + + const privateKey = thisCommand.getOptionValue("privateKey"); + + if (!chain) { + logger.error("āŒ Chain is required. Use --chain option (hoodi, holesky, mainnet)"); + process.exit(1); + } + + const supportedChains = ["hoodi", "holesky", "mainnet"]; + if (!supportedChains.includes(chain)) { + logger.error(`āŒ Unsupported chain: ${chain}. Supported chains: ${supportedChains.join(", ")}`); + process.exit(1); + } + + if (!privateKey) { + logger.warn("āš ļø Private key not provided. Will use PRIVATE_KEY environment variable"); + } +}; diff --git a/test/cli/handlers/contracts/index.ts b/test/cli/handlers/contracts/index.ts new file mode 100644 index 00000000..d26d528a --- /dev/null +++ b/test/cli/handlers/contracts/index.ts @@ -0,0 +1,3 @@ +export * from "./deploy"; +export * from "./status"; +export * from "./verify"; diff --git a/test/cli/handlers/contracts/status.ts b/test/cli/handlers/contracts/status.ts new file mode 100644 index 00000000..860cbabf --- /dev/null +++ b/test/cli/handlers/contracts/status.ts @@ -0,0 +1,143 @@ +import { logger, printDivider } from "utils"; +import { getChainDeploymentParams, loadChainConfig } from "../../../configs/contracts/config"; +import { checkContractVerification } from "./verify"; + +/** + * Shows the status of chain deployment and verification + */ +export const showDeploymentPlanAndStatus = async (chain: string) => { + try { + const config = await loadChainConfig(chain); + const deploymentParams = getChainDeploymentParams(chain); + + const displayData = { + Network: `${deploymentParams.network} (Chain ID: ${deploymentParams.chainId})`, + "RPC URL": deploymentParams.rpcUrl, + "Block Explorer": deploymentParams.blockExplorer, + "Genesis Time": new Date(deploymentParams.genesisTime * 1000).toISOString(), + "AVS Owner": `${config.avs.avsOwner.slice(0, 10)}...${config.avs.avsOwner.slice(-8)}`, + "Rewards Initiator": `${config.avs.rewardsInitiator.slice(0, 10)}...${config.avs.rewardsInitiator.slice(-8)}`, + "Veto Committee Member": `${config.avs.vetoCommitteeMember.slice(0, 10)}...${config.avs.vetoCommitteeMember.slice(-8)}` + }; + console.table(displayData); + + await showDatahavenContractStatus(chain, deploymentParams.rpcUrl); + await showEigenLayerContractStatus( + config, + deploymentParams.chainId.toString(), + deploymentParams.rpcUrl + ); + + printDivider(); + } catch (error) { + logger.error(`āŒ Failed to load ${chain} configuration: ${error}`); + } +}; + +/** + * Common function to print contract status (deployment + verification) + */ +const printContractStatus = async ( + contract: { name: string; address: string }, + etherscanApiKey?: string, + chainId?: string, + rpcUrl?: string +) => { + if (!contract.address || contract.address === "0x0000000000000000000000000000000000000000") { + logger.info(`āŒ ${contract.name}: Not deployed`); + } else if (!etherscanApiKey) { + logger.info(`āš ļø ${contract.name}: Deployed (${contract.address}) - verification unknown`); + } else { + try { + const isVerified = await checkContractVerification(contract.address, chainId, rpcUrl); + if (isVerified) { + logger.info(`āœ… ${contract.name}: Deployed and verified`); + } else { + logger.warn(`āš ļø ${contract.name}: Deployed but not verified`); + } + } catch (error) { + logger.warn( + `āš ļø ${contract.name}: Deployed but verification check failed with error: ${error}` + ); + } + + // Add small delay to respect rate limits + await new Promise((resolve) => setTimeout(resolve, 200)); + } +}; + +/** + * Shows the status of all contracts (deployment + verification) + */ +const showDatahavenContractStatus = async (chain: string, rpcUrl: string) => { + try { + const contracts = [ + { name: "DataHavenServiceManager", key: "ServiceManagerImplementation" }, + { name: "VetoableSlasher", key: "VetoableSlasher" }, + { name: "RewardsRegistry", key: "RewardsRegistry" }, + { name: "Snowbridge BeefyClient", key: "BeefyClient" }, + { name: "Snowbridge AgentExecutor", key: "AgentExecutor" }, + { name: "Snowbridge Gateway", key: "Gateway" }, + { name: "Snowbridge Agent", key: "RewardsAgent" } + ]; + + logger.info("DataHaven contracts"); + + const deploymentsPath = `../contracts/deployments/${chain}.json`; + const deploymentsFile = Bun.file(deploymentsPath); + const exists = await deploymentsFile.exists(); + + if (!exists) { + contracts.forEach(({ name }) => logger.info(` āŒ ${name}: Not deployed`)); + return; + } + + const deployments = await deploymentsFile.json(); + const etherscanApiKey = process.env.ETHERSCAN_API_KEY; + + for (const contract of contracts) { + const address = deployments[contract.key]; + await printContractStatus({ name: contract.name, address }, etherscanApiKey, chain, rpcUrl); + } + } catch (error) { + logger.warn(`āš ļø Could not check contract status: ${error}`); + } +}; + +/** + * Shows the status of EigenLayer contracts (verification only) + */ +const showEigenLayerContractStatus = async (config: any, chainId: string, rpcUrl: string) => { + try { + const contracts = [ + { + name: "DelegationManager", + address: config.eigenLayer.delegationManager + }, + { name: "StrategyManager", address: config.eigenLayer.strategyManager }, + { name: "EigenPodManager", address: config.eigenLayer.eigenPodManager }, + { name: "AVSDirectory", address: config.eigenLayer.avsDirectory }, + { + name: "RewardsCoordinator", + address: config.eigenLayer.rewardsCoordinator + }, + { + name: "AllocationManager", + address: config.eigenLayer.allocationManager + }, + { + name: "PermissionController", + address: config.eigenLayer.permissionController + } + ]; + + logger.info("EigenLayer contracts status:"); + const etherscanApiKey = process.env.ETHERSCAN_API_KEY; + + for (const contract of contracts) { + await printContractStatus(contract, etherscanApiKey, chainId, rpcUrl); + } + } catch (error) { + logger.warn(`āš ļø Could not check EigenLayer contract status: ${error}`); + } +}; diff --git a/test/cli/handlers/contracts/verify.ts b/test/cli/handlers/contracts/verify.ts new file mode 100644 index 00000000..68cf363c --- /dev/null +++ b/test/cli/handlers/contracts/verify.ts @@ -0,0 +1,245 @@ +import { execSync } from "node:child_process"; +import { logger } from "utils"; +import { parseDeploymentsFile } from "utils/contracts"; +import { CHAIN_CONFIGS, getChainConfig } from "../../../configs/contracts/config"; + +interface ContractsVerifyOptions { + chain: string; + rpcUrl?: string; + skipVerification: boolean; +} + +interface ContractToVerify { + name: string; + address: string; + artifactName: string; + constructorArgs: string[]; + constructorArgTypes: string[]; +} + +/** + * 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; + } + + logger.info(`šŸ” Verifying contracts on ${options.chain} 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(options.chain); + + const contractsToVerify: ContractToVerify[] = [ + { + name: "ServiceManager Implementation", + address: deployments.ServiceManagerImplementation, + artifactName: "DataHavenServiceManager", + constructorArgs: [ + deployments.RewardsCoordinator, + deployments.PermissionController, + deployments.AllocationManager + ], + constructorArgTypes: ["address", "address", "address"] + }, + { + name: "VetoableSlasher", + address: deployments.VetoableSlasher, + artifactName: "VetoableSlasher", + constructorArgs: [ + deployments.AllocationManager, + deployments.ServiceManager, + "0x0000000000000000000000000000000000000000", + "0" + ], + constructorArgTypes: ["address", "address", "address", "uint32"] + }, + { + name: "RewardsRegistry", + address: deployments.RewardsRegistry, + artifactName: "RewardsRegistry", + constructorArgs: [deployments.ServiceManager, deployments.RewardsAgent], + constructorArgTypes: ["address", "address"] + }, + { + name: "Gateway", + address: deployments.Gateway, + artifactName: "Gateway", + constructorArgs: [], + constructorArgTypes: [] + }, + { + name: "BeefyClient", + address: deployments.BeefyClient, + artifactName: "BeefyClient", + constructorArgs: [], + constructorArgTypes: [] + }, + { + name: "AgentExecutor", + address: deployments.AgentExecutor, + artifactName: "AgentExecutor", + constructorArgs: [], + constructorArgTypes: [] + } + ]; + + 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, constructorArgs: args, constructorArgTypes: types } = contract; + + const abiEncodedArgs = getEncodedConstructorArgs(args, types); + const 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} src/${artifactName}.sol:${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} src/${contract.artifactName}.sol:${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 => { + 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 => { + 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 => { + 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 !== ""; +}; diff --git a/test/cli/handlers/deploy/contracts.ts b/test/cli/handlers/deploy/contracts.ts index 62f613ff..73248624 100644 --- a/test/cli/handlers/deploy/contracts.ts +++ b/test/cli/handlers/deploy/contracts.ts @@ -8,7 +8,9 @@ import { logger, printDivider, printHeader } from "utils"; import type { ParameterCollection } from "utils/parameters"; interface DeployContractsOptions { + chain?: string; rpcUrl: string; + privateKey?: string | undefined; verified?: boolean; blockscoutBackendUrl?: string; parameterCollection?: ParameterCollection; @@ -42,7 +44,7 @@ export const deployContracts = async (options: DeployContractsOptions) => { // Construct and execute deployment const deployCommand = constructDeployCommand(options); - await executeDeployment(deployCommand, options.parameterCollection); + await executeDeployment(deployCommand, options.parameterCollection, options.chain); printDivider(); }; diff --git a/test/cli/handlers/index.ts b/test/cli/handlers/index.ts index 3bc4b87f..f4f23b53 100644 --- a/test/cli/handlers/index.ts +++ b/test/cli/handlers/index.ts @@ -1,4 +1,5 @@ export * from "./common"; +export * from "./contracts"; export * from "./deploy"; export * from "./exec"; export * from "./launch"; diff --git a/test/cli/handlers/launch/contracts.ts b/test/cli/handlers/launch/contracts.ts index 6380412c..b6c5d049 100644 --- a/test/cli/handlers/launch/contracts.ts +++ b/test/cli/handlers/launch/contracts.ts @@ -4,6 +4,7 @@ import { deployContracts as deployContractsCore } from "../../../launcher/contra interface DeployContractsOptions { rpcUrl: string; + privateKey?: string | undefined; verified?: boolean; blockscoutBackendUrl?: string; deployContracts?: boolean; diff --git a/test/cli/index.ts b/test/cli/index.ts index fc3d4a5c..8dafc0b6 100644 --- a/test/cli/index.ts +++ b/test/cli/index.ts @@ -2,6 +2,10 @@ import { Command, InvalidArgumentError } from "@commander-js/extra-typings"; import type { DeployEnvironment } from "utils"; import { + contractsCheck, + contractsDeploy, + contractsPreActionHook, + contractsVerify, deploy, deployPreActionHook, launch, @@ -172,6 +176,69 @@ program .hook("preAction", stopPreActionHook) .action(stop); +// ===== Contracts ====== +const contractsCommand = program + .command("contracts") + .addHelpText( + "before", + `šŸ«Ž DataHaven: Contracts Deployment CLI for deploying DataHaven AVS contracts to supported chains + + Commands: + - status: Show deployment plan, configuration, and status (default) + - deploy: Deploy contracts to specified chain + - verify: Verify deployed contracts on block explorer + + Common options: + --chain: Target chain (required: hoodi, holesky, mainnet) + --rpc-url: Chain RPC URL (optional, defaults based on chain) + --private-key: Private key for deployment + --skip-verification: Skip contract verification + ` + ) + .description("Deploy and manage DataHaven AVS contracts on supported chains"); + +// Contracts Check (default) +contractsCommand + .command("status") + .description("Show deployment plan, configuration, and status") + .option("--chain ", "Target chain (hoodi, holesky, mainnet)") + .option("--rpc-url ", "Chain RPC URL (optional, defaults based on chain)") + .option("--private-key ", "Private key for deployment", process.env.PRIVATE_KEY || "") + .option("--skip-verification", "Skip contract verification", false) + .hook("preAction", contractsPreActionHook) + .action(contractsCheck); + +// Contracts Deploy +contractsCommand + .command("deploy") + .description("Deploy DataHaven AVS contracts to specified chain") + .option("--chain ", "Target chain (hoodi, holesky, mainnet)") + .option("--rpc-url ", "Chain RPC URL (optional, defaults based on chain)") + .option("--private-key ", "Private key for deployment", process.env.PRIVATE_KEY || "") + .option("--skip-verification", "Skip contract verification", false) + .hook("preAction", contractsPreActionHook) + .action(contractsDeploy); + +// Contracts Verify +contractsCommand + .command("verify") + .description("Verify deployed contracts on block explorer") + .option("--chain ", "Target chain (hoodi, holesky, mainnet)") + .option("--rpc-url ", "Chain RPC URL (optional, defaults based on chain)") + .option("--skip-verification", "Skip contract verification", false) + .hook("preAction", contractsPreActionHook) + .action(contractsVerify); + +// Default Contracts command (runs check) +contractsCommand + .description("Show deployment plan, configuration, and status") + .option("--chain ", "Target chain (hoodi, holesky, mainnet)") + .option("--rpc-url ", "Chain RPC URL (optional, defaults based on chain)") + .option("--private-key ", "Private key for deployment", process.env.PRIVATE_KEY || "") + .option("--skip-verification", "Skip contract verification", false) + .hook("preAction", contractsPreActionHook) + .action(contractsCheck); + // ===== Exec ====== // Disabled until need arises // program diff --git a/test/configs/contracts/config.ts b/test/configs/contracts/config.ts new file mode 100644 index 00000000..30c0f85f --- /dev/null +++ b/test/configs/contracts/config.ts @@ -0,0 +1,85 @@ +import { logger } from "utils"; + +/** + * Chain-specific configuration constants + */ +export const CHAIN_CONFIGS = { + hoodi: { + NETWORK_NAME: "hoodi", + CHAIN_ID: 560048, + RPC_URL: "https://rpc.hoodi.ethpandaops.io", + BLOCK_EXPLORER: "https://hoodi.etherscan.io/", + GENESIS_TIME: 1710666600, + SLOT_TIME: 12, // seconds + EPOCHS_PER_SYNC_COMMITTEE_PERIOD: 256, + SYNC_COMMITTEE_SIZE: 512 + }, + holesky: { + NETWORK_NAME: "holesky", + CHAIN_ID: 17000, + RPC_URL: "https://ethereum-holesky-rpc.publicnode.com", + BLOCK_EXPLORER: "https://holesky.etherscan.io/", + GENESIS_TIME: 1695902400, + SLOT_TIME: 12, // seconds + EPOCHS_PER_SYNC_COMMITTEE_PERIOD: 256, + SYNC_COMMITTEE_SIZE: 512 + }, + mainnet: { + NETWORK_NAME: "mainnet", + CHAIN_ID: 1, + RPC_URL: "https://eth.llamarpc.com", + BLOCK_EXPLORER: "https://etherscan.io/", + GENESIS_TIME: 1606824023, + SLOT_TIME: 12, // seconds + EPOCHS_PER_SYNC_COMMITTEE_PERIOD: 256, + SYNC_COMMITTEE_SIZE: 512 + }, + anvil: { + NETWORK_NAME: "anvil", + CHAIN_ID: 31337, + RPC_URL: "http://localhost:8545", + BLOCK_EXPLORER: "https://etherscan.io/", + GENESIS_TIME: 1606824023 + } +}; + +export type ChainConfigType = typeof CHAIN_CONFIGS; + +export const getChainConfig = (chain: string) => { + return CHAIN_CONFIGS[chain as keyof ChainConfigType]; +}; + +export const loadChainConfig = async (chain: string) => { + try { + const configPath = `../contracts/config/${chain}.json`; + const configFile = Bun.file(configPath); + + if (!(await configFile.exists())) { + throw new Error(`${chain} configuration file not found at ${configPath}`); + } + + const configContent = await configFile.text(); + const config = JSON.parse(configContent); + + logger.debug(`āœ… ${chain} configuration loaded successfully`); + return config; + } catch (error) { + logger.error(`āŒ Failed to load ${chain} configuration: ${error}`); + throw error; + } +}; + +export const getChainDeploymentParams = (chain?: string) => { + let chainConfig = CHAIN_CONFIGS[chain as keyof typeof CHAIN_CONFIGS]; + if (!chainConfig) { + chainConfig = CHAIN_CONFIGS.anvil; + } + + return { + network: chainConfig.NETWORK_NAME, + chainId: chainConfig.CHAIN_ID, + rpcUrl: chainConfig.RPC_URL, + blockExplorer: chainConfig.BLOCK_EXPLORER, + genesisTime: chainConfig.GENESIS_TIME + }; +}; diff --git a/test/launcher/contracts.ts b/test/launcher/contracts.ts index 3f7b908e..63548093 100644 --- a/test/launcher/contracts.ts +++ b/test/launcher/contracts.ts @@ -1,6 +1,7 @@ import { buildContracts, constructDeployCommand, + deployContracts as deployContractsCore, executeDeployment, validateDeploymentParams } from "scripts/deploy-contracts"; @@ -11,7 +12,9 @@ import type { ParameterCollection } from "utils/parameters"; * Configuration options for contract deployment. */ export interface ContractsOptions { - rpcUrl: string; + chain?: string; + rpcUrl?: string; + privateKey?: string | undefined; verified?: boolean; blockscoutBackendUrl?: string; parameterCollection?: ParameterCollection; @@ -29,6 +32,7 @@ export interface ContractsOptions { * - Automatically adding deployed contract addresses to parameter collection if provided * * @param options - Configuration options for deployment + * @param options.chain - The network to deploy to (optional, defaults to local deployment) * @param options.rpcUrl - The RPC URL of the target network * @param options.verified - Whether to verify contracts on Blockscout (requires blockscoutBackendUrl) * @param options.blockscoutBackendUrl - URL for the Blockscout API (required if verified is true) @@ -41,15 +45,25 @@ export interface ContractsOptions { export const deployContracts = async (options: ContractsOptions): Promise => { logger.info("šŸš€ Deploying smart contracts..."); - // Validate required parameters - validateDeploymentParams(options); + if (options.parameterCollection) { + // Validate required parameters + validateDeploymentParams(options); - // Build contracts - await buildContracts(); + // Build contracts + await buildContracts(); - // Construct and execute deployment - const deployCommand = constructDeployCommand(options); - await executeDeployment(deployCommand, options.parameterCollection); + // Construct and execute deployment with parameter collection + const deployCommand = constructDeployCommand(options); + await executeDeployment(deployCommand, options.parameterCollection, options.chain); + } else { + await deployContractsCore({ + chain: options.chain || "anvil", + rpcUrl: options.rpcUrl, + privateKey: options.privateKey, + verified: options.verified, + blockscoutBackendUrl: options.blockscoutBackendUrl + }); + } logger.success("Smart contracts deployed successfully"); }; diff --git a/test/launcher/relayers.ts b/test/launcher/relayers.ts index 6470f6b3..23026c4c 100644 --- a/test/launcher/relayers.ts +++ b/test/launcher/relayers.ts @@ -433,10 +433,10 @@ export const launchRelayers = async ( // Check if BEEFY is ready before proceeding await waitBeefyReady(launchedNetwork, 2000, 60000); - const anvilDeployments = await parseDeploymentsFile(); - const beefyClientAddress = anvilDeployments.BeefyClient; - const gatewayAddress = anvilDeployments.Gateway; - const rewardsRegistryAddress = anvilDeployments.RewardsRegistry; + const deployments = await parseDeploymentsFile(); + const beefyClientAddress = deployments.BeefyClient; + const gatewayAddress = deployments.Gateway; + const rewardsRegistryAddress = deployments.RewardsRegistry; invariant(beefyClientAddress, "āŒ BeefyClient address not found in anvil.json"); invariant(gatewayAddress, "āŒ Gateway address not found in anvil.json"); invariant(rewardsRegistryAddress, "āŒ RewardsRegistry address not found in anvil.json"); diff --git a/test/scripts/deploy-contracts.ts b/test/scripts/deploy-contracts.ts index 581504dc..b2e6de90 100644 --- a/test/scripts/deploy-contracts.ts +++ b/test/scripts/deploy-contracts.ts @@ -1,4 +1,5 @@ import { $ } from "bun"; +import { CHAIN_CONFIGS } from "configs/contracts/config"; import invariant from "tiny-invariant"; import { logger, @@ -9,7 +10,9 @@ import { import type { ParameterCollection } from "utils/parameters"; interface ContractDeploymentOptions { - rpcUrl: string; + chain?: string; + rpcUrl?: string; + privateKey?: string | undefined; verified?: boolean; blockscoutBackendUrl?: string; } @@ -48,9 +51,21 @@ export const buildContracts = async () => { * Constructs the deployment command */ export const constructDeployCommand = (options: ContractDeploymentOptions): string => { - const { rpcUrl, verified, blockscoutBackendUrl } = options; + const { chain, rpcUrl, verified, blockscoutBackendUrl } = options; - let deployCommand = `forge script script/deploy/DeployLocal.s.sol --rpc-url ${rpcUrl} --color never -vv --no-rpc-rate-limit --non-interactive --broadcast`; + const deploymentScript = + !chain || chain === "anvil" || chain === "local" + ? "script/deploy/DeployLocal.s.sol" + : "script/deploy/DeployTestnet.s.sol"; + + logger.info(`šŸš€ Deploying contracts to ${chain} using ${deploymentScript}`); + + let deployCommand = `forge script ${deploymentScript} --rpc-url ${rpcUrl} --color never -vv --no-rpc-rate-limit --non-interactive --broadcast`; + + // Add environment variable for chain if specified + if (chain) { + deployCommand = `NETWORK=${chain} ${deployCommand}`; + } if (verified && blockscoutBackendUrl) { // TODO: Allow for other verifiers like Etherscan. @@ -63,10 +78,12 @@ export const constructDeployCommand = (options: ContractDeploymentOptions): stri /** * Executes contract deployment + * Supports multiple calling patterns for backwards compatibility: */ export const executeDeployment = async ( deployCommand: string, - parameterCollection?: ParameterCollection + parameterCollection?: ParameterCollection, + chain?: string ) => { logger.info("āŒ›ļø Deploying contracts (this might take a few minutes)..."); @@ -81,8 +98,8 @@ export const executeDeployment = async ( // and add it to parameters if collection is provided if (parameterCollection) { try { - const deployments = await parseDeploymentsFile(); - const rewardsInfo = await parseRewardsInfoFile(); + const deployments = await parseDeploymentsFile(chain); + const rewardsInfo = await parseRewardsInfoFile(chain); const gatewayAddress = deployments.Gateway; const rewardsRegistryAddress = deployments.RewardsRegistry; const rewardsAgentOrigin = rewardsInfo.RewardsAgentOrigin; @@ -138,6 +155,46 @@ export const executeDeployment = async ( logger.success("Contracts deployed successfully"); }; +/** + * Main function to deploy contracts with simplified interface + * This is the main entry point for CLI handlers + */ +export const deployContracts = async (options: { + chain: string; + rpcUrl?: string; + privateKey?: string | undefined; + verified?: boolean; + blockscoutBackendUrl?: string; +}) => { + const chainConfig = CHAIN_CONFIGS[options.chain as keyof typeof CHAIN_CONFIGS]; + + if (!chainConfig) { + throw new Error(`Unsupported chain: ${options.chain}`); + } + + const finalRpcUrl = options.rpcUrl || chainConfig.RPC_URL; + + const deploymentOptions: ContractDeploymentOptions = { + chain: options.chain, + rpcUrl: finalRpcUrl, + privateKey: options.privateKey, + verified: options.verified, + blockscoutBackendUrl: options.blockscoutBackendUrl + }; + + // Validate parameters + validateDeploymentParams(deploymentOptions); + + // Build contracts + await buildContracts(); + + // Construct and execute deployment + const deployCommand = constructDeployCommand(deploymentOptions); + await executeDeployment(deployCommand); + + logger.success(`DataHaven contracts deployed successfully to ${options.chain}`); +}; + // Allow script to be run directly with CLI arguments if (import.meta.main) { const args = process.argv.slice(2); @@ -147,12 +204,19 @@ if (import.meta.main) { invariant(rpcUrlIndex !== -1, "āŒ --rpc-url flag is required"); invariant(rpcUrlIndex + 1 < args.length, "āŒ --rpc-url flag requires an argument"); + // Extract private key + const privateKeyIndex = args.indexOf("--private-key"); + invariant(privateKeyIndex !== -1, "āŒ --private-key flag is required"); + invariant(privateKeyIndex + 1 < args.length, "āŒ --private-key flag requires an argument"); + const options: { rpcUrl: string; + privateKey: string; verified: boolean; blockscoutBackendUrl?: string; } = { rpcUrl: args[rpcUrlIndex + 1], + privateKey: args[privateKeyIndex + 1], verified: args.includes("--verified") }; diff --git a/test/utils/contracts.ts b/test/utils/contracts.ts index 2350f46e..717b9c70 100644 --- a/test/utils/contracts.ts +++ b/test/utils/contracts.ts @@ -22,29 +22,30 @@ const DeployedStrategySchema = z.object({ tokenCreator: ethAddress }); -const AnvilDeploymentsSchema = z.object({ +const DeploymentsSchema = z.object({ network: z.string(), BeefyClient: ethAddressCustom, AgentExecutor: ethAddressCustom, Gateway: ethAddressCustom, ServiceManager: ethAddressCustom, + ServiceManagerImplementation: ethAddressCustom, VetoableSlasher: ethAddressCustom, RewardsRegistry: ethAddressCustom, RewardsAgent: ethAddressCustom, DelegationManager: ethAddressCustom, StrategyManager: ethAddressCustom, AVSDirectory: ethAddressCustom, - EigenPodManager: ethAddressCustom, - EigenPodBeacon: ethAddressCustom, + EigenPodManager: ethAddressCustom.optional(), + EigenPodBeacon: ethAddressCustom.optional(), RewardsCoordinator: ethAddressCustom, AllocationManager: ethAddressCustom, PermissionController: ethAddressCustom, - ETHPOSDeposit: ethAddressCustom, - BaseStrategyImplementation: ethAddressCustom, - DeployedStrategies: z.array(DeployedStrategySchema) + ETHPOSDeposit: ethAddressCustom.optional(), + BaseStrategyImplementation: ethAddressCustom.optional(), + DeployedStrategies: z.array(DeployedStrategySchema).optional() }); -export type AnvilDeployments = z.infer; +export type Deployments = z.infer; const RewardsInfoSchema = z.object({ RewardsAgent: ethAddressCustom, @@ -54,39 +55,40 @@ const RewardsInfoSchema = z.object({ export type RewardsInfo = z.infer; -export const parseDeploymentsFile = async (): Promise => { - const anvilDeploymentsPath = "../contracts/deployments/anvil.json"; - const anvilDeploymentsFile = Bun.file(anvilDeploymentsPath); - if (!(await anvilDeploymentsFile.exists())) { - logger.error(`File ${anvilDeploymentsPath} does not exist`); - throw new Error("Error reading anvil deployments file"); +export const parseDeploymentsFile = async (network = "anvil"): Promise => { + const deploymentsPath = `../contracts/deployments/${network}.json`; + const deploymentsFile = Bun.file(deploymentsPath); + if (!(await deploymentsFile.exists())) { + logger.error(`File ${deploymentsPath} does not exist`); + throw new Error(`Error reading ${network} deployments file`); } - const anvilDeploymentsJson = await anvilDeploymentsFile.json(); + const deploymentsJson = await deploymentsFile.json(); + logger.info(`Deployments: ${JSON.stringify(deploymentsJson, null, 2)}`); try { - const parsedDeployments = AnvilDeploymentsSchema.parse(anvilDeploymentsJson); - logger.debug("Successfully parsed anvil deployments file."); + const parsedDeployments = DeploymentsSchema.parse(deploymentsJson); + logger.debug(`Successfully parsed ${network} deployments file.`); return parsedDeployments; } catch (error) { - logger.error("Failed to parse anvil deployments file:", error); - throw new Error("Invalid anvil deployments file format"); + logger.error(`Failed to parse ${network} deployments file:`, error); + throw new Error(`Invalid ${network} deployments file format`); } }; -export const parseRewardsInfoFile = async (): Promise => { - const rewardsInfoPath = "../contracts/deployments/anvil-rewards-info.json"; +export const parseRewardsInfoFile = async (network = "anvil"): Promise => { + const rewardsInfoPath = `../contracts/deployments/${network}-rewards-info.json`; const rewardsInfoFile = Bun.file(rewardsInfoPath); if (!(await rewardsInfoFile.exists())) { logger.error(`File ${rewardsInfoPath} does not exist`); - throw new Error("Error reading rewards info file"); + throw new Error(`Error reading ${network} rewards info file`); } const rewardsInfoJson = await rewardsInfoFile.json(); try { const parsedRewardsInfo = RewardsInfoSchema.parse(rewardsInfoJson); - logger.debug("Successfully parsed rewards info file."); + logger.debug(`Successfully parsed ${network} rewards info file.`); return parsedRewardsInfo; } catch (error) { - logger.error("Failed to parse rewards info file:", error); - throw new Error("Invalid rewards info file format"); + logger.error(`Failed to parse ${network} rewards info file:`, error); + throw new Error(`Invalid ${network} rewards info file format`); } }; @@ -96,6 +98,7 @@ const abiMap = { AgentExecutor: generated.agentExecutorAbi, Gateway: generated.gatewayAbi, ServiceManager: generated.dataHavenServiceManagerAbi, + ServiceManagerImplementation: generated.dataHavenServiceManagerAbi, VetoableSlasher: generated.vetoableSlasherAbi, RewardsRegistry: generated.rewardsRegistryAbi, RewardsAgent: generated.agentAbi, @@ -110,7 +113,7 @@ const abiMap = { ETHPOSDeposit: generated.iethposDepositAbi, BaseStrategyImplementation: generated.strategyBaseTvlLimitsAbi, DeployedStrategies: erc20Abi -} as const satisfies Record, Abi>; +} as const satisfies Record, Abi>; type ContractName = keyof typeof abiMap; type AbiFor = (typeof abiMap)[C]; @@ -121,9 +124,10 @@ export type ContractInstance = Awaited< // TODO: make this work with DeployedStrategies export const getContractInstance = async ( contract: C, - viemClient?: ViemClientInterface + viemClient?: ViemClientInterface, + network = "anvil" ) => { - const deployments = await parseDeploymentsFile(); + const deployments = await parseDeploymentsFile(network); const contractAddress = deployments[contract]; logger.debug(`Contract ${contract} deployed to ${contractAddress}`);