feat: Datahaven contracts deployment on public testnet (#123)

## 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 <hoodi | holesky |
mainnet>`
- 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
```


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## 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.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com>
Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com>
This commit is contained in:
Gonza Montiel 2025-08-21 12:02:31 +02:00 committed by GitHub
parent 9ce0a94979
commit 5121ae002b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 1931 additions and 508 deletions

View file

@ -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": {

View file

@ -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"
]
}
}

View file

@ -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"
]
}
}

View file

@ -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"}

View file

@ -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"}

View file

@ -111,6 +111,7 @@
[rpc_endpoints]
mainnet = "${RPC_MAINNET}"
holesky = "${RPC_HOLESKY}"
hoodi = "https://rpc.hoodi.ethpandaops.io"
anvil = "http://localhost:8545"
# [etherscan]

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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;
}

View file

@ -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";
}
}

View file

@ -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);
}

View file

@ -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

View file

@ -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

View file

@ -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");
}
};

View file

@ -0,0 +1,3 @@
export * from "./deploy";
export * from "./status";
export * from "./verify";

View file

@ -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}`);
}
};

View file

@ -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<boolean> => {
try {
const apiKey = process.env.ETHERSCAN_API_KEY;
if (!apiKey) throw new Error("ETHERSCAN_API_KEY not found");
// Try to get implementation address for proxy contracts
if (rpcUrl) {
const implAddress = await getProxyImplementation(contractAddress, rpcUrl);
if (implAddress && implAddress !== contractAddress) {
const implVerified = await isVerified(implAddress, chain, apiKey);
if (implVerified) return true;
}
}
// Check the original contract
return await isVerified(contractAddress, chain, apiKey);
} catch (error) {
logger.warn(`Failed to check verification status for ${contractAddress}: ${error}`);
return false;
}
};
const getProxyImplementation = async (address: string, rpcUrl: string): Promise<string | null> => {
try {
const response = await fetch(rpcUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
method: "eth_getStorageAt",
params: [
address,
"0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc",
"latest"
],
id: 1
})
});
const data = (await response.json()) as any;
return data.result ? `0x${data.result.slice(-40)}` : null;
} catch {
return null;
}
};
const isVerified = async (
address: string,
chain: string | undefined,
apiKey: string
): Promise<boolean> => {
if (!chain) {
return false;
}
const response = await fetch(
`https://api.etherscan.io/v2/api?module=contract&action=getsourcecode&address=${address}&chainid=${chain}&apikey=${apiKey}`
);
const data = (await response.json()) as any;
return data.result?.[0]?.SourceCode && data.result[0].SourceCode !== "";
};

View file

@ -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();
};

View file

@ -1,4 +1,5 @@
export * from "./common";
export * from "./contracts";
export * from "./deploy";
export * from "./exec";
export * from "./launch";

View file

@ -4,6 +4,7 @@ import { deployContracts as deployContractsCore } from "../../../launcher/contra
interface DeployContractsOptions {
rpcUrl: string;
privateKey?: string | undefined;
verified?: boolean;
blockscoutBackendUrl?: string;
deployContracts?: boolean;

View file

@ -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 <value>", "Target chain (hoodi, holesky, mainnet)")
.option("--rpc-url <value>", "Chain RPC URL (optional, defaults based on chain)")
.option("--private-key <value>", "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 <value>", "Target chain (hoodi, holesky, mainnet)")
.option("--rpc-url <value>", "Chain RPC URL (optional, defaults based on chain)")
.option("--private-key <value>", "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 <value>", "Target chain (hoodi, holesky, mainnet)")
.option("--rpc-url <value>", "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 <value>", "Target chain (hoodi, holesky, mainnet)")
.option("--rpc-url <value>", "Chain RPC URL (optional, defaults based on chain)")
.option("--private-key <value>", "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

View file

@ -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
};
};

View file

@ -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<void> => {
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");
};

View file

@ -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");

View file

@ -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")
};

View file

@ -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<typeof AnvilDeploymentsSchema>;
export type Deployments = z.infer<typeof DeploymentsSchema>;
const RewardsInfoSchema = z.object({
RewardsAgent: ethAddressCustom,
@ -54,39 +55,40 @@ const RewardsInfoSchema = z.object({
export type RewardsInfo = z.infer<typeof RewardsInfoSchema>;
export const parseDeploymentsFile = async (): Promise<AnvilDeployments> => {
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<Deployments> => {
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<RewardsInfo> => {
const rewardsInfoPath = "../contracts/deployments/anvil-rewards-info.json";
export const parseRewardsInfoFile = async (network = "anvil"): Promise<RewardsInfo> => {
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<keyof Omit<AnvilDeployments, "network" | "RewardsAgentOrigin">, Abi>;
} as const satisfies Record<keyof Omit<Deployments, "network" | "RewardsAgentOrigin">, Abi>;
type ContractName = keyof typeof abiMap;
type AbiFor<C extends ContractName> = (typeof abiMap)[C];
@ -121,9 +124,10 @@ export type ContractInstance<C extends ContractName> = Awaited<
// TODO: make this work with DeployedStrategies
export const getContractInstance = async <C extends ContractName>(
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}`);