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:
Gonza Montiel 2026-02-18 16:38:13 -03:00 committed by GitHub
parent b45009f62d
commit ddbc9bdd8b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 1012 additions and 570 deletions

View file

@ -1 +1 @@
711490494719593c219c35ca496cd28b86d9f54a
17cfdd8c5a2d3396260b62742e51a0b8985def63

File diff suppressed because one or more lines are too long

View file

@ -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(

View file

@ -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;
}
}

View file

@ -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

View file

@ -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": {

View 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"
);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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

View file

@ -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"`
*/

View file

@ -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...");