datahaven/contracts/script/deploy/DeployBase.s.sol
Steve Degosserie 46d752da01
feat: Add DH-AVS stagenet/testnet Hoodi deployment support (#422)
## Summary

- Add multi-environment deployment support (stagenet, testnet, mainnet)
to CLI and contracts
- Configure stagenet and testnet runtimes with correct genesis hashes
and Snowbridge Agent IDs
- Add CLI commands for BEEFY checkpoint updates and rewards origin
computation
- Add ETH validator strategies (native beacon chain ETH + LSTs) to all
config files

## Changes

### Runtime Configuration

**Stagenet Runtime:**
- Set `StagenetGenesisHash` to DataHaven stagenet genesis hash
- Configure `RewardsAgentOrigin` with computed Snowbridge Agent ID
- Add tests verifying rewards account derivation and agent ID
computation

**Testnet Runtime:**
- Set `TestnetGenesisHash` to DataHaven testnet genesis hash
- Configure `RewardsAgentOrigin` with computed Snowbridge Agent ID
- Add tests verifying rewards account derivation and agent ID
computation

The Rewards Agent ID is computed following Snowbridge's location
description pattern:
```
blake2_256(SCALE_ENCODE("GlobalConsensus", ByGenesis(genesis), "AccountKey20", rewards_account))
```

### CLI Enhancements

- All contracts subcommands (`status`, `deploy`, `verify`,
`update-metadata`) now accept `--environment` option
- Config and deployment files use environment-prefixed naming (e.g.,
`stagenet-hoodi.json`, `testnet-hoodi.json`)
- New `update-beefy-checkpoint` command that:
  - Connects to a live DataHaven chain via WebSocket RPC
  - Fetches all BEEFY data at the same finalized block for consistency
  - Uses parallel queries with `Promise.all` for better performance
- Computes authority hashes (keccak256 of Ethereum addresses derived
from BEEFY public keys)
- Uses Snowbridge's quorum formula `n - floor((n-1)/3)` for strictly >
2/3 majority
- New `update-rewards-origin` command that computes the Snowbridge Agent
ID for the rewards pallet
- Centralized validation via `contractsPreActionHook` for all contract
commands
- Environment validation against allowlist (`stagenet`, `testnet`,
`mainnet`)

### Contract Changes

- Network validation uses explicit allowlist instead of suffix matching
- Added `initialValidatorSetId` and `nextValidatorSetId` fields to
`SnowbridgeConfig` struct
- `DeployBase.s.sol` now uses config values for validator set IDs
instead of hardcoded 0/1
- `DeployParams.s.sol` loads validator set IDs from config with
backwards compatibility

### Validator Strategies

Added ETH-equivalent strategies to allow validators to stake using
native ETH or LSTs:

**All Networks:**
- `0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0` - Native beacon chain ETH
(virtual strategy)

**Hoodi Testnet:**
- `0xf8a1a66130d614c7360e868576d5e59203475fe0` - stETH
- `0x24579aD4fe83aC53546E5c2D3dF5F85D6383420d` - WETH

**Ethereum Mainnet:**
- `0x93c4b944D05dfe6df7645A86cd2206016c51564D` - stETH
- `0x1BeE69b7dFFfA4E2d53C2a2Df135C388AD25dCD2` - rETH
- `0x54945180dB7943c0ed0FEE7EdaB2Bd24620256bc` - cbETH

### Config Files

- `stagenet-hoodi.json` - Hoodi testnet with stagenet EigenLayer
addresses
- `testnet-hoodi.json` - Hoodi testnet with testnet EigenLayer addresses
- `mainnet-ethereum.json` - Ethereum mainnet with mainnet EigenLayer
addresses
- Removed `hoodi.json` (replaced by environment-prefixed files)

## Usage

```bash
# Deploy to stagenet on Hoodi
bun cli contracts deploy --chain hoodi --environment stagenet

# Update BEEFY checkpoint from live chain
bun cli contracts update-beefy-checkpoint \
  --chain hoodi \
  --environment stagenet \
  --rpc-url wss://services.datahaven-dev.network/stagenet

# Compute rewards origin for a chain
bun cli contracts update-rewards-origin \
  --chain hoodi \
  --environment stagenet \
  --rpc-url wss://services.datahaven-dev.network/stagenet

# Check deployment status
bun cli contracts status --chain hoodi --environment stagenet
```

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 16:41:15 +01:00

326 lines
13 KiB
Solidity

// 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 {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";
// 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 {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;
bool internal _txExecutionEnabled;
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 {
_txExecutionEnabled = vm.envOr("TX_EXECUTION", true);
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));
if (!_txExecutionEnabled) {
Logging.logInfo("TX EXECUTION DISABLED: owner transactions must be executed manually");
}
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)
(
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
) = _deployDataHavenContracts(avsConfig, proxyAdmin, gateway);
Logging.logFooter();
_logProgress();
// Final configuration (same for both modes)
Logging.logHeader("FINAL CONFIGURATION");
Logging.logContractDeployed("Rewards Agent Address", rewardsAgentAddress);
Logging.logFooter();
_logProgress();
// Output deployment info
_outputDeployedAddresses(
beefyClient,
agentExecutor,
gateway,
serviceManager,
serviceManagerImplementation,
rewardsAgentAddress
);
_outputRewardsAgentInfo(rewardsAgentAddress, snowbridgeConfig.rewardsMessageOrigin);
}
/**
* @notice Deploy Snowbridge components (shared across all deployment types)
*/
function _deploySnowbridge(
SnowbridgeConfig memory config
) internal returns (BeefyClient, AgentExecutor, IGatewayV2, address payable) {
Logging.logHeader("SNOWBRIDGE DEPLOYMENT");
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(
config.initialValidatorSetId, config.initialValidatorHashes
);
BeefyClient.ValidatorSet memory nextValidatorSet = ValidatorsUtils._buildValidatorSet(
config.nextValidatorSetId, 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) {
Logging.logHeader("DATAHAVEN CUSTOM CONTRACTS DEPLOYMENT");
// Deploy the Service Manager
vm.broadcast(_deployerPrivateKey);
DataHavenServiceManager serviceManagerImplementation =
new DataHavenServiceManager(rewardsCoordinator, 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));
Logging.logSection("Configuring Service Manager");
// Register the DataHaven service in the AllocationManager
if (_txExecutionEnabled) {
vm.broadcast(_avsOwnerPrivateKey);
serviceManager.updateAVSMetadataURI("");
Logging.logStep("DataHaven service registered in AllocationManager");
} else {
Logging.logInfo("TX EXECUTION DISABLED: call updateAVSMetadataURI via multisig");
}
return (serviceManager, serviceManagerImplementation);
}
/**
* @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,
address rewardsAgent
) internal virtual;
/**
* @notice Output rewards agent info (shared across all deployment types)
*/
function _outputRewardsAgentInfo(
address rewardsAgent,
bytes32 rewardsAgentOrigin
) internal {
Logging.logHeader("REWARDS AGENT INFO");
Logging.logContractDeployed("RewardsAgent", rewardsAgent);
Logging.logAgentOrigin("RewardsAgentOrigin", vm.toString(rewardsAgentOrigin));
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, "}");
// Write to file
vm.writeFile(rewardsInfoPath, json);
Logging.logInfo(string.concat("Rewards info saved to: ", rewardsInfoPath));
}
}