mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-23 17:28:23 +00:00
fix: 🩹 map validator address to operator address for rewards & slashes (#441)
## Summary Slashing and rewards submissions were submitted through the bridge with their **solochain address** , while EigenLayer expects the **ethereum operator address**, the addresses were not being translated, so the protocol was broken. This PR adds a **reverse mapping** (Solochain address → Eth address) and uses it in both the slashing and rewards paths so that: - `slashValidatorsOperator` accepts requests where `operator` is a Solochain address and translates it to the Eth operator before calling EigenLayer. - `submitRewards` translates each `operatorRewards[].operator` from Solochain to Eth before calling the RewardsCoordinator. - Unknown or unmapped solochain addresses cause a revert (`UnknownSolochainAddress`) instead of silently failing. ## What's changed ### DataHavenServiceManager - **Reverse mapping**: `mapping(address => address) public validatorSolochainAddressToEthAddress` (Solochain → Eth), with `__GAP` reduced by one slot for upgradeable layout. - **Helper**: `_ethOperatorFromSolochain(address)` – returns Eth operator for a Solochain address, reverts with `UnknownSolochainAddress()` if unmapped. - **Registration / lifecycle**: - `registerOperator`: populates both forward and reverse mappings; enforces uniqueness (one Solochain per operator) and clears old reverse entry when an operator re-registers with a new Solochain. - `deregisterOperator`: clears both forward and reverse entries. - `updateSolochainAddressForValidator`: updates both mappings, enforces uniqueness and clears the previous Solochain's reverse entry. - **Slashing**: `slashValidatorsOperator` uses `_ethOperatorFromSolochain(slashings[i].operator)` so requests keyed by Solochain address are translated before calling EigenLayer. - **Rewards**: `submitRewards` builds a translated copy of the submission with each `operatorRewards[].operator` set via `_ethOperatorFromSolochain(...)`; unmapped addresses revert. ### IDataHavenServiceManager - New getter: `validatorSolochainAddressToEthAddress(address solochain) external view returns (address)`. - New errors: `UnknownSolochainAddress()`, `SolochainAddressAlreadyAssigned()`. ### Storage and fixtures - Storage snapshot updated for the new state variable. - `DataHavenServiceManagerBadLayout.sol` updated (reverse mapping + gap) for layout negative tests. - Storage layout test extended to assert the reverse mapping is preserved across proxy upgrade. ### Tests - **Slashing.t.sol**: Slashing with Solochain address (translation and emit of Eth operator); negative test for unmapped Solochain reverting with `UnknownSolochainAddress()`. - **RewardsSubmitter.t.sol**: Rewards submission with Solochain addresses (translation to Eth in RewardsCoordinator calldata); negative test for unmapped Solochain. - **StorageLayout.t.sol**: Reverse mapping preserved after upgrade. - **OperatorAddressMappings.t.sol** (new): Uniqueness (Solochain already assigned to another operator), update/deregister clearing reverse mapping, and getter behaviour. ## Testing - **Unit tests**: `forge test` from `contracts/` (all existing and new tests pass). - **Storage**: - `./scripts/check-storage-layout.sh` - `./scripts/check-storage-layout-negative.sh` - **Coverage**: Slashing path (Solochain → Eth translation + revert), rewards path (translation + revert), registration/update/deregister (reverse mapping and uniqueness), and storage layout upgrade preservation. --------- Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
This commit is contained in:
parent
b45009f62d
commit
ddbc9bdd8b
12 changed files with 1012 additions and 570 deletions
|
|
@ -1 +1 @@
|
|||
711490494719593c219c35ca496cd28b86d9f54a
|
||||
17cfdd8c5a2d3396260b62742e51a0b8985def63
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -16,9 +16,10 @@ contract DataHavenServiceManagerBadLayout is OwnableUpgradeable {
|
|||
mapping(address => bool) public validatorsAllowlist;
|
||||
IGatewayV2 private _snowbridgeGateway;
|
||||
mapping(address => address) public validatorEthAddressToSolochainAddress;
|
||||
mapping(address => address) public validatorSolochainAddressToEthAddress;
|
||||
|
||||
// Keep the original gap size to mirror shape, despite the shift
|
||||
uint256[46] private __GAP;
|
||||
uint256[45] private __GAP;
|
||||
|
||||
// Keep a compatible constructor signature for upgrade tests.
|
||||
constructor(
|
||||
|
|
|
|||
|
|
@ -64,9 +64,12 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
|
|||
/// @inheritdoc IDataHavenServiceManager
|
||||
mapping(address => address) public validatorEthAddressToSolochainAddress;
|
||||
|
||||
/// @inheritdoc IDataHavenServiceManager
|
||||
mapping(address => address) public validatorSolochainAddressToEthAddress;
|
||||
|
||||
/// @notice Storage gap for upgradeability (must be at end of state variables)
|
||||
// solhint-disable-next-line var-name-mixedcase
|
||||
uint256[46] private __GAP;
|
||||
uint256[45] private __GAP;
|
||||
|
||||
// ============ Modifiers ============
|
||||
|
||||
|
|
@ -190,7 +193,20 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
|
|||
address solochainAddress
|
||||
) external onlyValidator {
|
||||
require(solochainAddress != address(0), ZeroAddress());
|
||||
|
||||
address existingEthOperator = validatorSolochainAddressToEthAddress[solochainAddress];
|
||||
require(
|
||||
existingEthOperator == address(0) || existingEthOperator == msg.sender,
|
||||
SolochainAddressAlreadyAssigned()
|
||||
);
|
||||
|
||||
address oldSolochainAddress = validatorEthAddressToSolochainAddress[msg.sender];
|
||||
if (oldSolochainAddress != address(0) && oldSolochainAddress != solochainAddress) {
|
||||
delete validatorSolochainAddressToEthAddress[oldSolochainAddress];
|
||||
}
|
||||
|
||||
validatorEthAddressToSolochainAddress[msg.sender] = solochainAddress;
|
||||
validatorSolochainAddressToEthAddress[solochainAddress] = msg.sender;
|
||||
emit SolochainAddressUpdated(msg.sender, solochainAddress);
|
||||
}
|
||||
|
||||
|
|
@ -221,7 +237,21 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
|
|||
require(operatorSetIds.length == 1, CantRegisterToMultipleOperatorSets());
|
||||
require(operatorSetIds[0] == VALIDATORS_SET_ID, InvalidOperatorSetId());
|
||||
require(validatorsAllowlist[operator], OperatorNotInAllowlist());
|
||||
validatorEthAddressToSolochainAddress[operator] = _toAddress(data);
|
||||
|
||||
address solochainAddress = _toAddress(data);
|
||||
address existingEthOperator = validatorSolochainAddressToEthAddress[solochainAddress];
|
||||
require(
|
||||
existingEthOperator == address(0) || existingEthOperator == operator,
|
||||
SolochainAddressAlreadyAssigned()
|
||||
);
|
||||
|
||||
address oldSolochainAddress = validatorEthAddressToSolochainAddress[operator];
|
||||
if (oldSolochainAddress != address(0) && oldSolochainAddress != solochainAddress) {
|
||||
delete validatorSolochainAddressToEthAddress[oldSolochainAddress];
|
||||
}
|
||||
|
||||
validatorEthAddressToSolochainAddress[operator] = solochainAddress;
|
||||
validatorSolochainAddressToEthAddress[solochainAddress] = operator;
|
||||
|
||||
emit OperatorRegistered(operator, operatorSetIds[0]);
|
||||
}
|
||||
|
|
@ -236,7 +266,11 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
|
|||
require(operatorSetIds.length == 1, CantDeregisterFromMultipleOperatorSets());
|
||||
require(operatorSetIds[0] == VALIDATORS_SET_ID, InvalidOperatorSetId());
|
||||
|
||||
address oldSolochainAddress = validatorEthAddressToSolochainAddress[operator];
|
||||
delete validatorEthAddressToSolochainAddress[operator];
|
||||
if (oldSolochainAddress != address(0)) {
|
||||
delete validatorSolochainAddressToEthAddress[oldSolochainAddress];
|
||||
}
|
||||
|
||||
emit OperatorDeregistered(operator, operatorSetIds[0]);
|
||||
}
|
||||
|
|
@ -297,16 +331,22 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
|
|||
function submitRewards(
|
||||
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission calldata submission
|
||||
) external override onlyRewardsInitiator {
|
||||
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission memory translatedSubmission =
|
||||
submission;
|
||||
uint256 totalAmount = 0;
|
||||
for (uint256 i = 0; i < submission.operatorRewards.length; i++) {
|
||||
totalAmount += submission.operatorRewards[i].amount;
|
||||
for (uint256 i = 0; i < translatedSubmission.operatorRewards.length; i++) {
|
||||
translatedSubmission.operatorRewards[i].operator =
|
||||
_ethOperatorFromSolochain(translatedSubmission.operatorRewards[i].operator);
|
||||
totalAmount += translatedSubmission.operatorRewards[i].amount;
|
||||
}
|
||||
|
||||
_sortOperatorRewards(translatedSubmission.operatorRewards);
|
||||
|
||||
submission.token.safeIncreaseAllowance(address(_REWARDS_COORDINATOR), totalAmount);
|
||||
|
||||
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission[] memory submissions =
|
||||
new IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission[](1);
|
||||
submissions[0] = submission;
|
||||
submissions[0] = translatedSubmission;
|
||||
|
||||
OperatorSet memory operatorSet = OperatorSet({avs: address(this), id: VALIDATORS_SET_ID});
|
||||
_REWARDS_COORDINATOR.createOperatorDirectedOperatorSetRewardsSubmission(
|
||||
|
|
@ -357,9 +397,10 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
|
|||
SlashingRequest[] calldata slashings
|
||||
) external onlyRewardsInitiator {
|
||||
for (uint256 i = 0; i < slashings.length; i++) {
|
||||
address ethOperator = _ethOperatorFromSolochain(slashings[i].operator);
|
||||
IAllocationManagerTypes.SlashingParams memory slashingParams =
|
||||
IAllocationManagerTypes.SlashingParams({
|
||||
operator: slashings[i].operator,
|
||||
operator: ethOperator,
|
||||
operatorSetId: VALIDATORS_SET_ID,
|
||||
strategies: slashings[i].strategies,
|
||||
wadsToSlash: slashings[i].wadsToSlash,
|
||||
|
|
@ -374,6 +415,26 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
|
|||
|
||||
// ============ Internal Functions ============
|
||||
|
||||
/**
|
||||
* @notice Sorts operator rewards array by operator address in ascending order using insertion sort
|
||||
* @dev Insertion sort is optimal for small arrays (validator set capped at 32)
|
||||
* @param rewards The operator rewards array to sort in-place
|
||||
*/
|
||||
function _sortOperatorRewards(
|
||||
IRewardsCoordinatorTypes.OperatorReward[] memory rewards
|
||||
) private pure {
|
||||
uint256 len = rewards.length;
|
||||
for (uint256 i = 1; i < len; i++) {
|
||||
IRewardsCoordinatorTypes.OperatorReward memory key = rewards[i];
|
||||
uint256 j = i;
|
||||
while (j > 0 && rewards[j - 1].operator > key.operator) {
|
||||
rewards[j] = rewards[j - 1];
|
||||
j--;
|
||||
}
|
||||
rewards[j] = key;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Safely converts a 20-byte array to an address
|
||||
* @param data The bytes to convert (must be exactly 20 bytes)
|
||||
|
|
@ -382,10 +443,22 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
|
|||
function _toAddress(
|
||||
bytes memory data
|
||||
) private pure returns (address result) {
|
||||
require(data.length == 20, "Invalid address length");
|
||||
require(data.length == 20, InvalidSolochainAddressLength());
|
||||
assembly {
|
||||
result := shr(96, mload(add(data, 32)))
|
||||
}
|
||||
require(result != address(0), ZeroAddress());
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Returns the EigenLayer operator address for a Solochain validator address
|
||||
* @dev Reverts if the Solochain address has not been mapped to an operator
|
||||
*/
|
||||
function _ethOperatorFromSolochain(
|
||||
address solochainAddress
|
||||
) internal view returns (address) {
|
||||
address ethOperator = validatorSolochainAddressToEthAddress[solochainAddress];
|
||||
require(ethOperator != address(0), UnknownSolochainAddress());
|
||||
return ethOperator;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,12 @@ interface IDataHavenServiceManagerErrors {
|
|||
error ZeroAddress();
|
||||
/// @notice Thrown when the solochain address data length is not 20 bytes
|
||||
error InvalidSolochainAddressLength();
|
||||
|
||||
/// @notice Thrown when a Solochain address has not been mapped to an EigenLayer operator
|
||||
error UnknownSolochainAddress();
|
||||
|
||||
/// @notice Thrown when a Solochain address is already assigned to a different operator
|
||||
error SolochainAddressAlreadyAssigned();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -117,6 +123,15 @@ interface IDataHavenServiceManager is
|
|||
address validatorAddress
|
||||
) external view returns (address);
|
||||
|
||||
/**
|
||||
* @notice Converts a Solochain validator address to the corresponding EigenLayer operator address
|
||||
* @param solochainAddress The Solochain validator address to convert
|
||||
* @return The corresponding EigenLayer operator address
|
||||
*/
|
||||
function validatorSolochainAddressToEthAddress(
|
||||
address solochainAddress
|
||||
) external view returns (address);
|
||||
|
||||
/**
|
||||
* @notice Initializes the DataHaven Service Manager
|
||||
* @param initialOwner Address of the initial owner
|
||||
|
|
|
|||
|
|
@ -75,10 +75,18 @@
|
|||
{
|
||||
"astId": 23790,
|
||||
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
|
||||
"label": "__GAP",
|
||||
"label": "validatorSolochainAddressToEthAddress",
|
||||
"offset": 0,
|
||||
"slot": "105",
|
||||
"type": "t_array(t_uint256)46_storage"
|
||||
"type": "t_mapping(t_address,t_address)"
|
||||
},
|
||||
{
|
||||
"astId": 23795,
|
||||
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
|
||||
"label": "__GAP",
|
||||
"offset": 0,
|
||||
"slot": "106",
|
||||
"type": "t_array(t_uint256)45_storage"
|
||||
}
|
||||
],
|
||||
"types": {
|
||||
|
|
@ -87,10 +95,10 @@
|
|||
"label": "address",
|
||||
"numberOfBytes": "20"
|
||||
},
|
||||
"t_array(t_uint256)46_storage": {
|
||||
"t_array(t_uint256)45_storage": {
|
||||
"encoding": "inplace",
|
||||
"label": "uint256[46]",
|
||||
"numberOfBytes": "1472",
|
||||
"label": "uint256[45]",
|
||||
"numberOfBytes": "1440",
|
||||
"base": "t_uint256"
|
||||
},
|
||||
"t_array(t_uint256)49_storage": {
|
||||
|
|
|
|||
158
contracts/test/OperatorAddressMappings.t.sol
Normal file
158
contracts/test/OperatorAddressMappings.t.sol
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.27;
|
||||
|
||||
import {AVSDeployer} from "./utils/AVSDeployer.sol";
|
||||
import {DataHavenServiceManager} from "../src/DataHavenServiceManager.sol";
|
||||
import {
|
||||
IAllocationManagerTypes
|
||||
} from "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol";
|
||||
import {Test} from "forge-std/Test.sol";
|
||||
|
||||
contract OperatorAddressMappingsTest is AVSDeployer {
|
||||
address public snowbridgeAgent = address(uint160(uint256(keccak256("snowbridgeAgent"))));
|
||||
|
||||
address internal operator1 = address(uint160(uint256(keccak256("operator1"))));
|
||||
address internal operator2 = address(uint160(uint256(keccak256("operator2"))));
|
||||
|
||||
function setUp() public virtual {
|
||||
_deployMockEigenLayerAndAVS();
|
||||
|
||||
// Configure the rewards initiator (not strictly needed for these tests,
|
||||
// but keeps setup consistent with other suites).
|
||||
vm.prank(avsOwner);
|
||||
serviceManager.setRewardsInitiator(snowbridgeAgent);
|
||||
}
|
||||
|
||||
function _registerOperator(
|
||||
address ethOperator,
|
||||
address solochainOperator
|
||||
) internal {
|
||||
vm.prank(avsOwner);
|
||||
serviceManager.addValidatorToAllowlist(ethOperator);
|
||||
|
||||
vm.prank(ethOperator);
|
||||
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(solochainOperator)
|
||||
});
|
||||
|
||||
vm.prank(ethOperator);
|
||||
allocationManager.registerForOperatorSets(ethOperator, registerParams);
|
||||
}
|
||||
|
||||
function test_registerOperator_revertsIfSolochainAlreadyAssignedToDifferentOperator() public {
|
||||
address sharedSolochain = address(0xBEEF);
|
||||
|
||||
_registerOperator(operator1, sharedSolochain);
|
||||
|
||||
// operator2 cannot claim the same solochain address
|
||||
vm.prank(avsOwner);
|
||||
serviceManager.addValidatorToAllowlist(operator2);
|
||||
vm.prank(operator2);
|
||||
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(sharedSolochain)
|
||||
});
|
||||
|
||||
vm.prank(operator2);
|
||||
vm.expectRevert(abi.encodeWithSignature("SolochainAddressAlreadyAssigned()"));
|
||||
allocationManager.registerForOperatorSets(operator2, registerParams);
|
||||
}
|
||||
|
||||
function test_updateSolochainAddressForValidator_revertsIfAlreadyAssignedToDifferentOperator()
|
||||
public
|
||||
{
|
||||
address solo1 = address(0xBEEF);
|
||||
address solo2 = address(0xCAFE);
|
||||
|
||||
_registerOperator(operator1, solo1);
|
||||
_registerOperator(operator2, solo2);
|
||||
|
||||
// operator2 cannot update to operator1's solochain address
|
||||
vm.prank(operator2);
|
||||
vm.expectRevert(abi.encodeWithSignature("SolochainAddressAlreadyAssigned()"));
|
||||
serviceManager.updateSolochainAddressForValidator(solo1);
|
||||
}
|
||||
|
||||
function test_updateSolochainAddressForValidator_clearsOldReverseMapping() public {
|
||||
address soloOld = address(0xBEEF);
|
||||
address soloNew = address(0xCAFE);
|
||||
|
||||
_registerOperator(operator1, soloOld);
|
||||
|
||||
assertEq(
|
||||
serviceManager.validatorEthAddressToSolochainAddress(operator1),
|
||||
soloOld,
|
||||
"forward mapping should be set"
|
||||
);
|
||||
assertEq(
|
||||
serviceManager.validatorSolochainAddressToEthAddress(soloOld),
|
||||
operator1,
|
||||
"reverse mapping should be set"
|
||||
);
|
||||
|
||||
vm.prank(operator1);
|
||||
serviceManager.updateSolochainAddressForValidator(soloNew);
|
||||
|
||||
assertEq(
|
||||
serviceManager.validatorEthAddressToSolochainAddress(operator1),
|
||||
soloNew,
|
||||
"forward mapping should update"
|
||||
);
|
||||
assertEq(
|
||||
serviceManager.validatorSolochainAddressToEthAddress(soloNew),
|
||||
operator1,
|
||||
"reverse mapping should update"
|
||||
);
|
||||
assertEq(
|
||||
serviceManager.validatorSolochainAddressToEthAddress(soloOld),
|
||||
address(0),
|
||||
"old reverse mapping should be cleared"
|
||||
);
|
||||
}
|
||||
|
||||
function test_registerOperator_replacesSolochainAndClearsOldReverseMapping() public {
|
||||
address soloOld = address(0xBEEF);
|
||||
address soloNew = address(0xCAFE);
|
||||
|
||||
_registerOperator(operator1, soloOld);
|
||||
|
||||
// simulate allocationManager registering operator1 again with a new solochain address
|
||||
uint32[] memory operatorSetIds = new uint32[](1);
|
||||
operatorSetIds[0] = serviceManager.VALIDATORS_SET_ID();
|
||||
|
||||
vm.prank(address(allocationManager));
|
||||
serviceManager.registerOperator(
|
||||
operator1, address(serviceManager), operatorSetIds, abi.encodePacked(soloNew)
|
||||
);
|
||||
|
||||
assertEq(
|
||||
serviceManager.validatorEthAddressToSolochainAddress(operator1),
|
||||
soloNew,
|
||||
"forward mapping should update"
|
||||
);
|
||||
assertEq(
|
||||
serviceManager.validatorSolochainAddressToEthAddress(soloNew),
|
||||
operator1,
|
||||
"reverse mapping should update"
|
||||
);
|
||||
assertEq(
|
||||
serviceManager.validatorSolochainAddressToEthAddress(soloOld),
|
||||
address(0),
|
||||
"old reverse mapping should be cleared"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -6,8 +6,13 @@ pragma solidity ^0.8.13;
|
|||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import {
|
||||
IRewardsCoordinator,
|
||||
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 {AVSDeployer} from "./utils/AVSDeployer.sol";
|
||||
import {ERC20FixedSupply} from "./utils/ERC20FixedSupply.sol";
|
||||
|
|
@ -41,6 +46,30 @@ contract RewardsSubmitterTest is AVSDeployer {
|
|||
IERC20(address(rewardToken)).safeTransfer(address(serviceManager), 100000e18);
|
||||
}
|
||||
|
||||
function _registerOperator(
|
||||
address ethOperator,
|
||||
address solochainOperator
|
||||
) internal {
|
||||
// Allow our operator to register
|
||||
vm.prank(avsOwner);
|
||||
serviceManager.addValidatorToAllowlist(ethOperator);
|
||||
|
||||
vm.prank(ethOperator);
|
||||
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(solochainOperator)
|
||||
});
|
||||
|
||||
vm.prank(ethOperator);
|
||||
allocationManager.registerForOperatorSets(ethOperator, registerParams);
|
||||
}
|
||||
|
||||
// Helper function to build a submission
|
||||
function _buildSubmission(
|
||||
uint256 rewardAmount,
|
||||
|
|
@ -91,6 +120,7 @@ contract RewardsSubmitterTest is AVSDeployer {
|
|||
// ============ Access Control Tests ============
|
||||
|
||||
function test_submitRewards_revertsIfNotRewardsInitiator() public {
|
||||
_registerOperator(operator1, operator1);
|
||||
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission memory submission =
|
||||
_buildSubmission(1000e18, operator1);
|
||||
|
||||
|
|
@ -102,6 +132,7 @@ contract RewardsSubmitterTest is AVSDeployer {
|
|||
// ============ Success Tests ============
|
||||
|
||||
function test_submitRewards_singleOperator() public {
|
||||
_registerOperator(operator1, operator1);
|
||||
uint256 rewardAmount = 1000e18;
|
||||
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission memory submission =
|
||||
_buildSubmission(rewardAmount, operator1);
|
||||
|
|
@ -116,6 +147,9 @@ contract RewardsSubmitterTest is AVSDeployer {
|
|||
}
|
||||
|
||||
function test_submitRewards_multipleOperators() public {
|
||||
_registerOperator(operator1, operator1);
|
||||
_registerOperator(operator2, operator2);
|
||||
|
||||
// Build strategies
|
||||
IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory strategiesAndMultipliers =
|
||||
new IRewardsCoordinatorTypes.StrategyAndMultiplier[](deployedStrategies.length);
|
||||
|
|
@ -126,8 +160,8 @@ contract RewardsSubmitterTest is AVSDeployer {
|
|||
}
|
||||
|
||||
// Ensure operators are sorted in ascending order (required by EigenLayer)
|
||||
address opLow = address(0x1);
|
||||
address opHigh = address(0x2);
|
||||
(address opLow, address opHigh) =
|
||||
operator1 < operator2 ? (operator1, operator2) : (operator2, operator1);
|
||||
|
||||
uint256 amount1 = 600e18;
|
||||
uint256 amount2 = 400e18;
|
||||
|
|
@ -160,6 +194,7 @@ contract RewardsSubmitterTest is AVSDeployer {
|
|||
}
|
||||
|
||||
function test_submitRewards_multipleSubmissions() public {
|
||||
_registerOperator(operator1, operator1);
|
||||
uint32 duration = TEST_CALCULATION_INTERVAL;
|
||||
|
||||
// Submit for period 0
|
||||
|
|
@ -188,6 +223,7 @@ contract RewardsSubmitterTest is AVSDeployer {
|
|||
}
|
||||
|
||||
function test_submitRewards_withCustomDescription() public {
|
||||
_registerOperator(operator1, operator1);
|
||||
// Build submission with custom description
|
||||
IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory strategiesAndMultipliers =
|
||||
new IRewardsCoordinatorTypes.StrategyAndMultiplier[](1);
|
||||
|
|
@ -217,6 +253,7 @@ contract RewardsSubmitterTest is AVSDeployer {
|
|||
}
|
||||
|
||||
function test_submitRewards_withDifferentToken() public {
|
||||
_registerOperator(operator1, operator1);
|
||||
// Deploy a different token
|
||||
ERC20FixedSupply otherToken =
|
||||
new ERC20FixedSupply("Other", "OTHER", 1000000e18, address(this));
|
||||
|
|
@ -251,4 +288,153 @@ contract RewardsSubmitterTest is AVSDeployer {
|
|||
emit IDataHavenServiceManagerEvents.RewardsSubmitted(500e18, 1);
|
||||
serviceManager.submitRewards(submission);
|
||||
}
|
||||
|
||||
function test_submitRewards_translatesSolochainOperatorToEthOperator() public {
|
||||
address solochainOperator = address(0xBEEF);
|
||||
_registerOperator(operator1, solochainOperator);
|
||||
assertEq(
|
||||
serviceManager.validatorEthAddressToSolochainAddress(operator1),
|
||||
solochainOperator,
|
||||
"forward mapping should be set"
|
||||
);
|
||||
assertEq(
|
||||
serviceManager.validatorSolochainAddressToEthAddress(solochainOperator),
|
||||
operator1,
|
||||
"reverse mapping should be set"
|
||||
);
|
||||
|
||||
uint256 rewardAmount = 1000e18;
|
||||
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission memory submission =
|
||||
_buildSubmission(rewardAmount, solochainOperator);
|
||||
|
||||
// Warp to a time after the period ends
|
||||
vm.warp(submission.startTimestamp + submission.duration + 1);
|
||||
|
||||
IRewardsCoordinatorTypes.OperatorReward[] memory expectedOperatorRewards =
|
||||
new IRewardsCoordinatorTypes.OperatorReward[](1);
|
||||
expectedOperatorRewards[0] =
|
||||
IRewardsCoordinatorTypes.OperatorReward({operator: operator1, amount: rewardAmount});
|
||||
|
||||
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission memory expectedSubmission =
|
||||
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission({
|
||||
strategiesAndMultipliers: submission.strategiesAndMultipliers,
|
||||
token: submission.token,
|
||||
operatorRewards: expectedOperatorRewards,
|
||||
startTimestamp: submission.startTimestamp,
|
||||
duration: submission.duration,
|
||||
description: submission.description
|
||||
});
|
||||
|
||||
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission[] memory submissions =
|
||||
new IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission[](1);
|
||||
submissions[0] = expectedSubmission;
|
||||
|
||||
OperatorSet memory operatorSet =
|
||||
OperatorSet({avs: address(serviceManager), id: serviceManager.VALIDATORS_SET_ID()});
|
||||
vm.expectCall(
|
||||
address(rewardsCoordinator),
|
||||
abi.encodeCall(
|
||||
IRewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission,
|
||||
(operatorSet, submissions)
|
||||
)
|
||||
);
|
||||
|
||||
assertEq(
|
||||
submission.operatorRewards[0].operator,
|
||||
solochainOperator,
|
||||
"submission should use solochain operator"
|
||||
);
|
||||
vm.prank(snowbridgeAgent);
|
||||
serviceManager.submitRewards(submission);
|
||||
}
|
||||
|
||||
function test_submitRewards_revertsIfUnknownSolochainAddress() public {
|
||||
address unknownSolochainOperator = address(0xDEAD);
|
||||
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission memory submission =
|
||||
_buildSubmission(1000e18, unknownSolochainOperator);
|
||||
|
||||
vm.prank(snowbridgeAgent);
|
||||
vm.expectRevert(abi.encodeWithSignature("UnknownSolochainAddress()"));
|
||||
serviceManager.submitRewards(submission);
|
||||
}
|
||||
|
||||
function test_submitRewards_sortsTranslatedOperatorsByAddress() public {
|
||||
(address ethLow, address ethHigh) =
|
||||
operator1 < operator2 ? (operator1, operator2) : (operator2, operator1);
|
||||
|
||||
address solochainLow = address(0x1000);
|
||||
address solochainHigh = address(0x2000);
|
||||
|
||||
_registerOperator(ethLow, solochainHigh);
|
||||
_registerOperator(ethHigh, solochainLow);
|
||||
|
||||
IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory strategiesAndMultipliers =
|
||||
new IRewardsCoordinatorTypes.StrategyAndMultiplier[](deployedStrategies.length);
|
||||
for (uint256 i = 0; i < deployedStrategies.length; i++) {
|
||||
strategiesAndMultipliers[i] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
|
||||
strategy: deployedStrategies[i], multiplier: uint96((i + 1) * 1e18)
|
||||
});
|
||||
}
|
||||
|
||||
uint256 amountForEthLow = 600e18;
|
||||
uint256 amountForEthHigh = 400e18;
|
||||
uint256 totalAmount = amountForEthLow + amountForEthHigh;
|
||||
|
||||
IRewardsCoordinatorTypes.OperatorReward[] memory inputOperatorRewards =
|
||||
new IRewardsCoordinatorTypes.OperatorReward[](2);
|
||||
inputOperatorRewards[0] = IRewardsCoordinatorTypes.OperatorReward({
|
||||
operator: solochainLow, amount: amountForEthHigh
|
||||
});
|
||||
inputOperatorRewards[1] = IRewardsCoordinatorTypes.OperatorReward({
|
||||
operator: solochainHigh, amount: amountForEthLow
|
||||
});
|
||||
|
||||
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission memory submission =
|
||||
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission({
|
||||
strategiesAndMultipliers: strategiesAndMultipliers,
|
||||
token: IERC20(address(rewardToken)),
|
||||
operatorRewards: inputOperatorRewards,
|
||||
startTimestamp: GENESIS_REWARDS_TIMESTAMP,
|
||||
duration: TEST_CALCULATION_INTERVAL,
|
||||
description: "DataHaven rewards"
|
||||
});
|
||||
|
||||
vm.warp(submission.startTimestamp + submission.duration + 1);
|
||||
|
||||
IRewardsCoordinatorTypes.OperatorReward[] memory expectedOperatorRewards =
|
||||
new IRewardsCoordinatorTypes.OperatorReward[](2);
|
||||
expectedOperatorRewards[0] =
|
||||
IRewardsCoordinatorTypes.OperatorReward({operator: ethLow, amount: amountForEthLow});
|
||||
expectedOperatorRewards[1] =
|
||||
IRewardsCoordinatorTypes.OperatorReward({operator: ethHigh, amount: amountForEthHigh});
|
||||
|
||||
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission memory expectedSubmission =
|
||||
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission({
|
||||
strategiesAndMultipliers: strategiesAndMultipliers,
|
||||
token: submission.token,
|
||||
operatorRewards: expectedOperatorRewards,
|
||||
startTimestamp: submission.startTimestamp,
|
||||
duration: submission.duration,
|
||||
description: submission.description
|
||||
});
|
||||
|
||||
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission[] memory submissions =
|
||||
new IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission[](1);
|
||||
submissions[0] = expectedSubmission;
|
||||
|
||||
OperatorSet memory operatorSet =
|
||||
OperatorSet({avs: address(serviceManager), id: serviceManager.VALIDATORS_SET_ID()});
|
||||
vm.expectCall(
|
||||
address(rewardsCoordinator),
|
||||
abi.encodeCall(
|
||||
IRewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission,
|
||||
(operatorSet, submissions)
|
||||
)
|
||||
);
|
||||
|
||||
vm.prank(snowbridgeAgent);
|
||||
vm.expectEmit(false, false, false, true);
|
||||
emit IDataHavenServiceManagerEvents.RewardsSubmitted(totalAmount, 2);
|
||||
serviceManager.submitRewards(submission);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ contract SlashingTest is AVSDeployer {
|
|||
}
|
||||
|
||||
function test_fulfilSlashingRequest() public {
|
||||
address solochainOperator = address(0xBEEF);
|
||||
|
||||
// Allow our operator to register
|
||||
vm.prank(avsOwner);
|
||||
serviceManager.addValidatorToAllowlist(operator);
|
||||
|
|
@ -43,7 +45,7 @@ contract SlashingTest is AVSDeployer {
|
|||
IAllocationManagerTypes.RegisterParams({
|
||||
avs: address(serviceManager),
|
||||
operatorSetIds: operatorSetIds,
|
||||
data: abi.encodePacked(address(operator))
|
||||
data: abi.encodePacked(solochainOperator)
|
||||
});
|
||||
|
||||
vm.prank(operator);
|
||||
|
|
@ -61,7 +63,7 @@ contract SlashingTest is AVSDeployer {
|
|||
IStrategy[] memory strategies = allocationManager.getStrategiesInOperatorSet(operatorSet);
|
||||
|
||||
slashings[0] = IDataHavenServiceManager.SlashingRequest(
|
||||
operator, strategies, wadsToSlash, "Testing slashing"
|
||||
solochainOperator, strategies, wadsToSlash, "Testing slashing"
|
||||
);
|
||||
|
||||
console.log(block.number);
|
||||
|
|
@ -83,6 +85,8 @@ contract SlashingTest is AVSDeployer {
|
|||
}
|
||||
|
||||
function test_fulfilSlashingRequestForOnlyOneStrategy() public {
|
||||
address solochainOperator = address(0xBEEF);
|
||||
|
||||
// Allow our operator to register
|
||||
vm.prank(avsOwner);
|
||||
serviceManager.addValidatorToAllowlist(operator);
|
||||
|
|
@ -100,7 +104,7 @@ contract SlashingTest is AVSDeployer {
|
|||
IAllocationManagerTypes.RegisterParams({
|
||||
avs: address(serviceManager),
|
||||
operatorSetIds: operatorSetIds,
|
||||
data: abi.encodePacked(address(operator))
|
||||
data: abi.encodePacked(solochainOperator)
|
||||
});
|
||||
|
||||
vm.prank(operator);
|
||||
|
|
@ -119,7 +123,7 @@ contract SlashingTest is AVSDeployer {
|
|||
strategiesToSlash[0] = strategies[0];
|
||||
|
||||
slashings[0] = IDataHavenServiceManager.SlashingRequest(
|
||||
operator, strategiesToSlash, wadsToSlash, "Testing slashing"
|
||||
solochainOperator, strategiesToSlash, wadsToSlash, "Testing slashing"
|
||||
);
|
||||
|
||||
console.log(block.number);
|
||||
|
|
@ -139,4 +143,24 @@ contract SlashingTest is AVSDeployer {
|
|||
emit IDataHavenServiceManagerEvents.SlashingComplete();
|
||||
serviceManager.slashValidatorsOperator(slashings);
|
||||
}
|
||||
|
||||
function test_fulfilSlashingRequest_revertsIfUnknownSolochainAddress() public {
|
||||
// Configure the rewards initiator (because only the reward agent can submit slashing request)
|
||||
vm.prank(avsOwner);
|
||||
serviceManager.setRewardsInitiator(snowbridgeAgent);
|
||||
|
||||
address unknownSolochainOperator = address(0xDEAD);
|
||||
DataHavenServiceManager.SlashingRequest[] memory slashings =
|
||||
new DataHavenServiceManager.SlashingRequest[](1);
|
||||
slashings[0] = IDataHavenServiceManager.SlashingRequest(
|
||||
unknownSolochainOperator,
|
||||
new IStrategy[](0),
|
||||
new uint256[](0),
|
||||
"Testing unknown solochain operator"
|
||||
);
|
||||
|
||||
vm.prank(snowbridgeAgent);
|
||||
vm.expectRevert(abi.encodeWithSignature("UnknownSolochainAddress()"));
|
||||
serviceManager.slashValidatorsOperator(slashings);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,9 +85,12 @@ contract StorageLayoutTest is AVSDeployer {
|
|||
bool inAllowlistBefore = serviceManager.validatorsAllowlist(testValidator);
|
||||
address solochainAddressBefore =
|
||||
serviceManager.validatorEthAddressToSolochainAddress(testValidator);
|
||||
address ethOperatorBefore =
|
||||
serviceManager.validatorSolochainAddressToEthAddress(testSolochainAddress);
|
||||
|
||||
// Verify the mapping was set correctly before upgrade
|
||||
assertEq(solochainAddressBefore, testSolochainAddress, "Solochain address should be set");
|
||||
assertEq(ethOperatorBefore, testValidator, "Eth operator should be set");
|
||||
|
||||
// Deploy new implementation and upgrade
|
||||
DataHavenServiceManager newImpl =
|
||||
|
|
@ -112,6 +115,16 @@ contract StorageLayoutTest is AVSDeployer {
|
|||
testSolochainAddress,
|
||||
"validatorEthAddressToSolochainAddress should have correct value after upgrade"
|
||||
);
|
||||
assertEq(
|
||||
serviceManager.validatorSolochainAddressToEthAddress(testSolochainAddress),
|
||||
ethOperatorBefore,
|
||||
"validatorSolochainAddressToEthAddress mapping should be preserved after upgrade"
|
||||
);
|
||||
assertEq(
|
||||
serviceManager.validatorSolochainAddressToEthAddress(testSolochainAddress),
|
||||
testValidator,
|
||||
"validatorSolochainAddressToEthAddress should have correct value after upgrade"
|
||||
);
|
||||
}
|
||||
|
||||
/// @notice Verifies multiple validators in allowlist are preserved
|
||||
|
|
|
|||
|
|
@ -2317,6 +2317,13 @@ export const dataHavenServiceManagerAbi = [
|
|||
outputs: [{ name: '', internalType: 'address', type: 'address' }],
|
||||
stateMutability: 'view',
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
inputs: [{ name: '', internalType: 'address', type: 'address' }],
|
||||
name: 'validatorSolochainAddressToEthAddress',
|
||||
outputs: [{ name: '', internalType: 'address', type: 'address' }],
|
||||
stateMutability: 'view',
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
inputs: [{ name: '', internalType: 'address', type: 'address' }],
|
||||
|
|
@ -2504,6 +2511,8 @@ export const dataHavenServiceManagerAbi = [
|
|||
{ type: 'error', inputs: [], name: 'OnlyAllocationManager' },
|
||||
{ type: 'error', inputs: [], name: 'OnlyRewardsInitiator' },
|
||||
{ type: 'error', inputs: [], name: 'OperatorNotInAllowlist' },
|
||||
{ type: 'error', inputs: [], name: 'SolochainAddressAlreadyAssigned' },
|
||||
{ type: 'error', inputs: [], name: 'UnknownSolochainAddress' },
|
||||
{ type: 'error', inputs: [], name: 'ZeroAddress' },
|
||||
] as const
|
||||
|
||||
|
|
@ -10705,6 +10714,15 @@ export const readDataHavenServiceManagerValidatorEthAddressToSolochainAddress =
|
|||
functionName: 'validatorEthAddressToSolochainAddress',
|
||||
})
|
||||
|
||||
/**
|
||||
* Wraps __{@link readContract}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `functionName` set to `"validatorSolochainAddressToEthAddress"`
|
||||
*/
|
||||
export const readDataHavenServiceManagerValidatorSolochainAddressToEthAddress =
|
||||
/*#__PURE__*/ createReadContract({
|
||||
abi: dataHavenServiceManagerAbi,
|
||||
functionName: 'validatorSolochainAddressToEthAddress',
|
||||
})
|
||||
|
||||
/**
|
||||
* Wraps __{@link readContract}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `functionName` set to `"validatorsAllowlist"`
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ async function formatStateDiff(): Promise<void> {
|
|||
|
||||
// Use a higher max size (3MB) to handle the large state-diff.json file
|
||||
const result =
|
||||
await $`bun run biome format --files-max-size=3000000 --write ${STATE_DIFF_PATH}`.quiet();
|
||||
await $`bun run biome format --files-max-size=4000000 --write ${STATE_DIFF_PATH}`.quiet();
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
logger.warn("⚠️ Biome formatting had issues, but continuing...");
|
||||
|
|
|
|||
Loading…
Reference in a new issue