mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-24 01:38:32 +00:00
## 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`
258 lines
12 KiB
Solidity
258 lines
12 KiB
Solidity
// SPDX-License-Identifier: UNLICENSED
|
|
pragma solidity ^0.8.27;
|
|
|
|
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
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";
|
|
import {AVSDeployer} from "./AVSDeployer.sol";
|
|
import {TestUtils} from "./TestUtils.sol";
|
|
import {
|
|
IAllocationManagerTypes
|
|
} from "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol";
|
|
import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol";
|
|
import {
|
|
IRewardsCoordinatorTypes
|
|
} from "eigenlayer-contracts/src/contracts/interfaces/IRewardsCoordinator.sol";
|
|
import {OperatorSet} from "eigenlayer-contracts/src/contracts/libraries/OperatorSetLib.sol";
|
|
import {ValidatorsUtils} from "../../script/utils/ValidatorsUtils.sol";
|
|
|
|
import {console} from "forge-std/Test.sol";
|
|
|
|
contract SnowbridgeAndAVSDeployer is AVSDeployer {
|
|
// Snowbridge contracts
|
|
BeefyClient public beefyClient;
|
|
IGatewayV2 public gateway;
|
|
Gateway public gatewayImplementation;
|
|
AgentExecutor public agentExecutor;
|
|
Agent public rewardsAgent;
|
|
Agent public wrongAgent;
|
|
|
|
// The addresses of the validators that are allowed to register to the DataHaven service.
|
|
address[] public validatorsAllowlist = [
|
|
0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266, // First pre-funded address in anvil
|
|
0x70997970C51812dc3A010C7d01b50e0d17dc79C8, // Second pre-funded address in anvil
|
|
0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC, // Third pre-funded address in anvil
|
|
0x90F79bf6EB2c4f870365E785982E1f101E93b906, // Fourth pre-funded address in anvil
|
|
0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65, // Fifth pre-funded address in anvil
|
|
0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc, // Sixth pre-funded address in anvil
|
|
0x976EA74026E726554dB657fA54763abd0C3a0aa9, // Seventh pre-funded address in anvil
|
|
0x14dC79964da2C08b23698B3D3cc7Ca32193d9955, // Eighth pre-funded address in anvil
|
|
0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f, // Ninth pre-funded address in anvil
|
|
0xa0Ee7A142d267C1f36714E4a8F75612F20a79720 // Tenth pre-funded address in anvil
|
|
];
|
|
|
|
// Snowbridge contracts params
|
|
// The hashes of the initial (current) Validators in the DataHaven solochain.
|
|
bytes32[] public initialValidatorHashes;
|
|
// The hashes of the next Validators in the DataHaven solochain.
|
|
bytes32[] public nextValidatorHashes;
|
|
// In reality this should be set to MAX_SEED_LOOKAHEAD (4 epochs = 128 blocks/slots)
|
|
// https://eth2book.info/capella/part3/config/preset/#time-parameters
|
|
uint256 public constant RANDAO_COMMIT_DELAY = 4;
|
|
// In reality this is set to 24 blocks https://etherscan.io/address/0x6eD05bAa904df3DE117EcFa638d4CB84e1B8A00C#readContract#F10
|
|
uint256 public constant RANDAO_COMMIT_EXPIRATION = 24;
|
|
// In reality this is set to 17 https://etherscan.io/address/0x6eD05bAa904df3DE117EcFa638d4CB84e1B8A00C#readContract#F7
|
|
uint256 public constant MIN_NUM_REQUIRED_SIGNATURES = 2;
|
|
uint64 public constant START_BLOCK = 1;
|
|
bytes32 public constant REWARDS_MESSAGE_ORIGIN = bytes32(0);
|
|
// "wrong origin" as bytes32 (hex-encoded, right-padded with zeros)
|
|
bytes32 public constant WRONG_MESSAGE_ORIGIN =
|
|
0x77726f6e67206f726967696e0000000000000000000000000000000000000000;
|
|
|
|
function _deployMockAllContracts() internal {
|
|
_deployMockSnowbridge();
|
|
_deployMockEigenLayerAndAVS();
|
|
_connectSnowbridgeToAVS();
|
|
}
|
|
|
|
function _deployMockSnowbridge() internal {
|
|
// Generate validator arrays using the generator functions
|
|
initialValidatorHashes = TestUtils.generateMockValidators(10);
|
|
nextValidatorHashes = TestUtils.generateMockValidators(10, 10);
|
|
|
|
BeefyClient.ValidatorSet memory validatorSet =
|
|
ValidatorsUtils._buildValidatorSet(0, initialValidatorHashes);
|
|
BeefyClient.ValidatorSet memory nextValidatorSet =
|
|
ValidatorsUtils._buildValidatorSet(1, nextValidatorHashes);
|
|
|
|
cheats.prank(regularDeployer);
|
|
beefyClient = new BeefyClient(
|
|
RANDAO_COMMIT_DELAY,
|
|
RANDAO_COMMIT_EXPIRATION,
|
|
MIN_NUM_REQUIRED_SIGNATURES,
|
|
START_BLOCK,
|
|
validatorSet,
|
|
nextValidatorSet
|
|
);
|
|
|
|
console.log("BeefyClient deployed at", address(beefyClient));
|
|
|
|
cheats.prank(regularDeployer);
|
|
agentExecutor = new AgentExecutor();
|
|
|
|
console.log("AgentExecutor deployed at", address(agentExecutor));
|
|
|
|
cheats.prank(regularDeployer);
|
|
gatewayImplementation = new Gateway(address(beefyClient), address(agentExecutor));
|
|
|
|
console.log("GatewayImplementation deployed at", address(gatewayImplementation));
|
|
|
|
OperatingMode defaultOperatingMode = OperatingMode.Normal;
|
|
|
|
Initializer.Config memory config = Initializer.Config({
|
|
mode: defaultOperatingMode,
|
|
deliveryCost: 1, // This is for v1, we don't really care about this
|
|
registerTokenFee: 1, // This is for v1, we don't really care about this
|
|
assetHubCreateAssetFee: 1, // This is for v1, we don't really care about this
|
|
assetHubReserveTransferFee: 1, // This is for v1, we don't really care about this
|
|
exchangeRate: ud60x18(1), // This is for v1, we don't really care about this
|
|
multiplier: ud60x18(1), // This is for v1, we don't really care about this
|
|
foreignTokenDecimals: 18, // This is for v1, we don't really care about this
|
|
maxDestinationFee: 1 // This is for v1, we don't really care about this
|
|
});
|
|
|
|
cheats.prank(regularDeployer);
|
|
gateway = IGatewayV2(
|
|
address(new GatewayProxy(address(gatewayImplementation), abi.encode(config)))
|
|
);
|
|
|
|
console.log("Gateway deployed at", address(gateway));
|
|
}
|
|
|
|
function _connectSnowbridgeToAVS() internal {
|
|
cheats.prank(regularDeployer);
|
|
gateway.v2_createAgent(REWARDS_MESSAGE_ORIGIN);
|
|
|
|
// Get the agent address after creation.
|
|
address payable agentAddress = payable(gateway.agentOf(REWARDS_MESSAGE_ORIGIN));
|
|
rewardsAgent = Agent(agentAddress);
|
|
|
|
console.log("Rewards agent deployed at", address(rewardsAgent));
|
|
|
|
cheats.prank(regularDeployer);
|
|
gateway.v2_createAgent(WRONG_MESSAGE_ORIGIN);
|
|
|
|
// Get the agent address after creation.
|
|
address payable wrongAgentAddress = payable(gateway.agentOf(WRONG_MESSAGE_ORIGIN));
|
|
wrongAgent = Agent(wrongAgentAddress);
|
|
|
|
console.log("Wrong agent deployed at", address(wrongAgent));
|
|
|
|
// Set the Snowbridge Gateway address in the DataHaven service.
|
|
cheats.prank(avsOwner);
|
|
serviceManager.setSnowbridgeGateway(address(gateway));
|
|
}
|
|
|
|
function setupValidatorsAsOperatorsWithAllocations() public {
|
|
setupValidatorsAsOperators();
|
|
|
|
// Remove strategies added during initialize (without multipliers)
|
|
// and re-add them with explicit multipliers
|
|
IStrategy[] memory strategies = new IStrategy[](deployedStrategies.length);
|
|
for (uint256 i = 0; i < deployedStrategies.length; i++) {
|
|
strategies[i] = deployedStrategies[i];
|
|
}
|
|
|
|
IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory sm =
|
|
new IRewardsCoordinatorTypes.StrategyAndMultiplier[](deployedStrategies.length);
|
|
for (uint256 i = 0; i < deployedStrategies.length; i++) {
|
|
sm[i] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
|
|
strategy: strategies[i],
|
|
multiplier: 1 // 1x multiplier for all strategies
|
|
});
|
|
}
|
|
|
|
cheats.startPrank(avsOwner);
|
|
serviceManager.removeStrategiesFromValidatorsSupportedStrategies(strategies);
|
|
serviceManager.addStrategiesToValidatorsSupportedStrategies(sm);
|
|
cheats.stopPrank();
|
|
|
|
// Advance past ALLOCATION_CONFIGURATION_DELAY (1 day = 86400 blocks in test setup)
|
|
// so operator allocation delays take effect
|
|
uint32 allocationConfigDelay = allocationManager.ALLOCATION_CONFIGURATION_DELAY();
|
|
cheats.roll(block.number + allocationConfigDelay + 1);
|
|
|
|
// For each operator, allocate full magnitude to the DataHaven operator set
|
|
for (uint256 i = 0; i < validatorsAllowlist.length; i++) {
|
|
IAllocationManagerTypes.AllocateParams[] memory allocParams =
|
|
new IAllocationManagerTypes.AllocateParams[](1);
|
|
|
|
uint64[] memory newMagnitudes = new uint64[](deployedStrategies.length);
|
|
for (uint256 j = 0; j < deployedStrategies.length; j++) {
|
|
newMagnitudes[j] = 1e18; // 100% magnitude
|
|
}
|
|
|
|
allocParams[0] = IAllocationManagerTypes.AllocateParams({
|
|
operatorSet: OperatorSet({
|
|
avs: address(serviceManager), id: serviceManager.VALIDATORS_SET_ID()
|
|
}),
|
|
strategies: strategies,
|
|
newMagnitudes: newMagnitudes
|
|
});
|
|
|
|
cheats.prank(validatorsAllowlist[i]);
|
|
allocationManager.modifyAllocations(validatorsAllowlist[i], allocParams);
|
|
}
|
|
|
|
// Advance past allocation effect delay (operator delay is 0, so just +1 block)
|
|
cheats.roll(block.number + 1);
|
|
}
|
|
|
|
function setupValidatorsAsOperators() public {
|
|
for (uint256 i = 0; i < validatorsAllowlist.length; i++) {
|
|
console.log("Setting up validator %s as operator", validatorsAllowlist[i]);
|
|
|
|
// Whitelist the validator in the DataHaven service.
|
|
cheats.prank(avsOwner);
|
|
serviceManager.addValidatorToAllowlist(validatorsAllowlist[i]);
|
|
|
|
cheats.startPrank(validatorsAllowlist[i]);
|
|
for (uint256 j = 0; j < deployedStrategies.length; j++) {
|
|
console.log(
|
|
"Depositing tokens from validator %s into strategy %s",
|
|
validatorsAllowlist[i],
|
|
address(deployedStrategies[j])
|
|
);
|
|
|
|
// Give the validator some balance in the strategy's linked token.
|
|
IERC20 linkedToken = deployedStrategies[j].underlyingToken();
|
|
_setERC20Balance(address(linkedToken), validatorsAllowlist[i], 1000 ether);
|
|
|
|
// Stake some of the validator's balance as stake for the strategy.
|
|
linkedToken.approve(address(strategyManager), 1000 ether);
|
|
strategyManager.depositIntoStrategy(deployedStrategies[j], linkedToken, 1000 ether);
|
|
|
|
console.log(
|
|
"Staked %s tokens from validator %s into strategy %s",
|
|
1000 ether,
|
|
validatorsAllowlist[i],
|
|
address(deployedStrategies[j])
|
|
);
|
|
}
|
|
|
|
// Register the validator as an operator in EigenLayer.
|
|
delegationManager.registerAsOperator(address(0), 0, "");
|
|
|
|
// Register the validator as an operator for the DataHaven service.
|
|
uint32[] memory operatorSetIds = new uint32[](1);
|
|
operatorSetIds[0] = serviceManager.VALIDATORS_SET_ID();
|
|
IAllocationManagerTypes.RegisterParams memory registerParams =
|
|
IAllocationManagerTypes.RegisterParams({
|
|
avs: address(serviceManager),
|
|
operatorSetIds: operatorSetIds,
|
|
data: abi.encodePacked(address(uint160(uint256(initialValidatorHashes[i]))))
|
|
});
|
|
allocationManager.registerForOperatorSets(validatorsAllowlist[i], registerParams);
|
|
cheats.stopPrank();
|
|
|
|
console.log("Validator %s setup as operator", validatorsAllowlist[i]);
|
|
}
|
|
}
|
|
}
|