datahaven/contracts/test/OperatorAddressMappings.t.sol

159 lines
5.7 KiB
Solidity
Raw Permalink Normal View History

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>
2026-02-18 19:38:13 +00:00
// 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"
);
}
}