mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-23 17:28:23 +00:00
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`
This commit is contained in:
parent
401f646286
commit
eaf55fb414
21 changed files with 1575 additions and 659 deletions
|
|
@ -21,16 +21,14 @@
|
|||
"rewardsCoordinatorInitPausedStatus": 0,
|
||||
"allocationManagerInitPausedStatus": 0,
|
||||
"deallocationDelay": 50,
|
||||
"allocationConfigurationDelay": 75,
|
||||
"allocationConfigurationDelay": 0,
|
||||
"beaconChainGenesisTimestamp": 1695902400
|
||||
},
|
||||
"avs": {
|
||||
"avsOwner": "0x976EA74026E726554dB657fA54763abd0C3a0aa9",
|
||||
"rewardsInitiator": "0x14dC79964da2C08b23698B3D3cc7Ca32193d9955",
|
||||
"validatorSetSubmitter": "0x976EA74026E726554dB657fA54763abd0C3a0aa9",
|
||||
"validatorsStrategies": [
|
||||
"0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0"
|
||||
]
|
||||
"validatorsStrategies": []
|
||||
},
|
||||
"snowbridge": {
|
||||
"randaoCommitDelay": 4,
|
||||
|
|
@ -49,4 +47,4 @@
|
|||
"0xf68aec7304bf37f340dae2ea20fb5271ee28a3128812b84a615da4789e458bde"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +1 @@
|
|||
aa4e5f7e459c7a4f337016e845cd05aa56cccb41
|
||||
9c861e3e1d290888127bc6d772fb1a3422bdf8b3
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -32,6 +32,10 @@ import {
|
|||
} 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";
|
||||
import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol";
|
||||
import {
|
||||
IRewardsCoordinatorTypes
|
||||
} from "eigenlayer-contracts/src/contracts/interfaces/IRewardsCoordinator.sol";
|
||||
|
||||
// DataHaven imports
|
||||
import {DataHavenServiceManager} from "../../src/DataHavenServiceManager.sol";
|
||||
|
|
@ -41,7 +45,7 @@ import {ValidatorsUtils} from "../../script/utils/ValidatorsUtils.sol";
|
|||
struct ServiceManagerInitParams {
|
||||
address avsOwner;
|
||||
address rewardsInitiator;
|
||||
address[] validatorsStrategies;
|
||||
IRewardsCoordinatorTypes.StrategyAndMultiplier[] validatorsStrategiesAndMultipliers;
|
||||
address gateway;
|
||||
address validatorSetSubmitter;
|
||||
}
|
||||
|
|
@ -246,11 +250,21 @@ abstract contract DeployBase is Script, DeployParams, Accounts {
|
|||
"ServiceManager Implementation", address(serviceManagerImplementation)
|
||||
);
|
||||
|
||||
// Build StrategyAndMultiplier[] from config addresses with default multiplier of 1.
|
||||
// Multipliers can be updated post-deployment via setStrategiesAndMultipliers if needed.
|
||||
IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory strategiesAndMultipliers = new IRewardsCoordinatorTypes
|
||||
.StrategyAndMultiplier[](avsConfig.validatorsStrategies.length);
|
||||
for (uint256 i = 0; i < avsConfig.validatorsStrategies.length; i++) {
|
||||
strategiesAndMultipliers[i] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
|
||||
strategy: IStrategy(avsConfig.validatorsStrategies[i]), multiplier: 1
|
||||
});
|
||||
}
|
||||
|
||||
// Create service manager initialisation parameters struct
|
||||
ServiceManagerInitParams memory initParams = ServiceManagerInitParams({
|
||||
avsOwner: avsConfig.avsOwner,
|
||||
rewardsInitiator: avsConfig.rewardsInitiator,
|
||||
validatorsStrategies: avsConfig.validatorsStrategies,
|
||||
validatorsStrategiesAndMultipliers: strategiesAndMultipliers,
|
||||
gateway: address(gateway),
|
||||
validatorSetSubmitter: avsConfig.validatorSetSubmitter
|
||||
});
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ contract DeployLive is DeployBase {
|
|||
DataHavenServiceManager.initialize.selector,
|
||||
params.avsOwner,
|
||||
params.rewardsInitiator,
|
||||
params.validatorsStrategies,
|
||||
params.validatorsStrategiesAndMultipliers,
|
||||
params.gateway,
|
||||
params.validatorSetSubmitter
|
||||
);
|
||||
|
|
|
|||
|
|
@ -206,7 +206,7 @@ contract DeployLocal is DeployBase {
|
|||
DataHavenServiceManager.initialize.selector,
|
||||
params.avsOwner,
|
||||
params.rewardsInitiator,
|
||||
params.validatorsStrategies,
|
||||
params.validatorsStrategiesAndMultipliers,
|
||||
params.gateway,
|
||||
params.validatorSetSubmitter
|
||||
);
|
||||
|
|
@ -356,11 +356,15 @@ contract DeployLocal is DeployBase {
|
|||
function _prepareStrategiesForServiceManager(
|
||||
ServiceManagerInitParams memory params
|
||||
) internal view {
|
||||
if (params.validatorsStrategies.length == 0) {
|
||||
params.validatorsStrategies = new address[](deployedStrategies.length);
|
||||
if (params.validatorsStrategiesAndMultipliers.length == 0) {
|
||||
IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory sm =
|
||||
new IRewardsCoordinatorTypes.StrategyAndMultiplier[](deployedStrategies.length);
|
||||
for (uint256 i = 0; i < deployedStrategies.length; i++) {
|
||||
params.validatorsStrategies[i] = deployedStrategies[i].address_;
|
||||
sm[i] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
|
||||
strategy: IStrategy(deployedStrategies[i].address_), multiplier: 1
|
||||
});
|
||||
}
|
||||
params.validatorsStrategiesAndMultipliers = sm;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
61
contracts/script/transact/AllocateOperatorStake.s.sol
Normal file
61
contracts/script/transact/AllocateOperatorStake.s.sol
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.27;
|
||||
|
||||
// EigenLayer imports
|
||||
import {
|
||||
IAllocationManagerTypes
|
||||
} from "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol";
|
||||
import {OperatorSet} from "eigenlayer-contracts/src/contracts/libraries/OperatorSetLib.sol";
|
||||
import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol";
|
||||
|
||||
// Testing imports
|
||||
import {Script} from "forge-std/Script.sol";
|
||||
import {console} from "forge-std/console.sol";
|
||||
import {Logging} from "../utils/Logging.sol";
|
||||
import {ELScriptStorage} from "../utils/ELScriptStorage.s.sol";
|
||||
import {DHScriptStorage} from "../utils/DHScriptStorage.s.sol";
|
||||
import {Accounts} from "../utils/Accounts.sol";
|
||||
|
||||
/**
|
||||
* @title AllocateOperatorStake
|
||||
* @notice Allocates full magnitude to the validator operator set.
|
||||
* Must be run AFTER SignUpValidator (needs at least 1 block gap
|
||||
* for the allocation delay to initialize).
|
||||
*/
|
||||
contract AllocateOperatorStake is Script, ELScriptStorage, DHScriptStorage, Accounts {
|
||||
function run() public {
|
||||
string memory network = vm.envOr("NETWORK", string("anvil"));
|
||||
Logging.logHeader("ALLOCATE OPERATOR STAKE");
|
||||
console.log("| Network: %s", network);
|
||||
Logging.logFooter();
|
||||
|
||||
_loadELContracts(network);
|
||||
_loadDHContracts(network);
|
||||
|
||||
IStrategy[] memory strategies = new IStrategy[](deployedStrategies.length);
|
||||
for (uint256 i = 0; i < deployedStrategies.length; i++) {
|
||||
strategies[i] = IStrategy(address(deployedStrategies[i].strategy));
|
||||
}
|
||||
|
||||
uint64[] memory newMagnitudes = new uint64[](strategies.length);
|
||||
for (uint256 i = 0; i < strategies.length; i++) {
|
||||
newMagnitudes[i] = 1e18;
|
||||
}
|
||||
|
||||
IAllocationManagerTypes.AllocateParams[] memory allocParams =
|
||||
new IAllocationManagerTypes.AllocateParams[](1);
|
||||
allocParams[0] = IAllocationManagerTypes.AllocateParams({
|
||||
operatorSet: OperatorSet({
|
||||
avs: address(serviceManager), id: serviceManager.VALIDATORS_SET_ID()
|
||||
}),
|
||||
strategies: strategies,
|
||||
newMagnitudes: newMagnitudes
|
||||
});
|
||||
|
||||
vm.broadcast(_operatorPrivateKey);
|
||||
allocationManager.modifyAllocations(_operator, allocParams);
|
||||
Logging.logStep(
|
||||
string.concat("Allocated full magnitude for operator: ", vm.toString(_operator))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -33,8 +33,8 @@ normalize_json() {
|
|||
| .storage
|
||||
| map(
|
||||
del(.astId, .contract)
|
||||
# Remove unstable AST ID suffixes from type strings (e.g., t_contract(IGatewayV2)12345)
|
||||
| .type |= sub("\\)[0-9]+$"; ")")
|
||||
# Remove unstable AST IDs from type strings (e.g., t_contract(IGatewayV2)12345, nested mappings)
|
||||
| .type |= gsub("\\)[0-9]+"; ")")
|
||||
)
|
||||
| sort_by(.slot | tonumber)' "$1"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,9 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
|
|||
/// @notice The EigenLayer operator set ID for the Validators securing the DataHaven network.
|
||||
uint32 public constant VALIDATORS_SET_ID = 0;
|
||||
|
||||
/// @notice Maximum number of active validators in the set
|
||||
uint32 public constant MAX_ACTIVE_VALIDATORS = 32;
|
||||
|
||||
// ============ Immutables ============
|
||||
|
||||
/// @notice The EigenLayer AllocationManager contract
|
||||
|
|
@ -64,15 +67,17 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
|
|||
/// @inheritdoc IDataHavenServiceManager
|
||||
mapping(address => address) public validatorEthAddressToSolochainAddress;
|
||||
|
||||
/// @inheritdoc IDataHavenServiceManager
|
||||
mapping(address => address) public validatorSolochainAddressToEthAddress;
|
||||
|
||||
/// @inheritdoc IDataHavenServiceManager
|
||||
address public validatorSetSubmitter;
|
||||
|
||||
/// @inheritdoc IDataHavenServiceManager
|
||||
mapping(IStrategy => uint96) public strategiesAndMultipliers;
|
||||
|
||||
/// @notice Storage gap for upgradeability (must be at end of state variables)
|
||||
// solhint-disable-next-line var-name-mixedcase
|
||||
uint256[44] private __GAP;
|
||||
uint256[43] private __GAP;
|
||||
|
||||
// ============ Modifiers ============
|
||||
|
||||
|
|
@ -138,7 +143,7 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
|
|||
function initialize(
|
||||
address initialOwner,
|
||||
address _rewardsInitiator,
|
||||
IStrategy[] memory validatorsStrategies,
|
||||
IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory validatorsStrategiesAndMultipliers,
|
||||
address _snowbridgeGatewayAddress,
|
||||
address _validatorSetSubmitter
|
||||
) public virtual initializer {
|
||||
|
|
@ -154,11 +159,20 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
|
|||
// Register the DataHaven service in the AllocationManager.
|
||||
_ALLOCATION_MANAGER.updateAVSMetadataURI(address(this), DATAHAVEN_AVS_METADATA);
|
||||
|
||||
// Build the strategies array and populate multipliers atomically so that
|
||||
// getStrategiesInOperatorSet and strategiesAndMultipliers are always consistent.
|
||||
IStrategy[] memory strategies = new IStrategy[](validatorsStrategiesAndMultipliers.length);
|
||||
for (uint256 i = 0; i < validatorsStrategiesAndMultipliers.length; i++) {
|
||||
strategies[i] = validatorsStrategiesAndMultipliers[i].strategy;
|
||||
strategiesAndMultipliers[validatorsStrategiesAndMultipliers[i].strategy] =
|
||||
validatorsStrategiesAndMultipliers[i].multiplier;
|
||||
}
|
||||
|
||||
// Create the operator set for the DataHaven service.
|
||||
IAllocationManagerTypes.CreateSetParams[] memory operatorSets =
|
||||
new IAllocationManagerTypes.CreateSetParams[](1);
|
||||
operatorSets[0] = IAllocationManagerTypes.CreateSetParams({
|
||||
operatorSetId: VALIDATORS_SET_ID, strategies: validatorsStrategies
|
||||
operatorSetId: VALIDATORS_SET_ID, strategies: strategies
|
||||
});
|
||||
_ALLOCATION_MANAGER.createOperatorSets(address(this), operatorSets);
|
||||
|
||||
|
|
@ -200,22 +214,72 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
|
|||
uint64 targetEra
|
||||
) public view returns (bytes memory) {
|
||||
OperatorSet memory operatorSet = OperatorSet({avs: address(this), id: VALIDATORS_SET_ID});
|
||||
address[] memory currentValidatorSet = _ALLOCATION_MANAGER.getMembers(operatorSet);
|
||||
address[] memory operators = _ALLOCATION_MANAGER.getMembers(operatorSet);
|
||||
IStrategy[] memory strategies = _ALLOCATION_MANAGER.getStrategiesInOperatorSet(operatorSet);
|
||||
|
||||
// Allocate max size, then resize after filtering
|
||||
address[] memory newValidatorSet = new address[](currentValidatorSet.length);
|
||||
uint256 validCount = 0;
|
||||
for (uint256 i = 0; i < currentValidatorSet.length; i++) {
|
||||
address solochainAddr = validatorEthAddressToSolochainAddress[currentValidatorSet[i]];
|
||||
if (solochainAddr != address(0)) {
|
||||
newValidatorSet[validCount] = solochainAddr;
|
||||
++validCount;
|
||||
// Get allocated stake for all operators across all strategies
|
||||
uint256[][] memory allocatedStake =
|
||||
_ALLOCATION_MANAGER.getAllocatedStake(operatorSet, operators, strategies);
|
||||
|
||||
// Collect candidates: operators with solochain mapping and non-zero weighted stake
|
||||
address[] memory candidateSolochain = new address[](operators.length);
|
||||
uint256[] memory candidateStake = new uint256[](operators.length);
|
||||
address[] memory candidateOperator = new address[](operators.length);
|
||||
uint256 candidateCount = 0;
|
||||
|
||||
for (uint256 i = 0; i < operators.length; i++) {
|
||||
address solochainAddr = validatorEthAddressToSolochainAddress[operators[i]];
|
||||
if (solochainAddr == address(0)) continue;
|
||||
|
||||
// Compute weighted stake across all strategies:
|
||||
// weightedStake = sum(allocatedStake[i][j] * multiplier[j])
|
||||
uint256 weightedStake = 0;
|
||||
for (uint256 j = 0; j < strategies.length; j++) {
|
||||
weightedStake += allocatedStake[i][j]
|
||||
* uint256(strategiesAndMultipliers[strategies[j]]);
|
||||
}
|
||||
|
||||
if (weightedStake == 0) continue;
|
||||
|
||||
candidateSolochain[candidateCount] = solochainAddr;
|
||||
candidateStake[candidateCount] = weightedStake;
|
||||
candidateOperator[candidateCount] = operators[i];
|
||||
candidateCount++;
|
||||
}
|
||||
|
||||
require(candidateCount != 0, EmptyValidatorSet());
|
||||
|
||||
// Partial selection sort: pick top min(MAX_ACTIVE_VALIDATORS, candidateCount)
|
||||
uint256 selectCount =
|
||||
candidateCount < MAX_ACTIVE_VALIDATORS ? candidateCount : MAX_ACTIVE_VALIDATORS;
|
||||
|
||||
for (uint256 i = 0; i < selectCount; i++) {
|
||||
uint256 bestIdx = i;
|
||||
for (uint256 j = i + 1; j < candidateCount; j++) {
|
||||
if (_isBetterCandidate(
|
||||
candidateStake[j],
|
||||
candidateOperator[j],
|
||||
candidateStake[bestIdx],
|
||||
candidateOperator[bestIdx]
|
||||
)) {
|
||||
bestIdx = j;
|
||||
}
|
||||
}
|
||||
if (bestIdx != i) {
|
||||
// Swap all parallel arrays
|
||||
(candidateSolochain[i], candidateSolochain[bestIdx]) =
|
||||
(candidateSolochain[bestIdx], candidateSolochain[i]);
|
||||
(candidateStake[i], candidateStake[bestIdx]) =
|
||||
(candidateStake[bestIdx], candidateStake[i]);
|
||||
(candidateOperator[i], candidateOperator[bestIdx]) =
|
||||
(candidateOperator[bestIdx], candidateOperator[i]);
|
||||
}
|
||||
}
|
||||
require(validCount != 0, EmptyValidatorSet());
|
||||
// Resize array to actual count
|
||||
assembly {
|
||||
mstore(newValidatorSet, validCount)
|
||||
|
||||
// Build the final validator set from sorted solochain addresses
|
||||
address[] memory newValidatorSet = new address[](selectCount);
|
||||
for (uint256 i = 0; i < selectCount; i++) {
|
||||
newValidatorSet[i] = candidateSolochain[i];
|
||||
}
|
||||
|
||||
return DataHavenSnowbridgeMessages.scaleEncodeNewValidatorSetMessagePayload(
|
||||
|
|
@ -351,15 +415,71 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
|
|||
_ALLOCATION_MANAGER.removeStrategiesFromOperatorSet(
|
||||
address(this), VALIDATORS_SET_ID, _strategies
|
||||
);
|
||||
|
||||
for (uint256 i = 0; i < _strategies.length; i++) {
|
||||
delete strategiesAndMultipliers[_strategies[i]];
|
||||
}
|
||||
}
|
||||
|
||||
/// @inheritdoc IDataHavenServiceManager
|
||||
function addStrategiesToValidatorsSupportedStrategies(
|
||||
IStrategy[] calldata _strategies
|
||||
IRewardsCoordinatorTypes.StrategyAndMultiplier[] calldata _strategyMultipliers
|
||||
) external onlyOwner {
|
||||
_ALLOCATION_MANAGER.addStrategiesToOperatorSet(
|
||||
address(this), VALIDATORS_SET_ID, _strategies
|
||||
);
|
||||
IStrategy[] memory strategies = new IStrategy[](_strategyMultipliers.length);
|
||||
for (uint256 i = 0; i < _strategyMultipliers.length; i++) {
|
||||
strategies[i] = _strategyMultipliers[i].strategy;
|
||||
strategiesAndMultipliers[_strategyMultipliers[i].strategy] =
|
||||
_strategyMultipliers[i].multiplier;
|
||||
}
|
||||
|
||||
_ALLOCATION_MANAGER.addStrategiesToOperatorSet(address(this), VALIDATORS_SET_ID, strategies);
|
||||
|
||||
emit StrategiesAndMultipliersSet(_strategyMultipliers);
|
||||
}
|
||||
|
||||
/// @inheritdoc IDataHavenServiceManager
|
||||
function setStrategiesAndMultipliers(
|
||||
IRewardsCoordinatorTypes.StrategyAndMultiplier[] calldata _strategyMultipliers
|
||||
) external onlyOwner {
|
||||
OperatorSet memory operatorSet = OperatorSet({avs: address(this), id: VALIDATORS_SET_ID});
|
||||
IStrategy[] memory registered = _ALLOCATION_MANAGER.getStrategiesInOperatorSet(operatorSet);
|
||||
|
||||
for (uint256 i = 0; i < _strategyMultipliers.length; i++) {
|
||||
bool found = false;
|
||||
for (uint256 j = 0; j < registered.length; j++) {
|
||||
if (registered[j] == _strategyMultipliers[i].strategy) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
require(found, StrategyNotInOperatorSet());
|
||||
|
||||
strategiesAndMultipliers[_strategyMultipliers[i].strategy] =
|
||||
_strategyMultipliers[i].multiplier;
|
||||
}
|
||||
|
||||
emit StrategiesAndMultipliersSet(_strategyMultipliers);
|
||||
}
|
||||
|
||||
/// @inheritdoc IDataHavenServiceManager
|
||||
function getStrategiesAndMultipliers()
|
||||
external
|
||||
view
|
||||
returns (IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory)
|
||||
{
|
||||
OperatorSet memory operatorSet = OperatorSet({avs: address(this), id: VALIDATORS_SET_ID});
|
||||
IStrategy[] memory strategies = _ALLOCATION_MANAGER.getStrategiesInOperatorSet(operatorSet);
|
||||
|
||||
IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory result =
|
||||
new IRewardsCoordinatorTypes.StrategyAndMultiplier[](strategies.length);
|
||||
|
||||
for (uint256 i = 0; i < strategies.length; i++) {
|
||||
result[i] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
|
||||
strategy: strategies[i], multiplier: strategiesAndMultipliers[strategies[i]]
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ============ Rewards Functions ============
|
||||
|
|
@ -487,6 +607,27 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
|
|||
require(result != address(0), ZeroAddress());
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Determines if candidate A ranks higher than candidate B
|
||||
* @dev Higher stake wins; on tie, lower operator address wins
|
||||
* @param stakeA Weighted stake of candidate A
|
||||
* @param opA Operator address of candidate A
|
||||
* @param stakeB Weighted stake of candidate B
|
||||
* @param opB Operator address of candidate B
|
||||
* @return True if candidate A ranks higher than candidate B
|
||||
*/
|
||||
function _isBetterCandidate(
|
||||
uint256 stakeA,
|
||||
address opA,
|
||||
uint256 stakeB,
|
||||
address opB
|
||||
) private pure returns (bool) {
|
||||
if (stakeA != stakeB) {
|
||||
return stakeA > stakeB;
|
||||
}
|
||||
return opA < opB;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Returns the EigenLayer operator address for a Solochain validator address
|
||||
* @dev Reverts if the Solochain address has not been mapped to an operator
|
||||
|
|
|
|||
|
|
@ -42,6 +42,9 @@ interface IDataHavenServiceManagerErrors {
|
|||
|
||||
/// @notice Thrown when a Solochain address is already assigned to a different operator
|
||||
error SolochainAddressAlreadyAssigned();
|
||||
|
||||
/// @notice Thrown when a strategy is not registered in the operator set
|
||||
error StrategyNotInOperatorSet();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -89,6 +92,10 @@ interface IDataHavenServiceManagerEvents {
|
|||
/// @notice Emitted when a batch of slashing request is being successfully slashed
|
||||
event SlashingComplete();
|
||||
|
||||
/// @notice Emitted when strategy multipliers are set or updated
|
||||
/// @param strategyMultipliers Array of strategy-multiplier pairs that were set
|
||||
event StrategiesAndMultipliersSet(IRewardsCoordinatorTypes
|
||||
.StrategyAndMultiplier[] strategyMultipliers);
|
||||
/// @notice Emitted when the validator set submitter address is updated
|
||||
/// @param oldSubmitter The previous validator set submitter address
|
||||
/// @param newSubmitter The new validator set submitter address
|
||||
|
|
@ -166,12 +173,15 @@ interface IDataHavenServiceManager is
|
|||
* @notice Initializes the DataHaven Service Manager
|
||||
* @param initialOwner Address of the initial owner
|
||||
* @param rewardsInitiator Address authorized to initiate rewards
|
||||
* @param validatorsStrategies Array of strategies supported by validators
|
||||
* @param validatorsStrategiesAndMultipliers Array of strategy-multiplier pairs for the validators
|
||||
* operator set. Each multiplier must be non-zero.
|
||||
* @param _snowbridgeGatewayAddress Address of the Snowbridge Gateway
|
||||
* @param _validatorSetSubmitter Address authorized to submit validator set messages
|
||||
*/
|
||||
function initialize(
|
||||
address initialOwner,
|
||||
address rewardsInitiator,
|
||||
IStrategy[] memory validatorsStrategies,
|
||||
IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory validatorsStrategiesAndMultipliers,
|
||||
address _snowbridgeGatewayAddress,
|
||||
address _validatorSetSubmitter
|
||||
) external;
|
||||
|
|
@ -193,9 +203,13 @@ interface IDataHavenServiceManager is
|
|||
) external payable;
|
||||
|
||||
/**
|
||||
* @notice Builds a new validator set message for a target era
|
||||
* @notice Builds a SCALE-encoded message containing the top validators by weighted stake
|
||||
* @dev Selects up to MAX_ACTIVE_VALIDATORS from registered operators. Each operator's
|
||||
* weighted stake is computed as: sum(allocatedStake[j] * multiplier[j])
|
||||
* across all strategies. Operators without a solochain address mapping or with zero
|
||||
* weighted stake are excluded. Ties are broken by lower operator address.
|
||||
* @param targetEra The target era to encode in the message
|
||||
* @return The encoded message bytes to be sent to the Snowbridge Gateway
|
||||
* @return The SCALE-encoded message bytes to be sent to the Snowbridge Gateway
|
||||
*/
|
||||
function buildNewValidatorSetMessageForEra(
|
||||
uint64 targetEra
|
||||
|
|
@ -251,12 +265,50 @@ interface IDataHavenServiceManager is
|
|||
|
||||
/**
|
||||
* @notice Adds strategies to the list of supported strategies for DataHaven Validators
|
||||
* @param _strategies Array of strategy contracts to add to validators operator set
|
||||
* @dev Each strategy's multiplier determines its weight in the validator selection
|
||||
* formula: weightedStake = sum(allocatedStake[j] * multiplier[j])
|
||||
* @param _strategyMultipliers Array of strategy-multiplier pairs to add
|
||||
*/
|
||||
function addStrategiesToValidatorsSupportedStrategies(
|
||||
IStrategy[] calldata _strategies
|
||||
IRewardsCoordinatorTypes.StrategyAndMultiplier[] calldata _strategyMultipliers
|
||||
) external;
|
||||
|
||||
/**
|
||||
* @notice Returns the maximum number of active validators in the set
|
||||
* @return The maximum active validators constant
|
||||
*/
|
||||
function MAX_ACTIVE_VALIDATORS() external pure returns (uint32);
|
||||
|
||||
/**
|
||||
* @notice Returns the multiplier for a given strategy
|
||||
* @dev The multiplier determines how much an operator's allocated stake in this strategy
|
||||
* contributes to their weighted stake during validator set selection.
|
||||
* @param strategy The strategy to look up
|
||||
* @return The multiplier weight
|
||||
*/
|
||||
function strategiesAndMultipliers(
|
||||
IStrategy strategy
|
||||
) external view returns (uint96);
|
||||
|
||||
/**
|
||||
* @notice Updates multipliers for strategies already in the operator set
|
||||
* @dev Does not add or remove strategies from EigenLayer; only updates multiplier weights
|
||||
* used in the validator selection weighted stake formula
|
||||
* @param _strategyMultipliers Array of strategy-multiplier pairs to update
|
||||
*/
|
||||
function setStrategiesAndMultipliers(
|
||||
IRewardsCoordinatorTypes.StrategyAndMultiplier[] calldata _strategyMultipliers
|
||||
) external;
|
||||
|
||||
/**
|
||||
* @notice Returns all strategies with their multipliers
|
||||
* @return Array of StrategyAndMultiplier structs with strategy addresses and multiplier weights
|
||||
*/
|
||||
function getStrategiesAndMultipliers()
|
||||
external
|
||||
view
|
||||
returns (IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory);
|
||||
|
||||
// ============ Rewards Submitter Functions ============
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@
|
|||
"type": "t_array(t_uint256)49_storage"
|
||||
},
|
||||
{
|
||||
"astId": 23771,
|
||||
"astId": 23775,
|
||||
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
|
||||
"label": "rewardsInitiator",
|
||||
"offset": 0,
|
||||
|
|
@ -49,7 +49,7 @@
|
|||
"type": "t_address"
|
||||
},
|
||||
{
|
||||
"astId": 23776,
|
||||
"astId": 23780,
|
||||
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
|
||||
"label": "validatorsAllowlist",
|
||||
"offset": 0,
|
||||
|
|
@ -57,7 +57,7 @@
|
|||
"type": "t_mapping(t_address,t_bool)"
|
||||
},
|
||||
{
|
||||
"astId": 23780,
|
||||
"astId": 23784,
|
||||
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
|
||||
"label": "_snowbridgeGateway",
|
||||
"offset": 0,
|
||||
|
|
@ -65,7 +65,7 @@
|
|||
"type": "t_contract(IGatewayV2)23481"
|
||||
},
|
||||
{
|
||||
"astId": 23785,
|
||||
"astId": 23789,
|
||||
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
|
||||
"label": "validatorEthAddressToSolochainAddress",
|
||||
"offset": 0,
|
||||
|
|
@ -73,7 +73,7 @@
|
|||
"type": "t_mapping(t_address,t_address)"
|
||||
},
|
||||
{
|
||||
"astId": 23790,
|
||||
"astId": 23793,
|
||||
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
|
||||
"label": "validatorSolochainAddressToEthAddress",
|
||||
"offset": 0,
|
||||
|
|
@ -81,7 +81,7 @@
|
|||
"type": "t_mapping(t_address,t_address)"
|
||||
},
|
||||
{
|
||||
"astId": 23793,
|
||||
"astId": 23796,
|
||||
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
|
||||
"label": "validatorSetSubmitter",
|
||||
"offset": 0,
|
||||
|
|
@ -89,12 +89,20 @@
|
|||
"type": "t_address"
|
||||
},
|
||||
{
|
||||
"astId": 23798,
|
||||
"astId": 23802,
|
||||
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
|
||||
"label": "strategiesAndMultipliers",
|
||||
"offset": 0,
|
||||
"slot": "107",
|
||||
"type": "t_mapping(t_contract(IStrategy)7361,t_uint96)"
|
||||
},
|
||||
{
|
||||
"astId": 23807,
|
||||
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
|
||||
"label": "__GAP",
|
||||
"offset": 0,
|
||||
"slot": "107",
|
||||
"type": "t_array(t_uint256)44_storage"
|
||||
"slot": "108",
|
||||
"type": "t_array(t_uint256)43_storage"
|
||||
}
|
||||
],
|
||||
"types": {
|
||||
|
|
@ -103,10 +111,10 @@
|
|||
"label": "address",
|
||||
"numberOfBytes": "20"
|
||||
},
|
||||
"t_array(t_uint256)44_storage": {
|
||||
"t_array(t_uint256)43_storage": {
|
||||
"encoding": "inplace",
|
||||
"label": "uint256[44]",
|
||||
"numberOfBytes": "1408",
|
||||
"label": "uint256[43]",
|
||||
"numberOfBytes": "1376",
|
||||
"base": "t_uint256"
|
||||
},
|
||||
"t_array(t_uint256)49_storage": {
|
||||
|
|
@ -131,6 +139,11 @@
|
|||
"label": "contract IGatewayV2",
|
||||
"numberOfBytes": "20"
|
||||
},
|
||||
"t_contract(IStrategy)7361": {
|
||||
"encoding": "inplace",
|
||||
"label": "contract IStrategy",
|
||||
"numberOfBytes": "20"
|
||||
},
|
||||
"t_mapping(t_address,t_address)": {
|
||||
"encoding": "mapping",
|
||||
"key": "t_address",
|
||||
|
|
@ -145,6 +158,13 @@
|
|||
"numberOfBytes": "32",
|
||||
"value": "t_bool"
|
||||
},
|
||||
"t_mapping(t_contract(IStrategy)7361,t_uint96)": {
|
||||
"encoding": "mapping",
|
||||
"key": "t_contract(IStrategy)7361",
|
||||
"label": "mapping(contract IStrategy => uint96)",
|
||||
"numberOfBytes": "32",
|
||||
"value": "t_uint96"
|
||||
},
|
||||
"t_uint256": {
|
||||
"encoding": "inplace",
|
||||
"label": "uint256",
|
||||
|
|
@ -154,6 +174,11 @@
|
|||
"encoding": "inplace",
|
||||
"label": "uint8",
|
||||
"numberOfBytes": "1"
|
||||
},
|
||||
"t_uint96": {
|
||||
"encoding": "inplace",
|
||||
"label": "uint96",
|
||||
"numberOfBytes": "12"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@ contract SnowbridgeIntegrationTest is SnowbridgeAndAVSDeployer {
|
|||
) public pure returns (bytes[] memory beforeTestCalldata) {
|
||||
if (testSelector == this.test_sendNewValidatorsSetMessage.selector) {
|
||||
beforeTestCalldata = new bytes[](1);
|
||||
beforeTestCalldata[0] = abi.encodeWithSelector(this.setupValidatorsAsOperators.selector);
|
||||
beforeTestCalldata[0] =
|
||||
abi.encodeWithSelector(this.setupValidatorsAsOperatorsWithAllocations.selector);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
568
contracts/test/ValidatorSetSelection.t.sol
Normal file
568
contracts/test/ValidatorSetSelection.t.sol
Normal file
|
|
@ -0,0 +1,568 @@
|
|||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.27;
|
||||
|
||||
/* solhint-disable func-name-mixedcase */
|
||||
|
||||
import {SnowbridgeAndAVSDeployer} from "./utils/SnowbridgeAndAVSDeployer.sol";
|
||||
import {DataHavenSnowbridgeMessages} from "../src/libraries/DataHavenSnowbridgeMessages.sol";
|
||||
import {IDataHavenServiceManagerErrors} from "../src/interfaces/IDataHavenServiceManager.sol";
|
||||
import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol";
|
||||
import {
|
||||
IRewardsCoordinatorTypes
|
||||
} from "eigenlayer-contracts/src/contracts/interfaces/IRewardsCoordinator.sol";
|
||||
import {
|
||||
IAllocationManagerTypes
|
||||
} from "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol";
|
||||
import {OperatorSet} from "eigenlayer-contracts/src/contracts/libraries/OperatorSetLib.sol";
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
|
||||
contract ValidatorSetSelectionTest is SnowbridgeAndAVSDeployer {
|
||||
function setUp() public {
|
||||
_deployMockAllContracts();
|
||||
}
|
||||
|
||||
// ============ Helpers ============
|
||||
|
||||
function _getStrategies() internal view returns (IStrategy[] memory) {
|
||||
IStrategy[] memory strategies = new IStrategy[](deployedStrategies.length);
|
||||
for (uint256 i = 0; i < deployedStrategies.length; i++) {
|
||||
strategies[i] = deployedStrategies[i];
|
||||
}
|
||||
return strategies;
|
||||
}
|
||||
|
||||
function _setupMultipliers(
|
||||
uint96[] memory multipliers
|
||||
) internal {
|
||||
IStrategy[] memory strategies = _getStrategies();
|
||||
|
||||
IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory sm =
|
||||
new IRewardsCoordinatorTypes.StrategyAndMultiplier[](strategies.length);
|
||||
for (uint256 i = 0; i < strategies.length; i++) {
|
||||
sm[i] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
|
||||
strategy: strategies[i], multiplier: multipliers[i]
|
||||
});
|
||||
}
|
||||
|
||||
cheats.startPrank(avsOwner);
|
||||
serviceManager.removeStrategiesFromValidatorsSupportedStrategies(strategies);
|
||||
serviceManager.addStrategiesToValidatorsSupportedStrategies(sm);
|
||||
cheats.stopPrank();
|
||||
}
|
||||
|
||||
function _uniformMultipliers() internal pure returns (uint96[] memory) {
|
||||
uint96[] memory m = new uint96[](3);
|
||||
m[0] = 1;
|
||||
m[1] = 1;
|
||||
m[2] = 1;
|
||||
return m;
|
||||
}
|
||||
|
||||
function _registerOperator(
|
||||
address op,
|
||||
address solochainAddr,
|
||||
uint256[] memory stakeAmounts
|
||||
) internal {
|
||||
cheats.prank(avsOwner);
|
||||
serviceManager.addValidatorToAllowlist(op);
|
||||
|
||||
cheats.startPrank(op);
|
||||
for (uint256 j = 0; j < deployedStrategies.length; j++) {
|
||||
IERC20 linkedToken = deployedStrategies[j].underlyingToken();
|
||||
_setERC20Balance(address(linkedToken), op, stakeAmounts[j]);
|
||||
linkedToken.approve(address(strategyManager), stakeAmounts[j]);
|
||||
strategyManager.depositIntoStrategy(deployedStrategies[j], linkedToken, stakeAmounts[j]);
|
||||
}
|
||||
delegationManager.registerAsOperator(address(0), 0, "");
|
||||
|
||||
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(solochainAddr)
|
||||
});
|
||||
allocationManager.registerForOperatorSets(op, registerParams);
|
||||
cheats.stopPrank();
|
||||
}
|
||||
|
||||
function _uniformStakes(
|
||||
uint256 amount
|
||||
) internal view returns (uint256[] memory) {
|
||||
uint256[] memory stakes = new uint256[](deployedStrategies.length);
|
||||
for (uint256 j = 0; j < stakes.length; j++) {
|
||||
stakes[j] = amount;
|
||||
}
|
||||
return stakes;
|
||||
}
|
||||
|
||||
function _allocateForOperator(
|
||||
address op
|
||||
) internal {
|
||||
IStrategy[] memory strategies = _getStrategies();
|
||||
uint64[] memory newMagnitudes = new uint64[](strategies.length);
|
||||
for (uint256 j = 0; j < strategies.length; j++) {
|
||||
newMagnitudes[j] = 1e18;
|
||||
}
|
||||
|
||||
IAllocationManagerTypes.AllocateParams[] memory allocParams =
|
||||
new IAllocationManagerTypes.AllocateParams[](1);
|
||||
allocParams[0] = IAllocationManagerTypes.AllocateParams({
|
||||
operatorSet: OperatorSet({
|
||||
avs: address(serviceManager), id: serviceManager.VALIDATORS_SET_ID()
|
||||
}),
|
||||
strategies: strategies,
|
||||
newMagnitudes: newMagnitudes
|
||||
});
|
||||
|
||||
cheats.prank(op);
|
||||
allocationManager.modifyAllocations(op, allocParams);
|
||||
}
|
||||
|
||||
function _advancePastAllocationConfigDelay() internal {
|
||||
uint32 delay = allocationManager.ALLOCATION_CONFIGURATION_DELAY();
|
||||
cheats.roll(block.number + delay + 1);
|
||||
}
|
||||
|
||||
function _advancePastAllocationEffect() internal {
|
||||
cheats.roll(block.number + 1);
|
||||
}
|
||||
|
||||
function _buildExpectedMessage(
|
||||
address[] memory validators,
|
||||
uint64 externalIndex
|
||||
) internal pure returns (bytes memory) {
|
||||
return DataHavenSnowbridgeMessages.scaleEncodeNewValidatorSetMessagePayload(
|
||||
DataHavenSnowbridgeMessages.NewValidatorSetPayload({
|
||||
validators: validators, externalIndex: externalIndex
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// ============ Admin Function Tests ============
|
||||
|
||||
// Test #7: Add strategy + multiplier in one call; verify both stored
|
||||
function test_addStrategies_setsMultiplierAtomically() public {
|
||||
IStrategy[] memory strategies = _getStrategies();
|
||||
|
||||
IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory sm =
|
||||
new IRewardsCoordinatorTypes.StrategyAndMultiplier[](3);
|
||||
sm[0] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
|
||||
strategy: strategies[0], multiplier: 5000
|
||||
});
|
||||
sm[1] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
|
||||
strategy: strategies[1], multiplier: 10000
|
||||
});
|
||||
sm[2] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
|
||||
strategy: strategies[2], multiplier: 2000
|
||||
});
|
||||
|
||||
cheats.startPrank(avsOwner);
|
||||
serviceManager.removeStrategiesFromValidatorsSupportedStrategies(strategies);
|
||||
serviceManager.addStrategiesToValidatorsSupportedStrategies(sm);
|
||||
cheats.stopPrank();
|
||||
|
||||
assertEq(serviceManager.strategiesAndMultipliers(strategies[0]), 5000);
|
||||
assertEq(serviceManager.strategiesAndMultipliers(strategies[1]), 10000);
|
||||
assertEq(serviceManager.strategiesAndMultipliers(strategies[2]), 2000);
|
||||
}
|
||||
|
||||
// Test #9: Remove strategy → multiplier and tracking bool deleted
|
||||
function test_removeStrategies_cleansUpMultiplier() public {
|
||||
IStrategy[] memory strategies = _getStrategies();
|
||||
|
||||
IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory sm =
|
||||
new IRewardsCoordinatorTypes.StrategyAndMultiplier[](3);
|
||||
sm[0] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
|
||||
strategy: strategies[0], multiplier: 5000
|
||||
});
|
||||
sm[1] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
|
||||
strategy: strategies[1], multiplier: 10000
|
||||
});
|
||||
sm[2] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
|
||||
strategy: strategies[2], multiplier: 2000
|
||||
});
|
||||
|
||||
cheats.startPrank(avsOwner);
|
||||
serviceManager.removeStrategiesFromValidatorsSupportedStrategies(strategies);
|
||||
serviceManager.addStrategiesToValidatorsSupportedStrategies(sm);
|
||||
|
||||
assertEq(serviceManager.strategiesAndMultipliers(strategies[1]), 10000);
|
||||
|
||||
serviceManager.removeStrategiesFromValidatorsSupportedStrategies(strategies);
|
||||
cheats.stopPrank();
|
||||
|
||||
assertEq(serviceManager.strategiesAndMultipliers(strategies[0]), 0);
|
||||
assertEq(serviceManager.strategiesAndMultipliers(strategies[1]), 0);
|
||||
assertEq(serviceManager.strategiesAndMultipliers(strategies[2]), 0);
|
||||
}
|
||||
|
||||
// Test #11: Returns correct StrategyAndMultiplier structs
|
||||
function test_getStrategiesAndMultipliers_returnsCorrect() public {
|
||||
IStrategy[] memory strategies = _getStrategies();
|
||||
|
||||
IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory sm =
|
||||
new IRewardsCoordinatorTypes.StrategyAndMultiplier[](3);
|
||||
sm[0] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
|
||||
strategy: strategies[0], multiplier: 5000
|
||||
});
|
||||
sm[1] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
|
||||
strategy: strategies[1], multiplier: 10000
|
||||
});
|
||||
sm[2] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
|
||||
strategy: strategies[2], multiplier: 2000
|
||||
});
|
||||
|
||||
cheats.startPrank(avsOwner);
|
||||
serviceManager.removeStrategiesFromValidatorsSupportedStrategies(strategies);
|
||||
serviceManager.addStrategiesToValidatorsSupportedStrategies(sm);
|
||||
cheats.stopPrank();
|
||||
|
||||
IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory result =
|
||||
serviceManager.getStrategiesAndMultipliers();
|
||||
|
||||
assertEq(result.length, 3);
|
||||
for (uint256 i = 0; i < result.length; i++) {
|
||||
uint96 expectedMultiplier = serviceManager.strategiesAndMultipliers(result[i].strategy);
|
||||
assertEq(result[i].multiplier, expectedMultiplier);
|
||||
}
|
||||
}
|
||||
|
||||
// Test: setStrategiesAndMultipliers updates existing multipliers
|
||||
function test_setStrategiesAndMultipliers_updatesMultipliers() public {
|
||||
IStrategy[] memory strategies = _getStrategies();
|
||||
|
||||
// Set initial multipliers via _setupMultipliers
|
||||
uint96[] memory initial = new uint96[](3);
|
||||
initial[0] = 5000;
|
||||
initial[1] = 10000;
|
||||
initial[2] = 2000;
|
||||
_setupMultipliers(initial);
|
||||
|
||||
// Update multipliers
|
||||
IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory updated =
|
||||
new IRewardsCoordinatorTypes.StrategyAndMultiplier[](3);
|
||||
updated[0] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
|
||||
strategy: strategies[0], multiplier: 1
|
||||
});
|
||||
updated[1] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
|
||||
strategy: strategies[1], multiplier: 1
|
||||
});
|
||||
updated[2] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
|
||||
strategy: strategies[2], multiplier: 9999
|
||||
});
|
||||
|
||||
cheats.prank(avsOwner);
|
||||
serviceManager.setStrategiesAndMultipliers(updated);
|
||||
|
||||
assertEq(serviceManager.strategiesAndMultipliers(strategies[0]), 1);
|
||||
assertEq(serviceManager.strategiesAndMultipliers(strategies[1]), 1);
|
||||
assertEq(serviceManager.strategiesAndMultipliers(strategies[2]), 9999);
|
||||
}
|
||||
|
||||
// Test: setStrategiesAndMultipliers changes validator ranking
|
||||
function test_setStrategiesAndMultipliers_affectsRanking() public {
|
||||
uint96[] memory mults = new uint96[](3);
|
||||
mults[0] = 10000;
|
||||
mults[1] = 1;
|
||||
mults[2] = 1;
|
||||
_setupMultipliers(mults);
|
||||
|
||||
// Op A: heavy in strategy 0 (high multiplier) → initially ranked first
|
||||
address opA = vm.addr(801);
|
||||
address solochainA = address(uint160(0x6001));
|
||||
uint256[] memory stakesA = new uint256[](3);
|
||||
stakesA[0] = 1000 ether;
|
||||
stakesA[1] = 10 ether;
|
||||
stakesA[2] = 10 ether;
|
||||
_registerOperator(opA, solochainA, stakesA);
|
||||
|
||||
// Op B: heavy in strategy 1 (low multiplier) → initially ranked second
|
||||
address opB = vm.addr(802);
|
||||
address solochainB = address(uint160(0x6002));
|
||||
uint256[] memory stakesB = new uint256[](3);
|
||||
stakesB[0] = 10 ether;
|
||||
stakesB[1] = 1000 ether;
|
||||
stakesB[2] = 10 ether;
|
||||
_registerOperator(opB, solochainB, stakesB);
|
||||
|
||||
_advancePastAllocationConfigDelay();
|
||||
_allocateForOperator(opA);
|
||||
_allocateForOperator(opB);
|
||||
_advancePastAllocationEffect();
|
||||
|
||||
// Before update: A ranks first (strategy 0 has multiplier 10_000)
|
||||
address[] memory expectedBefore = new address[](2);
|
||||
expectedBefore[0] = solochainA;
|
||||
expectedBefore[1] = solochainB;
|
||||
assertEq(
|
||||
serviceManager.buildNewValidatorSetMessageForEra(0),
|
||||
_buildExpectedMessage(expectedBefore, 0)
|
||||
);
|
||||
|
||||
// Flip multipliers: strategy 1 now has high multiplier
|
||||
IStrategy[] memory strategies = _getStrategies();
|
||||
IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory flipped =
|
||||
new IRewardsCoordinatorTypes.StrategyAndMultiplier[](3);
|
||||
flipped[0] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
|
||||
strategy: strategies[0], multiplier: 1
|
||||
});
|
||||
flipped[1] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
|
||||
strategy: strategies[1], multiplier: 10000
|
||||
});
|
||||
flipped[2] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
|
||||
strategy: strategies[2], multiplier: 1
|
||||
});
|
||||
|
||||
cheats.prank(avsOwner);
|
||||
serviceManager.setStrategiesAndMultipliers(flipped);
|
||||
|
||||
// After update: B ranks first (strategy 1 now has multiplier 10_000)
|
||||
address[] memory expectedAfter = new address[](2);
|
||||
expectedAfter[0] = solochainB;
|
||||
expectedAfter[1] = solochainA;
|
||||
assertEq(
|
||||
serviceManager.buildNewValidatorSetMessageForEra(0),
|
||||
_buildExpectedMessage(expectedAfter, 0)
|
||||
);
|
||||
}
|
||||
|
||||
// ============ Selection Tests ============
|
||||
|
||||
// Test #1: 3 strategies with different multipliers; verify correct ordering
|
||||
function test_weightedStake_multipleStrategies() public {
|
||||
uint96[] memory mults = new uint96[](3);
|
||||
mults[0] = 5000;
|
||||
mults[1] = 10000;
|
||||
mults[2] = 2000;
|
||||
_setupMultipliers(mults);
|
||||
|
||||
// Op A: heavy in strategy 0 (multiplier 5000)
|
||||
address opA = vm.addr(101);
|
||||
address solochainA = address(uint160(0xA01));
|
||||
uint256[] memory stakesA = new uint256[](3);
|
||||
stakesA[0] = 1000 ether;
|
||||
stakesA[1] = 100 ether;
|
||||
stakesA[2] = 100 ether;
|
||||
_registerOperator(opA, solochainA, stakesA);
|
||||
|
||||
// Op B: heavy in strategy 1 (multiplier 10000) → highest weighted stake
|
||||
address opB = vm.addr(102);
|
||||
address solochainB = address(uint160(0xB01));
|
||||
uint256[] memory stakesB = new uint256[](3);
|
||||
stakesB[0] = 100 ether;
|
||||
stakesB[1] = 1000 ether;
|
||||
stakesB[2] = 100 ether;
|
||||
_registerOperator(opB, solochainB, stakesB);
|
||||
|
||||
// Op C: heavy in strategy 2 (multiplier 2000) → lowest weighted stake
|
||||
address opC = vm.addr(103);
|
||||
address solochainC = address(uint160(0xC01));
|
||||
uint256[] memory stakesC = new uint256[](3);
|
||||
stakesC[0] = 100 ether;
|
||||
stakesC[1] = 100 ether;
|
||||
stakesC[2] = 1000 ether;
|
||||
_registerOperator(opC, solochainC, stakesC);
|
||||
|
||||
_advancePastAllocationConfigDelay();
|
||||
|
||||
_allocateForOperator(opA);
|
||||
_allocateForOperator(opB);
|
||||
_allocateForOperator(opC);
|
||||
|
||||
_advancePastAllocationEffect();
|
||||
|
||||
// Expected order: B (highest multiplied strategy), A, C
|
||||
address[] memory expected = new address[](3);
|
||||
expected[0] = solochainB;
|
||||
expected[1] = solochainA;
|
||||
expected[2] = solochainC;
|
||||
|
||||
assertEq(
|
||||
serviceManager.buildNewValidatorSetMessageForEra(0), _buildExpectedMessage(expected, 0)
|
||||
);
|
||||
}
|
||||
|
||||
// Test #2: 2 operators with identical weighted stake; lower Eth address ranks first
|
||||
function test_tieBreak_lowerAddressWins() public {
|
||||
_setupMultipliers(_uniformMultipliers());
|
||||
|
||||
address addrA = vm.addr(201);
|
||||
address addrB = vm.addr(202);
|
||||
|
||||
// Ensure addrLow < addrHigh
|
||||
address addrLow = addrA < addrB ? addrA : addrB;
|
||||
address addrHigh = addrA < addrB ? addrB : addrA;
|
||||
|
||||
address solochainLow = address(uint160(0xBB));
|
||||
address solochainHigh = address(uint160(0xAA));
|
||||
|
||||
_registerOperator(addrLow, solochainLow, _uniformStakes(500 ether));
|
||||
_registerOperator(addrHigh, solochainHigh, _uniformStakes(500 ether));
|
||||
|
||||
_advancePastAllocationConfigDelay();
|
||||
|
||||
_allocateForOperator(addrLow);
|
||||
_allocateForOperator(addrHigh);
|
||||
|
||||
_advancePastAllocationEffect();
|
||||
|
||||
// Lower Eth address wins tie-break
|
||||
address[] memory expected = new address[](2);
|
||||
expected[0] = solochainLow;
|
||||
expected[1] = solochainHigh;
|
||||
|
||||
assertEq(
|
||||
serviceManager.buildNewValidatorSetMessageForEra(0), _buildExpectedMessage(expected, 0)
|
||||
);
|
||||
}
|
||||
|
||||
// Test #3: Register 35 operators; verify only top 32 selected
|
||||
function test_topN_moreThan32() public {
|
||||
_setupMultipliers(_uniformMultipliers());
|
||||
|
||||
uint256 totalOps = 35;
|
||||
address[] memory operators = new address[](totalOps);
|
||||
address[] memory solochainAddrs = new address[](totalOps);
|
||||
|
||||
for (uint256 i = 0; i < totalOps; i++) {
|
||||
operators[i] = vm.addr(300 + i);
|
||||
solochainAddrs[i] = address(uint160(0x1000 + i));
|
||||
_registerOperator(operators[i], solochainAddrs[i], _uniformStakes((i + 1) * 10 ether));
|
||||
}
|
||||
|
||||
_advancePastAllocationConfigDelay();
|
||||
|
||||
for (uint256 i = 0; i < totalOps; i++) {
|
||||
_allocateForOperator(operators[i]);
|
||||
}
|
||||
|
||||
_advancePastAllocationEffect();
|
||||
|
||||
bytes memory message = serviceManager.buildNewValidatorSetMessageForEra(0);
|
||||
|
||||
// Top 32 by descending stake: operators at indices 34, 33, ..., 3
|
||||
address[] memory expected = new address[](32);
|
||||
for (uint256 i = 0; i < 32; i++) {
|
||||
expected[i] = solochainAddrs[totalOps - 1 - i];
|
||||
}
|
||||
|
||||
assertEq(message, _buildExpectedMessage(expected, 0));
|
||||
}
|
||||
|
||||
// Test #4: 5 operators; all included in output
|
||||
function test_lessThan32_includesAll() public {
|
||||
_setupMultipliers(_uniformMultipliers());
|
||||
|
||||
uint256 totalOps = 5;
|
||||
address[] memory operators = new address[](totalOps);
|
||||
address[] memory solochainAddrs = new address[](totalOps);
|
||||
|
||||
for (uint256 i = 0; i < totalOps; i++) {
|
||||
operators[i] = vm.addr(400 + i);
|
||||
solochainAddrs[i] = address(uint160(0x2000 + i));
|
||||
_registerOperator(operators[i], solochainAddrs[i], _uniformStakes((i + 1) * 100 ether));
|
||||
}
|
||||
|
||||
_advancePastAllocationConfigDelay();
|
||||
|
||||
for (uint256 i = 0; i < totalOps; i++) {
|
||||
_allocateForOperator(operators[i]);
|
||||
}
|
||||
|
||||
_advancePastAllocationEffect();
|
||||
|
||||
// All 5 included, sorted by descending stake
|
||||
address[] memory expected = new address[](5);
|
||||
for (uint256 i = 0; i < 5; i++) {
|
||||
expected[i] = solochainAddrs[totalOps - 1 - i];
|
||||
}
|
||||
|
||||
assertEq(
|
||||
serviceManager.buildNewValidatorSetMessageForEra(0), _buildExpectedMessage(expected, 0)
|
||||
);
|
||||
}
|
||||
|
||||
// Test #5: Operator with zero allocation excluded
|
||||
function test_zeroWeightedStake_filtered() public {
|
||||
_setupMultipliers(_uniformMultipliers());
|
||||
|
||||
address op1 = vm.addr(501);
|
||||
address solochain1 = address(uint160(0x3001));
|
||||
_registerOperator(op1, solochain1, _uniformStakes(100 ether));
|
||||
|
||||
address op2 = vm.addr(502);
|
||||
address solochain2 = address(uint160(0x3002));
|
||||
_registerOperator(op2, solochain2, _uniformStakes(200 ether));
|
||||
|
||||
// op3 registered but NOT allocated → zero weighted stake
|
||||
address op3 = vm.addr(503);
|
||||
address solochain3 = address(uint160(0x3003));
|
||||
_registerOperator(op3, solochain3, _uniformStakes(300 ether));
|
||||
|
||||
_advancePastAllocationConfigDelay();
|
||||
|
||||
// Only allocate for op1 and op2
|
||||
_allocateForOperator(op1);
|
||||
_allocateForOperator(op2);
|
||||
|
||||
_advancePastAllocationEffect();
|
||||
|
||||
// op3 should be filtered out
|
||||
address[] memory expected = new address[](2);
|
||||
expected[0] = solochain2;
|
||||
expected[1] = solochain1;
|
||||
|
||||
assertEq(
|
||||
serviceManager.buildNewValidatorSetMessageForEra(0), _buildExpectedMessage(expected, 0)
|
||||
);
|
||||
}
|
||||
|
||||
// Test #6: A zero multiplier is accepted and causes that strategy's stake to contribute
|
||||
// no weight. The operator is still included if other strategies have non-zero multipliers.
|
||||
function test_zeroMultiplier_accepted_contributesNoWeight() public {
|
||||
IStrategy[] memory strategies = _getStrategies();
|
||||
|
||||
// Zero-out the first strategy's multiplier via setStrategiesAndMultipliers
|
||||
IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory sm =
|
||||
new IRewardsCoordinatorTypes.StrategyAndMultiplier[](1);
|
||||
sm[0] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
|
||||
strategy: strategies[0], multiplier: 0
|
||||
});
|
||||
|
||||
cheats.prank(avsOwner);
|
||||
serviceManager.setStrategiesAndMultipliers(sm);
|
||||
|
||||
assertEq(serviceManager.strategiesAndMultipliers(strategies[0]), 0);
|
||||
}
|
||||
|
||||
// Test #12: Full integration — weighted selection + correct message encoding
|
||||
function test_buildMessage_encodesCorrectly() public {
|
||||
_setupMultipliers(_uniformMultipliers());
|
||||
|
||||
address op1 = vm.addr(701);
|
||||
address solochain1 = address(uint160(0x5001));
|
||||
_registerOperator(op1, solochain1, _uniformStakes(500 ether));
|
||||
|
||||
address op2 = vm.addr(702);
|
||||
address solochain2 = address(uint160(0x5002));
|
||||
_registerOperator(op2, solochain2, _uniformStakes(1000 ether));
|
||||
|
||||
_advancePastAllocationConfigDelay();
|
||||
|
||||
_allocateForOperator(op1);
|
||||
_allocateForOperator(op2);
|
||||
|
||||
_advancePastAllocationEffect();
|
||||
|
||||
bytes memory message = serviceManager.buildNewValidatorSetMessageForEra(0);
|
||||
|
||||
// op2 has higher stake → first
|
||||
address[] memory expected = new address[](2);
|
||||
expected[0] = solochain2;
|
||||
expected[1] = solochain1;
|
||||
|
||||
assertEq(message, _buildExpectedMessage(expected, 0));
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,9 @@ import {DataHavenServiceManager} from "../src/DataHavenServiceManager.sol";
|
|||
import {
|
||||
TransparentUpgradeableProxy
|
||||
} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
|
||||
import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol";
|
||||
import {
|
||||
IRewardsCoordinatorTypes
|
||||
} from "eigenlayer-contracts/src/contracts/interfaces/IRewardsCoordinator.sol";
|
||||
|
||||
contract ValidatorSetSubmitterTest is SnowbridgeAndAVSDeployer {
|
||||
address public submitterA = address(uint160(uint256(keccak256("submitterA"))));
|
||||
|
|
@ -35,7 +37,8 @@ contract ValidatorSetSubmitterTest is SnowbridgeAndAVSDeployer {
|
|||
== this.test_buildNewValidatorSetMessageForEra_exactEncoding.selector
|
||||
) {
|
||||
beforeTestCalldata = new bytes[](1);
|
||||
beforeTestCalldata[0] = abi.encodeWithSelector(this.setupValidatorsAsOperators.selector);
|
||||
beforeTestCalldata[0] =
|
||||
abi.encodeWithSelector(this.setupValidatorsAsOperatorsWithAllocations.selector);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -179,7 +182,8 @@ contract ValidatorSetSubmitterTest is SnowbridgeAndAVSDeployer {
|
|||
|
||||
function test_sendNewValidatorSetForEra_revertsWhenSubmitterIsZeroAddress() public {
|
||||
// Deploy a fresh proxy with address(0) as the submitter
|
||||
IStrategy[] memory emptyStrategies = new IStrategy[](0);
|
||||
IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory emptyStrategies =
|
||||
new IRewardsCoordinatorTypes.StrategyAndMultiplier[](0);
|
||||
|
||||
cheats.startPrank(regularDeployer);
|
||||
DataHavenServiceManager zeroSubmitterSM = DataHavenServiceManager(
|
||||
|
|
|
|||
|
|
@ -238,14 +238,6 @@ contract AVSDeployer is Test {
|
|||
serviceManagerImplementation =
|
||||
new DataHavenServiceManager(rewardsCoordinator, allocationManager);
|
||||
|
||||
// Create array for validators strategies required by DataHavenServiceManager
|
||||
IStrategy[] memory validatorsStrategies = new IStrategy[](deployedStrategies.length);
|
||||
|
||||
// For testing purposes, we'll use the deployed strategies for validators
|
||||
for (uint256 i = 0; i < deployedStrategies.length; i++) {
|
||||
validatorsStrategies[i] = deployedStrategies[i];
|
||||
}
|
||||
|
||||
serviceManager = DataHavenServiceManager(
|
||||
address(
|
||||
new TransparentUpgradeableProxy(
|
||||
|
|
@ -255,7 +247,7 @@ contract AVSDeployer is Test {
|
|||
DataHavenServiceManager.initialize.selector,
|
||||
avsOwner,
|
||||
rewardsInitiator,
|
||||
validatorsStrategies,
|
||||
defaultStrategyAndMultipliers,
|
||||
address(snowbridgeGatewayMock),
|
||||
avsOwner
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,11 @@ 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";
|
||||
|
|
@ -145,6 +150,61 @@ contract SnowbridgeAndAVSDeployer is AVSDeployer {
|
|||
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]);
|
||||
|
|
|
|||
247
specs/validator-set-selection/validator-set-selection.md
Normal file
247
specs/validator-set-selection/validator-set-selection.md
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
# Validator Set Selection Specification
|
||||
## Top-32 by Weighted Stake (Continuation of PR #433)
|
||||
|
||||
- Status: Draft
|
||||
- Owners: DataHaven Team
|
||||
- Last Updated: February 12, 2026
|
||||
- Depends on: PR #433 (`feat: automated validator set submission with era targeting`)
|
||||
|
||||
## 1. Summary
|
||||
|
||||
PR #433 introduced era-targeted validator-set submission with a dedicated submitter role and runtime era validation. This spec is a continuation of that work.
|
||||
|
||||
This document adds deterministic weighted-stake selection so the outbound validator set is ranked before it is bridged:
|
||||
|
||||
1. Ethereum computes weighted stake per operator.
|
||||
2. Ethereum deterministically sorts operators and selects top candidates.
|
||||
3. DataHaven enforces a final total active authority cap of 32 after combining whitelisted and external validators.
|
||||
|
||||
The era-targeting model from PR #433 remains unchanged.
|
||||
|
||||
## 2. Baseline From PR #433
|
||||
|
||||
This spec assumes the following behavior already exists:
|
||||
|
||||
1. `DataHavenServiceManager.sendNewValidatorSetForEra(uint64 targetEra, ...)` is used for submission.
|
||||
2. Submission is restricted to `validatorSetSubmitter` (`onlyValidatorSetSubmitter`).
|
||||
3. `external_index` in the Snowbridge payload is the `targetEra`.
|
||||
4. DataHaven runtime enforces era validity (`targetEra` old/too-new/duplicate checks).
|
||||
|
||||
## 3. Goals
|
||||
|
||||
1. Select external validators by weighted stake instead of raw member ordering.
|
||||
2. Keep selection deterministic (`same chain state -> same selected set`).
|
||||
3. Preserve PR #433 era-targeting invariants and submitter authorization flow.
|
||||
4. Enforce total active authority cap = 32 (`whitelisted + external`).
|
||||
5. Keep payload shape stable unless there is a hard requirement to version it.
|
||||
|
||||
## 4. Non-Goals
|
||||
|
||||
1. Replacing PR #433 submitter-role model.
|
||||
2. Changing PR #433 era-target validation semantics.
|
||||
3. Redesigning Snowbridge transport internals.
|
||||
4. Changing reward formulas in this spec.
|
||||
|
||||
## 5. Current Behavior (Post-PR #433)
|
||||
|
||||
### 5.1 Ethereum
|
||||
|
||||
`buildNewValidatorSetMessageForEra(targetEra)` gathers all operator-set members with a mapped solochain address and forwards them in that order. There is no stake-based ranking.
|
||||
|
||||
### 5.2 Payload
|
||||
|
||||
Current payload carries:
|
||||
|
||||
1. `validators`
|
||||
2. `external_index` (interpreted as `targetEra`)
|
||||
|
||||
### 5.3 DataHaven Runtime
|
||||
|
||||
`set_external_validators_inner()` stores incoming validators and `ExternalIndex`, then era application and validator composition logic consume them.
|
||||
|
||||
### 5.4 Limitation
|
||||
|
||||
Without stake-aware ordering, high-stake operators may be displaced by lower-stake operators when list size pressure or downstream caps apply.
|
||||
|
||||
## 6. Design Decisions
|
||||
|
||||
### D1. Do ranking on Ethereum
|
||||
|
||||
EigenLayer membership/allocation context is available on Ethereum, so weighted ranking is computed there.
|
||||
|
||||
### D2. Keep PR #433 era semantics unchanged
|
||||
|
||||
`external_index` must continue to encode `targetEra`. This spec does not repurpose it (no nonce/block-number substitution).
|
||||
|
||||
### D3. Deterministic tie-break
|
||||
|
||||
For equal weighted stake, lower Ethereum operator address wins.
|
||||
|
||||
### D4. Cap applies to total active authorities
|
||||
|
||||
Final active validator set must satisfy:
|
||||
|
||||
`final_active = take_32(dedupe(whitelisted ++ external_sorted_limited))`
|
||||
|
||||
### D5. Strategy multipliers are explicit and default to zero if unset
|
||||
|
||||
Multipliers are owner-managed in `strategiesAndMultipliers`. If an entry is unset for a strategy, its effective multiplier is `0` (no weighted contribution).
|
||||
|
||||
### D6. Keep strategy list and multipliers in sync
|
||||
|
||||
Multiplier lifecycle is tied to strategy lifecycle:
|
||||
|
||||
1. Add strategy -> add multiplier in the same call via `IRewardsCoordinatorTypes.StrategyAndMultiplier` struct.
|
||||
2. Remove strategy -> delete multiplier in the same call.
|
||||
|
||||
## 7. Weighted Stake Model
|
||||
|
||||
For each operator `o`:
|
||||
|
||||
`weightedStake(o) = sum_i( allocatedStake(o, strategy_i) * multiplier(strategy_i) )`
|
||||
|
||||
Where:
|
||||
|
||||
1. `allocatedStake` comes from EigenLayer allocation data.
|
||||
2. `multiplier` is a per-strategy weight (no normalization divisor is applied during ranking).
|
||||
|
||||
### 7.1 Strategy Weight Semantics
|
||||
|
||||
1. Every supported strategy should have an explicit multiplier entry for operational clarity.
|
||||
2. Missing multiplier entry is treated as `0` multiplier.
|
||||
3. Multiplier values are managed explicitly by owner/governance.
|
||||
|
||||
### 7.2 Unit Assumption
|
||||
|
||||
Stake inputs must be unit-consistent across strategies. If they are not, normalize before summing.
|
||||
|
||||
## 8. Ethereum Contract Changes (On Top of PR #433)
|
||||
|
||||
File: `contracts/src/DataHavenServiceManager.sol`
|
||||
|
||||
### 8.1 New State
|
||||
|
||||
```solidity
|
||||
uint32 public constant MAX_ACTIVE_VALIDATORS = 32;
|
||||
mapping(IStrategy => uint96) public strategiesAndMultipliers;
|
||||
```
|
||||
|
||||
### 8.2 New/Updated Admin APIs
|
||||
|
||||
```solidity
|
||||
function setStrategiesAndMultipliers(IRewardsCoordinatorTypes.StrategyAndMultiplier[] calldata strategyMultipliers) external onlyOwner;
|
||||
function addStrategiesToValidatorsSupportedStrategies(IRewardsCoordinatorTypes.StrategyAndMultiplier[] calldata strategyMultipliers) external onlyOwner;
|
||||
function removeStrategiesFromValidatorsSupportedStrategies(IStrategy[] calldata strategies) external onlyOwner;
|
||||
function getStrategiesAndMultipliers() external view returns (IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory);
|
||||
```
|
||||
|
||||
Using EigenLayer's `StrategyAndMultiplier` struct pairs each strategy with its multiplier, eliminating the possibility of length mismatches between parallel arrays. Duplicate strategies in `addStrategies` are rejected by EigenLayer's `StrategyAlreadyInOperatorSet` check; duplicates in `setStrategiesAndMultipliers` are harmless (last-write-wins on the mapping).
|
||||
|
||||
### 8.3 Updated Selection Flow
|
||||
|
||||
`buildNewValidatorSetMessageForEra(uint64 targetEra)` should:
|
||||
|
||||
1. Read validator operator set members.
|
||||
2. Compute weighted stake per operator.
|
||||
3. Filter out operators with no solochain mapping.
|
||||
4. Resolve multiplier from `strategiesAndMultipliers` for each strategy used.
|
||||
5. If any strategy is missing a multiplier entry, treat it as `0` multiplier.
|
||||
6. Filter out operators with zero weighted stake.
|
||||
7. Select at most `MAX_ACTIVE_VALIDATORS` (32) candidates by weighted stake desc + address asc tie-break (if fewer than 32 eligible candidates exist, include all).
|
||||
8. Encode using existing payload shape with `externalIndex = targetEra`.
|
||||
|
||||
For any EigenLayer call that consumes `StrategyAndMultiplier[]`, materialize the list in ascending strategy-address order.
|
||||
|
||||
`sendNewValidatorSetForEra(...)` and `onlyValidatorSetSubmitter` remain unchanged from PR #433.
|
||||
|
||||
## 9. Bridge Message Format
|
||||
|
||||
No payload version bump in this spec.
|
||||
|
||||
Continue using existing `ReceiveValidators` message shape:
|
||||
|
||||
```text
|
||||
[EL_MESSAGE_ID]
|
||||
[MessageVersion]
|
||||
[ReceiveValidators]
|
||||
[validator_count]
|
||||
[validators (N * 20B)]
|
||||
[external_index (u64 targetEra)]
|
||||
```
|
||||
|
||||
If stake vectors are required in the future, that should be a separate versioned command proposal.
|
||||
|
||||
## 10. DataHaven Runtime Changes
|
||||
|
||||
File: `operator/pallets/external-validators/src/lib.rs`
|
||||
|
||||
### 10.1 Keep PR #433 era validation
|
||||
|
||||
Retain existing target-era gates and error semantics (`TargetEraTooOld`, `TargetEraTooNew`, `DuplicateOrStaleTargetEra`).
|
||||
|
||||
### 10.2 Enforce final total cap = 32
|
||||
|
||||
At validator composition time:
|
||||
|
||||
1. `w = whitelisted.len()`
|
||||
2. `external_budget = 32.saturating_sub(w)`
|
||||
3. Use at most `external_budget` external validators from the ranked list.
|
||||
4. Build final set as `take_32(dedupe(whitelisted ++ external_limited))`.
|
||||
|
||||
### 10.3 Runtime constants
|
||||
|
||||
`MaxExternalValidators` can remain a defensive bound, but final active enforcement must guarantee max 32 authorities.
|
||||
|
||||
## 11. Rollout Plan
|
||||
|
||||
1. Merge/deploy PR #433 baseline first (submitter role + era-target checks).
|
||||
2. Deploy ServiceManager upgrade with weighted ranking logic.
|
||||
3. Backfill/confirm `strategiesAndMultipliers` for all currently supported strategies.
|
||||
4. Deploy runtime changes for final total-cap enforcement.
|
||||
5. Re-run submitter daemon unchanged (it still submits `targetEra = ActiveEra + 1`).
|
||||
6. Monitor across multiple era cycles before production rollout.
|
||||
|
||||
## 12. Testing Plan
|
||||
|
||||
### 12.1 Solidity
|
||||
|
||||
1. Weighted stake computation across multiple strategies.
|
||||
2. Deterministic tie-break behavior.
|
||||
3. Top-32 selection when candidate count exceeds 32.
|
||||
4. Behavior when candidate count is below 32.
|
||||
5. Zero-stake filtering.
|
||||
6. Missing multiplier entries are treated as zero contribution.
|
||||
7. `addStrategies...` sets multipliers atomically via `StrategyAndMultiplier` struct.
|
||||
8. `removeStrategies...` removes multiplier entries for removed strategies.
|
||||
9. `getStrategiesAndMultipliers()` returns a list matching EigenLayer's operator set strategies.
|
||||
11. Integration with `buildNewValidatorSetMessageForEra(targetEra)` and correct target era encoding.
|
||||
|
||||
### 12.2 Runtime
|
||||
|
||||
1. Existing PR #433 era-validation tests continue to pass unchanged.
|
||||
2. Final active authority cap remains <= 32 with mixed whitelisted/external sets.
|
||||
3. Composition logic preserves whitelisted priority while enforcing cap.
|
||||
|
||||
### 12.3 Integration / E2E
|
||||
|
||||
1. End-to-end submission through `sendNewValidatorSetForEra` with ranked validator output.
|
||||
2. Delayed relay still fails with PR #433 semantics (no regressions).
|
||||
3. Ranked selection outcome is deterministic across repeated runs at fixed state.
|
||||
|
||||
## 13. Security Considerations
|
||||
|
||||
1. Owner-managed strategy weights are governance-sensitive and should remain multisig/governance controlled.
|
||||
2. Deterministic ordering prevents non-deterministic set drift.
|
||||
3. Preserve PR #433 stale/duplicate/too-early rejection invariants.
|
||||
4. Apply overflow checks in weighted arithmetic and any integer downcasts.
|
||||
|
||||
## 14. File Change Summary
|
||||
|
||||
1. `contracts/src/DataHavenServiceManager.sol`
|
||||
- weighted stake computation and deterministic top selection in `buildNewValidatorSetMessageForEra`.
|
||||
2. `contracts/src/interfaces/IDataHavenServiceManager.sol`
|
||||
- `strategiesAndMultipliers` naming and add/remove strategy API signature updates with multipliers.
|
||||
3. `operator/pallets/external-validators/src/lib.rs`
|
||||
- final authority cap enforcement at composition time (while keeping PR #433 era validation behavior).
|
||||
4. `contracts/test/*`, `operator/pallets/external-validators/src/tests.rs`, `test/e2e/suites/validator-set-update.test.ts`
|
||||
- unit/runtime/e2e coverage for weighted selection + strategy/multiplier sync + cap behavior + non-regression on era-targeted flow.
|
||||
|
|
@ -2047,6 +2047,13 @@ export const dataHavenServiceManagerAbi = [
|
|||
outputs: [{ name: '', internalType: 'string', type: 'string' }],
|
||||
stateMutability: 'view',
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
inputs: [],
|
||||
name: 'MAX_ACTIVE_VALIDATORS',
|
||||
outputs: [{ name: '', internalType: 'uint32', type: 'uint32' }],
|
||||
stateMutability: 'view',
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
inputs: [],
|
||||
|
|
@ -2058,9 +2065,17 @@ export const dataHavenServiceManagerAbi = [
|
|||
type: 'function',
|
||||
inputs: [
|
||||
{
|
||||
name: '_strategies',
|
||||
internalType: 'contract IStrategy[]',
|
||||
type: 'address[]',
|
||||
name: '_strategyMultipliers',
|
||||
internalType: 'struct IRewardsCoordinatorTypes.StrategyAndMultiplier[]',
|
||||
type: 'tuple[]',
|
||||
components: [
|
||||
{
|
||||
name: 'strategy',
|
||||
internalType: 'contract IStrategy',
|
||||
type: 'address',
|
||||
},
|
||||
{ name: 'multiplier', internalType: 'uint96', type: 'uint96' },
|
||||
],
|
||||
},
|
||||
],
|
||||
name: 'addStrategiesToValidatorsSupportedStrategies',
|
||||
|
|
@ -2102,15 +2117,44 @@ export const dataHavenServiceManagerAbi = [
|
|||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
inputs: [],
|
||||
name: 'getStrategiesAndMultipliers',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
internalType: 'struct IRewardsCoordinatorTypes.StrategyAndMultiplier[]',
|
||||
type: 'tuple[]',
|
||||
components: [
|
||||
{
|
||||
name: 'strategy',
|
||||
internalType: 'contract IStrategy',
|
||||
type: 'address',
|
||||
},
|
||||
{ name: 'multiplier', internalType: 'uint96', type: 'uint96' },
|
||||
],
|
||||
},
|
||||
],
|
||||
stateMutability: 'view',
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
inputs: [
|
||||
{ name: 'initialOwner', internalType: 'address', type: 'address' },
|
||||
{ name: '_rewardsInitiator', internalType: 'address', type: 'address' },
|
||||
{
|
||||
name: 'validatorsStrategies',
|
||||
internalType: 'contract IStrategy[]',
|
||||
type: 'address[]',
|
||||
name: 'validatorsStrategiesAndMultipliers',
|
||||
internalType: 'struct IRewardsCoordinatorTypes.StrategyAndMultiplier[]',
|
||||
type: 'tuple[]',
|
||||
components: [
|
||||
{
|
||||
name: 'strategy',
|
||||
internalType: 'contract IStrategy',
|
||||
type: 'address',
|
||||
},
|
||||
{ name: 'multiplier', internalType: 'uint96', type: 'uint96' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '_snowbridgeGatewayAddress',
|
||||
|
|
@ -2213,6 +2257,27 @@ export const dataHavenServiceManagerAbi = [
|
|||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
inputs: [
|
||||
{
|
||||
name: '_strategyMultipliers',
|
||||
internalType: 'struct IRewardsCoordinatorTypes.StrategyAndMultiplier[]',
|
||||
type: 'tuple[]',
|
||||
components: [
|
||||
{
|
||||
name: 'strategy',
|
||||
internalType: 'contract IStrategy',
|
||||
type: 'address',
|
||||
},
|
||||
{ name: 'multiplier', internalType: 'uint96', type: 'uint96' },
|
||||
],
|
||||
},
|
||||
],
|
||||
name: 'setStrategiesAndMultipliers',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
inputs: [
|
||||
|
|
@ -2252,6 +2317,13 @@ export const dataHavenServiceManagerAbi = [
|
|||
outputs: [{ name: '', internalType: 'address', type: 'address' }],
|
||||
stateMutability: 'view',
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
inputs: [{ name: '', internalType: 'contract IStrategy', type: 'address' }],
|
||||
name: 'strategiesAndMultipliers',
|
||||
outputs: [{ name: '', internalType: 'uint96', type: 'uint96' }],
|
||||
stateMutability: 'view',
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
inputs: [
|
||||
|
|
@ -2498,6 +2570,27 @@ export const dataHavenServiceManagerAbi = [
|
|||
],
|
||||
name: 'SolochainAddressUpdated',
|
||||
},
|
||||
{
|
||||
type: 'event',
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
name: 'strategyMultipliers',
|
||||
internalType: 'struct IRewardsCoordinatorTypes.StrategyAndMultiplier[]',
|
||||
type: 'tuple[]',
|
||||
components: [
|
||||
{
|
||||
name: 'strategy',
|
||||
internalType: 'contract IStrategy',
|
||||
type: 'address',
|
||||
},
|
||||
{ name: 'multiplier', internalType: 'uint96', type: 'uint96' },
|
||||
],
|
||||
indexed: false,
|
||||
},
|
||||
],
|
||||
name: 'StrategiesAndMultipliersSet',
|
||||
},
|
||||
{
|
||||
type: 'event',
|
||||
anonymous: false,
|
||||
|
|
@ -2580,6 +2673,7 @@ export const dataHavenServiceManagerAbi = [
|
|||
{ type: 'error', inputs: [], name: 'OnlyValidatorSetSubmitter' },
|
||||
{ type: 'error', inputs: [], name: 'OperatorNotInAllowlist' },
|
||||
{ type: 'error', inputs: [], name: 'SolochainAddressAlreadyAssigned' },
|
||||
{ type: 'error', inputs: [], name: 'StrategyNotInOperatorSet' },
|
||||
{ type: 'error', inputs: [], name: 'UnknownSolochainAddress' },
|
||||
{ type: 'error', inputs: [], name: 'ZeroAddress' },
|
||||
] as const
|
||||
|
|
@ -10719,6 +10813,15 @@ export const readDataHavenServiceManagerDatahavenAvsMetadata =
|
|||
functionName: 'DATAHAVEN_AVS_METADATA',
|
||||
})
|
||||
|
||||
/**
|
||||
* Wraps __{@link readContract}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `functionName` set to `"MAX_ACTIVE_VALIDATORS"`
|
||||
*/
|
||||
export const readDataHavenServiceManagerMaxActiveValidators =
|
||||
/*#__PURE__*/ createReadContract({
|
||||
abi: dataHavenServiceManagerAbi,
|
||||
functionName: 'MAX_ACTIVE_VALIDATORS',
|
||||
})
|
||||
|
||||
/**
|
||||
* Wraps __{@link readContract}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `functionName` set to `"VALIDATORS_SET_ID"`
|
||||
*/
|
||||
|
|
@ -10737,6 +10840,15 @@ export const readDataHavenServiceManagerBuildNewValidatorSetMessageForEra =
|
|||
functionName: 'buildNewValidatorSetMessageForEra',
|
||||
})
|
||||
|
||||
/**
|
||||
* Wraps __{@link readContract}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `functionName` set to `"getStrategiesAndMultipliers"`
|
||||
*/
|
||||
export const readDataHavenServiceManagerGetStrategiesAndMultipliers =
|
||||
/*#__PURE__*/ createReadContract({
|
||||
abi: dataHavenServiceManagerAbi,
|
||||
functionName: 'getStrategiesAndMultipliers',
|
||||
})
|
||||
|
||||
/**
|
||||
* Wraps __{@link readContract}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `functionName` set to `"owner"`
|
||||
*/
|
||||
|
|
@ -10764,6 +10876,15 @@ export const readDataHavenServiceManagerSnowbridgeGateway =
|
|||
functionName: 'snowbridgeGateway',
|
||||
})
|
||||
|
||||
/**
|
||||
* Wraps __{@link readContract}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `functionName` set to `"strategiesAndMultipliers"`
|
||||
*/
|
||||
export const readDataHavenServiceManagerStrategiesAndMultipliers =
|
||||
/*#__PURE__*/ createReadContract({
|
||||
abi: dataHavenServiceManagerAbi,
|
||||
functionName: 'strategiesAndMultipliers',
|
||||
})
|
||||
|
||||
/**
|
||||
* Wraps __{@link readContract}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `functionName` set to `"supportsAVS"`
|
||||
*/
|
||||
|
|
@ -10933,6 +11054,15 @@ export const writeDataHavenServiceManagerSetSnowbridgeGateway =
|
|||
functionName: 'setSnowbridgeGateway',
|
||||
})
|
||||
|
||||
/**
|
||||
* Wraps __{@link writeContract}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `functionName` set to `"setStrategiesAndMultipliers"`
|
||||
*/
|
||||
export const writeDataHavenServiceManagerSetStrategiesAndMultipliers =
|
||||
/*#__PURE__*/ createWriteContract({
|
||||
abi: dataHavenServiceManagerAbi,
|
||||
functionName: 'setStrategiesAndMultipliers',
|
||||
})
|
||||
|
||||
/**
|
||||
* Wraps __{@link writeContract}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `functionName` set to `"setValidatorSetSubmitter"`
|
||||
*/
|
||||
|
|
@ -11101,6 +11231,15 @@ export const simulateDataHavenServiceManagerSetSnowbridgeGateway =
|
|||
functionName: 'setSnowbridgeGateway',
|
||||
})
|
||||
|
||||
/**
|
||||
* Wraps __{@link simulateContract}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `functionName` set to `"setStrategiesAndMultipliers"`
|
||||
*/
|
||||
export const simulateDataHavenServiceManagerSetStrategiesAndMultipliers =
|
||||
/*#__PURE__*/ createSimulateContract({
|
||||
abi: dataHavenServiceManagerAbi,
|
||||
functionName: 'setStrategiesAndMultipliers',
|
||||
})
|
||||
|
||||
/**
|
||||
* Wraps __{@link simulateContract}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `functionName` set to `"setValidatorSetSubmitter"`
|
||||
*/
|
||||
|
|
@ -11242,6 +11381,15 @@ export const watchDataHavenServiceManagerSolochainAddressUpdatedEvent =
|
|||
eventName: 'SolochainAddressUpdated',
|
||||
})
|
||||
|
||||
/**
|
||||
* Wraps __{@link watchContractEvent}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `eventName` set to `"StrategiesAndMultipliersSet"`
|
||||
*/
|
||||
export const watchDataHavenServiceManagerStrategiesAndMultipliersSetEvent =
|
||||
/*#__PURE__*/ createWatchContractEvent({
|
||||
abi: dataHavenServiceManagerAbi,
|
||||
eventName: 'StrategiesAndMultipliersSet',
|
||||
})
|
||||
|
||||
/**
|
||||
* Wraps __{@link watchContractEvent}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `eventName` set to `"ValidatorAddedToAllowlist"`
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -7,11 +7,13 @@ import { $ } from "bun";
|
|||
import {
|
||||
allocationManagerAbi,
|
||||
dataHavenServiceManagerAbi,
|
||||
delegationManagerAbi
|
||||
delegationManagerAbi,
|
||||
strategyManagerAbi
|
||||
} from "contract-bindings";
|
||||
import { type Deployments, logger, waitForContainerToStart } from "utils";
|
||||
import { DEFAULT_SUBSTRATE_WS_PORT } from "utils/constants";
|
||||
import { getPublicPort } from "utils/docker";
|
||||
import { erc20Abi } from "viem";
|
||||
import { privateKeyToAccount } from "viem/accounts";
|
||||
import validatorSet from "../../configs/validator-set.json";
|
||||
import type { LaunchedNetwork } from "../../launcher/types/launchedNetwork";
|
||||
|
|
@ -120,9 +122,50 @@ export async function registerOperator(
|
|||
const { connectors, deployments } = options;
|
||||
const validator = getValidator(validatorName);
|
||||
const account = privateKeyToAccount(validator.privateKey as `0x${string}`);
|
||||
const { publicClient, walletClient } = connectors;
|
||||
|
||||
// Deposit tokens into deployed strategies
|
||||
const deployedStrategies = deployments.DeployedStrategies ?? [];
|
||||
for (const strategy of deployedStrategies) {
|
||||
const balance = await publicClient.readContract({
|
||||
address: strategy.underlyingToken as `0x${string}`,
|
||||
abi: erc20Abi,
|
||||
functionName: "balanceOf",
|
||||
args: [account.address]
|
||||
});
|
||||
|
||||
if (balance > 0n) {
|
||||
const depositAmount = balance / 10n;
|
||||
|
||||
const approveHash = await walletClient.writeContract({
|
||||
address: strategy.underlyingToken as `0x${string}`,
|
||||
abi: erc20Abi,
|
||||
functionName: "approve",
|
||||
args: [deployments.StrategyManager, depositAmount],
|
||||
account,
|
||||
chain: null
|
||||
});
|
||||
await publicClient.waitForTransactionReceipt({ hash: approveHash });
|
||||
|
||||
const depositHash = await walletClient.writeContract({
|
||||
address: deployments.StrategyManager,
|
||||
abi: strategyManagerAbi,
|
||||
functionName: "depositIntoStrategy",
|
||||
args: [
|
||||
strategy.address as `0x${string}`,
|
||||
strategy.underlyingToken as `0x${string}`,
|
||||
depositAmount
|
||||
],
|
||||
account,
|
||||
chain: null
|
||||
});
|
||||
await publicClient.waitForTransactionReceipt({ hash: depositHash });
|
||||
logger.debug(`Deposited ${depositAmount} tokens into strategy ${strategy.address}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Register as EigenLayer operator
|
||||
const operatorHash = await connectors.walletClient.writeContract({
|
||||
const operatorHash = await walletClient.writeContract({
|
||||
address: deployments.DelegationManager as `0x${string}`,
|
||||
abi: delegationManagerAbi,
|
||||
functionName: "registerAsOperator",
|
||||
|
|
@ -131,7 +174,7 @@ export async function registerOperator(
|
|||
chain: null
|
||||
});
|
||||
|
||||
const operatorReceipt = await connectors.publicClient.waitForTransactionReceipt({
|
||||
const operatorReceipt = await publicClient.waitForTransactionReceipt({
|
||||
hash: operatorHash
|
||||
});
|
||||
if (operatorReceipt.status !== "success") {
|
||||
|
|
@ -139,7 +182,7 @@ export async function registerOperator(
|
|||
}
|
||||
|
||||
// Register for operator sets
|
||||
const hash = await connectors.walletClient.writeContract({
|
||||
const registerHash = await walletClient.writeContract({
|
||||
address: deployments.AllocationManager as `0x${string}`,
|
||||
abi: allocationManagerAbi,
|
||||
functionName: "registerForOperatorSets",
|
||||
|
|
@ -155,10 +198,40 @@ export async function registerOperator(
|
|||
chain: null
|
||||
});
|
||||
|
||||
const receipt = await connectors.publicClient.waitForTransactionReceipt({ hash });
|
||||
if (receipt.status !== "success") {
|
||||
throw new Error(`Operator set registration failed: ${receipt.status}`);
|
||||
const registerReceipt = await publicClient.waitForTransactionReceipt({ hash: registerHash });
|
||||
if (registerReceipt.status !== "success") {
|
||||
throw new Error(`Operator set registration failed: ${registerReceipt.status}`);
|
||||
}
|
||||
|
||||
logger.debug(`Registered ${validatorName} as operator (gas: ${receipt.gasUsed})`);
|
||||
// Allocate full magnitude to the validator operator set
|
||||
const strategyAddresses = deployedStrategies.map((s) => s.address as `0x${string}`);
|
||||
const newMagnitudes = strategyAddresses.map(() => BigInt(1e18));
|
||||
|
||||
const allocateHash = await walletClient.writeContract({
|
||||
address: deployments.AllocationManager as `0x${string}`,
|
||||
abi: allocationManagerAbi,
|
||||
functionName: "modifyAllocations",
|
||||
args: [
|
||||
account.address,
|
||||
[
|
||||
{
|
||||
operatorSet: {
|
||||
avs: deployments.ServiceManager as `0x${string}`,
|
||||
id: 0
|
||||
},
|
||||
strategies: strategyAddresses,
|
||||
newMagnitudes
|
||||
}
|
||||
]
|
||||
],
|
||||
account,
|
||||
chain: null
|
||||
});
|
||||
|
||||
const allocateReceipt = await publicClient.waitForTransactionReceipt({ hash: allocateHash });
|
||||
if (allocateReceipt.status !== "success") {
|
||||
throw new Error(`Magnitude allocation failed: ${allocateReceipt.status}`);
|
||||
}
|
||||
|
||||
logger.debug(`Registered ${validatorName} as operator (gas: ${registerReceipt.gasUsed})`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,6 +69,20 @@ describe("Validator Set Update", () => {
|
|||
beforeAll(async () => {
|
||||
deployments = await parseDeploymentsFile();
|
||||
connectors = suite.getTestConnectors();
|
||||
|
||||
// Pause era rotation early so the active era stabilizes during tests 1-3 (~28s),
|
||||
// avoiding the ~80s wait inside the cross-chain test.
|
||||
// Tests 1-3 only touch Ethereum contracts and don't depend on era rotation.
|
||||
const { dhApi } = connectors;
|
||||
const pauseTx = dhApi.tx.Sudo.sudo({
|
||||
call: dhApi.tx.ExternalValidators.force_era({
|
||||
mode: { type: "ForceNone", value: undefined }
|
||||
}).decodedCall
|
||||
});
|
||||
const pauseResult = await pauseTx.signAndSubmit(getPapiSigner("ALITH"));
|
||||
if (!pauseResult.ok) {
|
||||
throw new Error("Failed to pause era rotation");
|
||||
}
|
||||
});
|
||||
|
||||
it("should verify test environment", async () => {
|
||||
|
|
@ -161,31 +175,18 @@ describe("Validator Set Update", () => {
|
|||
async () => {
|
||||
const { publicClient, walletClient, dhApi } = connectors;
|
||||
|
||||
// Pause era rotation so the active era doesn't advance while
|
||||
// Snowbridge relays the message (relay latency > era duration with fast-runtime).
|
||||
// DatahavenServiceManagerAddress is set during infrastructure setup by set-datahaven-parameters.
|
||||
const setupTx = dhApi.tx.Sudo.sudo({
|
||||
call: dhApi.tx.ExternalValidators.force_era({
|
||||
mode: { type: "ForceNone", value: undefined }
|
||||
}).decodedCall
|
||||
});
|
||||
const setupResult = await setupTx.signAndSubmit(getPapiSigner("ALITH"));
|
||||
if (!setupResult.ok) {
|
||||
throw new Error("Failed to pause era rotation");
|
||||
}
|
||||
// Wait for the active era to stabilize: ForceNone prevents new eras but
|
||||
// an already-triggered era may still be pending activation at the next session boundary.
|
||||
// Poll until CurrentEra == ActiveEra, meaning no pending era transition remains.
|
||||
// Era rotation was paused in beforeAll. Wait for any pending transition to settle
|
||||
// (ForceNone prevents new eras, but an in-progress one must finish first).
|
||||
let stableEraIndex: number;
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
await new Promise((r) => setTimeout(r, 12_000)); // ~2 substrate blocks
|
||||
const activeEra = (await dhApi.query.ExternalValidators.ActiveEra.getValue())?.index ?? 0;
|
||||
const currentEra = (await dhApi.query.ExternalValidators.CurrentEra.getValue()) ?? 0;
|
||||
if (currentEra === activeEra) {
|
||||
stableEraIndex = activeEra;
|
||||
break;
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 6_000)); // ~1 substrate block
|
||||
}
|
||||
|
||||
const targetEra = BigInt(stableEraIndex + 1);
|
||||
|
|
@ -201,12 +202,22 @@ describe("Validator Set Update", () => {
|
|||
chain: null
|
||||
});
|
||||
const receipt = await publicClient.waitForTransactionReceipt({ hash });
|
||||
logger.info(
|
||||
`sendNewValidatorSet tx status: ${receipt.status}, block: ${receipt.blockNumber}`
|
||||
);
|
||||
expect(receipt.status).toBe("success");
|
||||
|
||||
// Verify OutboundMessageAccepted event was emitted
|
||||
const hasOutboundAccepted = (receipt.logs ?? []).some((log: any) => {
|
||||
try {
|
||||
const decoded = decodeEventLog({ abi: gatewayAbi, data: log.data, topics: log.topics });
|
||||
const decoded = decodeEventLog({
|
||||
abi: gatewayAbi,
|
||||
data: log.data,
|
||||
topics: log.topics
|
||||
});
|
||||
if (decoded.eventName === "OutboundMessageAccepted") {
|
||||
logger.info(`OutboundMessageAccepted event: nonce=${(decoded.args as any)?.nonce}`);
|
||||
}
|
||||
return decoded.eventName === "OutboundMessageAccepted";
|
||||
} catch {
|
||||
return false;
|
||||
|
|
@ -214,6 +225,7 @@ describe("Validator Set Update", () => {
|
|||
});
|
||||
expect(hasOutboundAccepted).toBe(true);
|
||||
|
||||
logger.info("Waiting for ExternalValidators.ExternalValidatorsSet event on DataHaven...");
|
||||
// Wait for the validator set to be updated on Substrate
|
||||
await waitForDataHavenEvent({
|
||||
api: dhApi,
|
||||
|
|
|
|||
|
|
@ -124,6 +124,29 @@ export const setupValidators = async (options: SetupValidatorsOptions): Promise<
|
|||
logger.success(`Successfully registered validator ${validator.publicKey}`);
|
||||
}
|
||||
|
||||
// Allocate stake for each validator (must run in a separate script because
|
||||
// the allocation delay needs at least 1 block after registerAsOperator)
|
||||
logger.info("📊 Allocating operator stake...");
|
||||
for (const [i, validator] of validatorsToRegister.entries()) {
|
||||
logger.info(`📊 Allocating stake for validator ${i} (${validator.publicKey})`);
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
NETWORK: networkName,
|
||||
OPERATOR_PRIVATE_KEY: validator.privateKey,
|
||||
OPERATOR_SOLOCHAIN_ADDRESS: validator.solochainAddress || ""
|
||||
};
|
||||
|
||||
const allocateCommand = `forge script script/transact/AllocateOperatorStake.s.sol --rpc-url ${rpcUrl} --broadcast --no-rpc-rate-limit --non-interactive`;
|
||||
await runShellCommandWithLogger(allocateCommand, {
|
||||
env,
|
||||
cwd: "../contracts",
|
||||
logLevel: "debug"
|
||||
});
|
||||
|
||||
logger.success(`Successfully allocated stake for validator ${validator.publicKey}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue