datahaven/contracts/script/deploy/DeployLive.s.sol
Ahmad Kaouk eaf55fb414
feat: implement weighted top-32 validator selection (#443)
## Overview

Implements deterministic weighted-stake-based validator selection in
`DataHavenServiceManager`, building on the era-targeting submitter model
from PR #433. Previously, `buildNewValidatorSetMessage()` forwarded all
registered operators in arbitrary membership order with no stake-based
ranking, meaning high-stake operators could be displaced by lower-stake
ones when downstream caps applied. This PR fixes that by computing a
weighted stake score per operator and selecting the top-32 candidates
before bridging the set to DataHaven.

Spec: `specs/validator-set-selection/validator-set-selection.md`

## Contract Changes (`DataHavenServiceManager.sol`)

**New state:**
- `MAX_ACTIVE_VALIDATORS = 32` — cap on the outbound validator set
- `mapping(IStrategy => uint96) public strategiesAndMultipliers` —
per-strategy weight used in the selection formula

**Updated `buildNewValidatorSetMessage()`:**
1. Fetches allocated stake for all operators × strategies from
`AllocationManager`
2. Computes `weightedStake(op) = Σ(allocatedStake[op][j] ×
multiplier[j])` across all strategies
3. Filters operators with no solochain address mapping or zero weighted
stake
4. Runs a partial selection sort to pick the top `min(candidateCount,
32)` by descending weighted stake; ties broken by lower operator address
(deterministic)
5. Reverts with `EmptyValidatorSet()` if no eligible candidates remain

**Admin API changes:**
- `addStrategiesToValidatorsSupportedStrategies()` signature changed
from `IStrategy[]` to `IRewardsCoordinatorTypes.StrategyAndMultiplier[]`
— strategy and multiplier are stored atomically in one call, eliminating
the risk of a strategy being registered without a multiplier
- New `setStrategiesAndMultipliers(StrategyAndMultiplier[])` — updates
multiplier weights for existing strategies without touching the
EigenLayer strategy set
- New `getStrategiesAndMultipliers()` — returns all strategies with
their current multipliers
- `removeStrategiesFromValidatorsSupportedStrategies()` now cleans up
multiplier entries on removal

**New error / event:**
- `EmptyValidatorSet()` — reverts when no eligible candidates exist
- `StrategiesAndMultipliersSet(StrategyAndMultiplier[])` — emitted on
add or update of multipliers

## Tests (`ValidatorSetSelection.t.sol`)

New 552-line Foundry test suite covering all cases from the spec:

| Case |
|------|
| `addStrategies` stores multiplier atomically |
| `removeStrategies` deletes multiplier |
| `setStrategiesAndMultipliers` updates without touching the strategy
set |
| `getStrategiesAndMultipliers` returns correct pairs |
| Weighted stake computed correctly across multiple strategies |
| Operators with zero weighted stake are excluded |
| Unset multiplier treated as 0 |
| Top-32 selection when candidate count > 32 |
| All candidates included when count < 32 |
| Tie-breaking by lower operator address |
| `EmptyValidatorSet` revert when no eligible operators |

## Deploy Scripts

- **`DeployBase.s.sol`**: Sets a default multiplier of `1` for all
configured validator strategies after AVS registration via
`setStrategiesAndMultipliers`
- **New `AllocateOperatorStake.s.sol`**: Forge script that allocates
full magnitude (`1e18`) to the validator operator set for a given
operator. Must be run at least one block after `SignUpValidator` to
respect EigenLayer's allocation configuration delay.

## E2E Framework

- **`validators.ts` — `registerOperator()`**: Extended to deposit tokens
into each deployed strategy and allocate full magnitude to the DataHaven
operator set after registration. Previously operators registered without
staking, producing zero weighted stake and getting filtered out by the
new selection logic.
- **`setup-validators.ts`**: Added a stake allocation pass after the
registration loop, invoking `AllocateOperatorStake.s.sol` per validator.
- **`validator-set-update.test.ts`**: Added debug logging for
transaction receipts and the `OutboundMessageAccepted` /
`ExternalValidatorsSet` events.
- **`generated.ts`**: Regenerated contract bindings to include new
functions, events, and the `EmptyValidatorSet` error.

## ⚠️ Breaking Changes ⚠️

- `addStrategiesToValidatorsSupportedStrategies(IStrategy[])` →
`addStrategiesToValidatorsSupportedStrategies(StrategyAndMultiplier[])`:
callers must supply multipliers alongside strategies.
- Operators with zero weighted stake are no longer included in the
bridged validator set.

## Rollout Notes

1. PR #433 (era-targeting + submitter role) must be deployed first
2. Deploy this `ServiceManager` upgrade
3. Confirm `strategiesAndMultipliers` is set for all active strategies
(default multiplier `1` applied automatically by `DeployBase`)
4. Deploy the runtime cap-enforcement changes (spec section 10.2)
5. Submitter daemon requires no changes — continues submitting
`targetEra = ActiveEra + 1`
2026-02-24 09:23:57 +01:00

266 lines
11 KiB
Solidity

// 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";
// 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 {
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 DeployLive
* @notice Deployment script for live networks (hoodi testnet, ethereum mainnet)
* @dev References existing EigenLayer contracts on the target chain
*/
contract DeployLive is DeployBase {
string public networkName;
function run() public {
// Network detection and validation
networkName = vm.envString("NETWORK");
require(
bytes(networkName).length > 0,
"NETWORK environment variable required for live deployment"
);
_validateNetwork(networkName);
totalSteps = 4;
address avsOwnerEnv = vm.envOr("AVS_OWNER_ADDRESS", address(0));
require(
avsOwnerEnv != address(0),
"AVS_OWNER_ADDRESS env variable required for live deployments"
);
_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) {
return string.concat("LIVE_", networkName);
}
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();
// Live 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 live deployment
ServiceManagerInitParams memory params
) internal override returns (DataHavenServiceManager) {
// Live 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.initialize.selector,
params.avsOwner,
params.rewardsInitiator,
params.validatorsStrategiesAndMultipliers,
params.gateway,
params.validatorSetSubmitter
);
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,
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.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, '"RewardsAgent": "', vm.toString(rewardsAgent), '",');
// EigenLayer contracts (existing on live network)
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));
}
// LIVE DEPLOYMENT FUNCTIONS
/**
* @notice Validate that the network is in the supported allowlist
* @dev Supported networks:
* - "hoodi", "stagenet-hoodi", "testnet-hoodi" (Hoodi testnet)
* - "ethereum", "mainnet-ethereum" (Ethereum mainnet)
*/
function _validateNetwork(
string memory network
) internal pure {
bytes32 h = keccak256(bytes(network));
if (
h == keccak256("hoodi") || h == keccak256("stagenet-hoodi")
|| h == keccak256("testnet-hoodi") || h == keccak256("mainnet-ethereum")
|| h == keccak256("ethereum")
) {
return;
}
revert(
string.concat(
"Unsupported network: ",
network,
". Supported: hoodi, stagenet-hoodi, testnet-hoodi, ethereum, mainnet-ethereum"
)
);
}
/**
* @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)
)
);
}
}