From 2cc8c1b70b23632f456e689021fdd45dc5922464 Mon Sep 17 00:00:00 2001 From: steven Date: Mon, 24 Mar 2025 16:44:23 -0400 Subject: [PATCH 1/4] feat: example self slasher avs --- src/examples/SelfSlasher/SelfSlasher.sol | 90 ++++++ .../SelfSlasher/interfaces/ISelfSlasher.sol | 20 ++ test/unit/SelfSlasher.t.sol | 272 ++++++++++++++++++ 3 files changed, 382 insertions(+) create mode 100644 src/examples/SelfSlasher/SelfSlasher.sol create mode 100644 src/examples/SelfSlasher/interfaces/ISelfSlasher.sol create mode 100644 test/unit/SelfSlasher.t.sol diff --git a/src/examples/SelfSlasher/SelfSlasher.sol b/src/examples/SelfSlasher/SelfSlasher.sol new file mode 100644 index 00000000..7ddc927b --- /dev/null +++ b/src/examples/SelfSlasher/SelfSlasher.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import {SlasherBase} from "../../slashers/base/SlasherBase.sol"; +import { + IAllocationManager, + IAllocationManagerTypes +} from "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; +import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; +import {ISlashingRegistryCoordinator} from "../../interfaces/ISlashingRegistryCoordinator.sol"; +import {ISelfSlasher} from "./interfaces/ISelfSlasher.sol"; +import {OperatorSet} from "eigenlayer-contracts/src/contracts/libraries/OperatorSetLib.sol"; + +/// @title SelfSlasher +/// @notice Example contract allowing operators to slash themselves for testing purposes +/// @dev Based on SlasherBase, provides a simplified way to test slashing effects +contract SelfSlasher is ISelfSlasher, SlasherBase { + /// @notice Error thrown when wad to slash is invalid + error InvalidWadToSlash(); + + /// @notice Error thrown when no strategies are found in the operator set + error NoStrategiesInOperatorSet(); + + /// @notice Constructs the SelfSlasher contract + /// @param _allocationManager The EigenLayer allocation manager contract + /// @param _registryCoordinator The registry coordinator for this middleware + /// @param _slasher The address of the slasher (admin) + constructor( + IAllocationManager _allocationManager, + ISlashingRegistryCoordinator _registryCoordinator, + address _slasher + ) SlasherBase(_allocationManager, _registryCoordinator, _slasher) {} + + /// @inheritdoc ISelfSlasher + function selfSlash( + uint32 operatorSetId, + uint256 wadToSlash, + string calldata description + ) external override { + // Validate wad to slash + if (wadToSlash == 0 || wadToSlash > 1e18) { + revert InvalidWadToSlash(); + } + + // Get the operator set + OperatorSet memory operatorSet = + OperatorSet(slashingRegistryCoordinator.avs(), operatorSetId); + + // Get all strategies from the operator set + IStrategy[] memory strategies = allocationManager.getStrategiesInOperatorSet(operatorSet); + + // Ensure there are strategies in the operator set + if (strategies.length == 0) { + revert NoStrategiesInOperatorSet(); + } + + // Create wadsToSlash array with the same value for all strategies + uint256[] memory wadsToSlash = new uint256[](strategies.length); + for (uint256 i = 0; i < strategies.length; i++) { + wadsToSlash[i] = wadToSlash; + } + + // Create slashing parameters + IAllocationManagerTypes.SlashingParams memory params = IAllocationManagerTypes + .SlashingParams({ + operator: msg.sender, + operatorSetId: operatorSetId, + strategies: strategies, + wadsToSlash: wadsToSlash, + description: description + }); + + // Execute slashing request + uint256 requestId = nextRequestId++; + _fulfillSlashingRequest(requestId, params); + + // Update operator status in registry + _updateOperatorStatus(msg.sender); + } + + /// @notice Updates the operator status in the registry after slashing + /// @param operator The address of the operator to update + function _updateOperatorStatus( + address operator + ) internal { + address[] memory operators = new address[](1); + operators[0] = operator; + slashingRegistryCoordinator.updateOperators(operators); + } +} diff --git a/src/examples/SelfSlasher/interfaces/ISelfSlasher.sol b/src/examples/SelfSlasher/interfaces/ISelfSlasher.sol new file mode 100644 index 00000000..bd1c231b --- /dev/null +++ b/src/examples/SelfSlasher/interfaces/ISelfSlasher.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; +import {ISlasher} from "../../../interfaces/ISlasher.sol"; + +/// @title ISelfSlasher +/// @notice Interface for a self-slashing contract that allows operators to slash their own stake +/// @dev This is an example implementation for testing slashing functionality +interface ISelfSlasher is ISlasher { + /// @notice Allows an operator to slash their own stake + /// @param operatorSetId The ID of the operator set in which to slash the operator + /// @param wadToSlash The proportion to slash from each strategy (between 0 and 1e18) + /// @param description A description of why the operator is slashing themselves + function selfSlash( + uint32 operatorSetId, + uint256 wadToSlash, + string calldata description + ) external; +} diff --git a/test/unit/SelfSlasher.t.sol b/test/unit/SelfSlasher.t.sol new file mode 100644 index 00000000..2be27f9f --- /dev/null +++ b/test/unit/SelfSlasher.t.sol @@ -0,0 +1,272 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import {Test, console2 as console} from "forge-std/Test.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {SelfSlasher} from "../../src/examples/SelfSlasher/SelfSlasher.sol"; +import { + IAllocationManager, + IAllocationManagerTypes +} from "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; +import {IAVSRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IAVSRegistrar.sol"; +import {IAVSDirectory} from "eigenlayer-contracts/src/contracts/interfaces/IAVSDirectory.sol"; +import {IRegistryCoordinator} from "../../src/interfaces/IRegistryCoordinator.sol"; +import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; +import {IPermissionController} from + "eigenlayer-contracts/src/contracts/interfaces/IPermissionController.sol"; +import {ISlasher, ISlasherTypes, ISlasherErrors} from "../../src/interfaces/ISlasher.sol"; +import {ISlashingRegistryCoordinator} from "../../src/interfaces/ISlashingRegistryCoordinator.sol"; +import {IStakeRegistry, IStakeRegistryTypes} from "../../src/interfaces/IStakeRegistry.sol"; +import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +import {TransparentUpgradeableProxy} from + "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {EmptyContract} from "eigenlayer-contracts/src/test/mocks/EmptyContract.sol"; +import {AllocationManager} from "eigenlayer-contracts/src/contracts/core/AllocationManager.sol"; +import {PermissionController} from + "eigenlayer-contracts/src/contracts/permissions/PermissionController.sol"; +import {PauserRegistry} from "eigenlayer-contracts/src/contracts/permissions/PauserRegistry.sol"; +import {IPauserRegistry} from "eigenlayer-contracts/src/contracts/interfaces/IPauserRegistry.sol"; +import {IDelegationManager} from + "eigenlayer-contracts/src/contracts/interfaces/IDelegationManager.sol"; +import {IStrategyManager} from "eigenlayer-contracts/src/contracts/interfaces/IStrategyManager.sol"; +import {DelegationMock} from "../mocks/DelegationMock.sol"; +import {SlashingRegistryCoordinator} from "../../src/SlashingRegistryCoordinator.sol"; +import {ISlashingRegistryCoordinatorTypes} from + "../../src/interfaces/ISlashingRegistryCoordinator.sol"; +import {IBLSApkRegistry, IBLSApkRegistryTypes} from "../../src/interfaces/IBLSApkRegistry.sol"; +import {IIndexRegistry} from "../../src/interfaces/IIndexRegistry.sol"; +import {ISocketRegistry} from "../../src/interfaces/ISocketRegistry.sol"; +import {CoreDeployLib} from "../utils/CoreDeployLib.sol"; +import { + OperatorWalletLib, + Operator, + Wallet, + BLSWallet, + SigningKeyOperationsLib +} from "../utils/OperatorWalletLib.sol"; +import {OperatorSet} from "eigenlayer-contracts/src/contracts/libraries/OperatorSetLib.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ERC20Mock} from "@openzeppelin/contracts/mocks/ERC20Mock.sol"; +import {StrategyFactory} from "eigenlayer-contracts/src/contracts/strategies/StrategyFactory.sol"; +import {StakeRegistry} from "../../src/StakeRegistry.sol"; +import {BLSApkRegistry} from "../../src/BLSApkRegistry.sol"; +import {IndexRegistry} from "../../src/IndexRegistry.sol"; +import {SocketRegistry} from "../../src/SocketRegistry.sol"; +import {MiddlewareDeployLib} from "../utils/MiddlewareDeployLib.sol"; + +contract SelfSlasherTest is Test { + SelfSlasher public selfSlasher; + ProxyAdmin public proxyAdmin; + EmptyContract public emptyContract; + SlashingRegistryCoordinator public slashingRegistryCoordinator; + CoreDeployLib.DeploymentData public coreDeployment; + PauserRegistry public pauserRegistry; + ERC20Mock public mockToken; + StrategyFactory public strategyFactory; + StakeRegistry public stakeRegistry; + BLSApkRegistry public blsApkRegistry; + IndexRegistry public indexRegistry; + SocketRegistry public socketRegistry; + + address public slasher; + address public serviceManager; + Operator public operatorWallet; + IStrategy public mockStrategy; + address public proxyAdminOwner = address(uint160(uint256(keccak256("proxyAdminOwner")))); + address public pauser = address(uint160(uint256(keccak256("pauser")))); + address public unpauser = address(uint160(uint256(keccak256("unpauser")))); + address public churnApprover = address(uint160(uint256(keccak256("churnApprover")))); + address public ejector = address(uint160(uint256(keccak256("ejector")))); + + uint32 constant DEALLOCATION_DELAY = 7 days; + uint32 constant ALLOCATION_CONFIGURATION_DELAY = 1 days; + + function setUp() public { + serviceManager = address(0x2); + slasher = address(0x3); + operatorWallet = OperatorWalletLib.createOperator("operator"); + + mockToken = new ERC20Mock(); + + vm.startPrank(proxyAdminOwner); + proxyAdmin = new ProxyAdmin(); + emptyContract = new EmptyContract(); + + address[] memory pausers = new address[](1); + pausers[0] = pauser; + pauserRegistry = new PauserRegistry(pausers, unpauser); + + CoreDeployLib.DeploymentConfigData memory configData; + configData.strategyManager.initialOwner = proxyAdminOwner; + configData.strategyManager.initialStrategyWhitelister = proxyAdminOwner; + configData.strategyManager.initPausedStatus = 0; + + configData.delegationManager.initialOwner = proxyAdminOwner; + configData.delegationManager.minWithdrawalDelayBlocks = 50400; + configData.delegationManager.initPausedStatus = 0; + + configData.eigenPodManager.initialOwner = proxyAdminOwner; + configData.eigenPodManager.initPausedStatus = 0; + + configData.allocationManager.initialOwner = proxyAdminOwner; + configData.allocationManager.deallocationDelay = DEALLOCATION_DELAY; + configData.allocationManager.allocationConfigurationDelay = ALLOCATION_CONFIGURATION_DELAY; + configData.allocationManager.initPausedStatus = 0; + + configData.strategyFactory.initialOwner = proxyAdminOwner; + configData.strategyFactory.initPausedStatus = 0; + + configData.avsDirectory.initialOwner = proxyAdminOwner; + configData.avsDirectory.initPausedStatus = 0; + + configData.rewardsCoordinator.initialOwner = proxyAdminOwner; + configData.rewardsCoordinator.rewardsUpdater = address(0x123); + configData.rewardsCoordinator.initPausedStatus = 0; + configData.rewardsCoordinator.activationDelay = 0; + configData.rewardsCoordinator.defaultSplitBips = 1000; + configData.rewardsCoordinator.calculationIntervalSeconds = 86400; + configData.rewardsCoordinator.maxRewardsDuration = 864000; + configData.rewardsCoordinator.maxRetroactiveLength = 86400; + configData.rewardsCoordinator.maxFutureLength = 86400; + configData.rewardsCoordinator.genesisRewardsTimestamp = 1672531200; + + configData.ethPOSDeposit.ethPOSDepositAddress = address(0x123); + + coreDeployment = CoreDeployLib.deployContracts(address(proxyAdmin), configData); + // Deploy AllocationManager + AllocationManager implAllocationManager = new AllocationManager( + IDelegationManager(coreDeployment.delegationManager), + pauserRegistry, + IPermissionController(coreDeployment.permissionController), + DEALLOCATION_DELAY, + ALLOCATION_CONFIGURATION_DELAY, + "1.0.0" + ); + + bytes memory initData = abi.encodeWithSelector( + AllocationManager.initialize.selector, + proxyAdminOwner, + 0 // initialPausedStatus + ); + + TransparentUpgradeableProxy transparentProxyAllocationManager = new TransparentUpgradeableProxy( + address(implAllocationManager), address(proxyAdmin), initData + ); + + AllocationManager allocationManager = + AllocationManager(address(transparentProxyAllocationManager)); + + // Deploy EigenLayer middleware deployment (registries, coordinator) + MiddlewareDeployLib.MiddlewareDeployConfig memory middlewareConfig; + middlewareConfig.instantSlasher.initialOwner = proxyAdminOwner; + middlewareConfig.instantSlasher.slasher = slasher; + middlewareConfig.slashingRegistryCoordinator.initialOwner = proxyAdminOwner; + middlewareConfig.slashingRegistryCoordinator.churnApprover = churnApprover; + middlewareConfig.slashingRegistryCoordinator.ejector = ejector; + middlewareConfig.slashingRegistryCoordinator.initPausedStatus = 0; + middlewareConfig.slashingRegistryCoordinator.serviceManager = serviceManager; + middlewareConfig.socketRegistry.initialOwner = proxyAdminOwner; + middlewareConfig.indexRegistry.initialOwner = proxyAdminOwner; + middlewareConfig.stakeRegistry.initialOwner = proxyAdminOwner; + middlewareConfig.stakeRegistry.minimumStake = 1 ether; + middlewareConfig.stakeRegistry.strategyParams = 0; + middlewareConfig.stakeRegistry.delegationManager = coreDeployment.delegationManager; + middlewareConfig.stakeRegistry.avsDirectory = coreDeployment.avsDirectory; + + { + IStakeRegistryTypes.StrategyParams[] memory stratParams = + new IStakeRegistryTypes.StrategyParams[](1); + stratParams[0] = IStakeRegistryTypes.StrategyParams({ + strategy: IStrategy(address(mockToken)), + multiplier: 1 ether + }); + middlewareConfig.stakeRegistry.strategyParamsArray = stratParams; + } + + middlewareConfig.stakeRegistry.lookAheadPeriod = 0; + middlewareConfig.stakeRegistry.stakeType = IStakeRegistryTypes.StakeType(1); // TOTAL_SLASHABLE + middlewareConfig.blsApkRegistry.initialOwner = proxyAdminOwner; + + MiddlewareDeployLib.MiddlewareDeployData memory middlewareData = MiddlewareDeployLib + .deployMiddleware( + address(proxyAdmin), + coreDeployment.allocationManager, + address(pauserRegistry), + middlewareConfig + ); + + slashingRegistryCoordinator = + SlashingRegistryCoordinator(payable(middlewareData.slashingRegistryCoordinator)); + stakeRegistry = StakeRegistry(middlewareData.stakeRegistry); + blsApkRegistry = BLSApkRegistry(middlewareData.blsApkRegistry); + indexRegistry = IndexRegistry(middlewareData.indexRegistry); + socketRegistry = SocketRegistry(middlewareData.socketRegistry); + + // Deploy SelfSlasher + selfSlasher = new SelfSlasher( + IAllocationManager(address(allocationManager)), + ISlashingRegistryCoordinator(slashingRegistryCoordinator), + slasher + ); + + vm.stopPrank(); + } + + function testSelfSlashReverts_WhenInvalidWad() public { + // Test with 0 wad + vm.expectRevert(SelfSlasher.InvalidWadToSlash.selector); + selfSlasher.selfSlash(1, 0, "test slash"); + + // Test with wad > 1e18 + vm.expectRevert(SelfSlasher.InvalidWadToSlash.selector); + selfSlasher.selfSlash(1, 1e18 + 1, "test slash"); + } + + function testSelfSlashReverts_WhenNoStrategiesInOperatorSet() public { + // Mock getStrategiesInOperatorSet to return empty array + vm.mockCall( + address(selfSlasher.allocationManager()), + abi.encodeWithSelector(IAllocationManager.getStrategiesInOperatorSet.selector), + abi.encode(new IStrategy[](0)) + ); + + vm.expectRevert(SelfSlasher.NoStrategiesInOperatorSet.selector); + selfSlasher.selfSlash(1, 1e18, "test slash"); + } + + function testSelfSlashSuccessful() public { + // Mock the AllocationManager getStrategiesInOperatorSet to return strategies + IStrategy[] memory strategies = new IStrategy[](1); + strategies[0] = IStrategy(address(0x10)); + + vm.mockCall( + address(selfSlasher.allocationManager()), + abi.encodeWithSelector(IAllocationManager.getStrategiesInOperatorSet.selector), + abi.encode(strategies) + ); + + // Mock the slashOperator call + vm.mockCall( + address(selfSlasher.allocationManager()), + abi.encodeWithSelector(IAllocationManager.slashOperator.selector), + abi.encode() + ); + + // Mock the updateOperators call + vm.mockCall( + address(selfSlasher.slashingRegistryCoordinator()), + abi.encodeWithSelector(ISlashingRegistryCoordinator.updateOperators.selector), + abi.encode() + ); + + uint256 requestIdBefore = selfSlasher.nextRequestId(); + + // Test slashing as operator + vm.prank(operatorWallet.key.addr); + selfSlasher.selfSlash(1, 1e18, "test slash"); + + assertEq( + selfSlasher.nextRequestId(), requestIdBefore + 1, "Request ID should be incremented" + ); + } +} From 67de91985f118546f75bafa7f10d8ac75c9117de Mon Sep 17 00:00:00 2001 From: steven Date: Mon, 24 Mar 2025 16:45:32 -0400 Subject: [PATCH 2/4] feat: self slasher --- src/examples/SelfSlasher/SelfSlasher.sol | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/examples/SelfSlasher/SelfSlasher.sol b/src/examples/SelfSlasher/SelfSlasher.sol index 7ddc927b..233c3c47 100644 --- a/src/examples/SelfSlasher/SelfSlasher.sol +++ b/src/examples/SelfSlasher/SelfSlasher.sol @@ -36,20 +36,16 @@ contract SelfSlasher is ISelfSlasher, SlasherBase { uint32 operatorSetId, uint256 wadToSlash, string calldata description - ) external override { - // Validate wad to slash + ) external { if (wadToSlash == 0 || wadToSlash > 1e18) { revert InvalidWadToSlash(); } - // Get the operator set OperatorSet memory operatorSet = OperatorSet(slashingRegistryCoordinator.avs(), operatorSetId); - // Get all strategies from the operator set IStrategy[] memory strategies = allocationManager.getStrategiesInOperatorSet(operatorSet); - // Ensure there are strategies in the operator set if (strategies.length == 0) { revert NoStrategiesInOperatorSet(); } @@ -70,7 +66,6 @@ contract SelfSlasher is ISelfSlasher, SlasherBase { description: description }); - // Execute slashing request uint256 requestId = nextRequestId++; _fulfillSlashingRequest(requestId, params); From 2c414dcc7166f6ab5594f2ca69b9e004e4e49e73 Mon Sep 17 00:00:00 2001 From: steven Date: Mon, 24 Mar 2025 17:05:02 -0400 Subject: [PATCH 3/4] test: more realistic test setup --- src/examples/SelfSlasher/SelfSlasher.sol | 7 + test/unit/SelfSlasher.t.sol | 405 +++++++++++++++++++---- 2 files changed, 348 insertions(+), 64 deletions(-) diff --git a/src/examples/SelfSlasher/SelfSlasher.sol b/src/examples/SelfSlasher/SelfSlasher.sol index 233c3c47..1c4b6f3b 100644 --- a/src/examples/SelfSlasher/SelfSlasher.sol +++ b/src/examples/SelfSlasher/SelfSlasher.sol @@ -21,6 +21,9 @@ contract SelfSlasher is ISelfSlasher, SlasherBase { /// @notice Error thrown when no strategies are found in the operator set error NoStrategiesInOperatorSet(); + /// @notice Error thrown when operator is not slashable in this operator set + error OperatorNotSlashable(); + /// @notice Constructs the SelfSlasher contract /// @param _allocationManager The EigenLayer allocation manager contract /// @param _registryCoordinator The registry coordinator for this middleware @@ -44,6 +47,10 @@ contract SelfSlasher is ISelfSlasher, SlasherBase { OperatorSet memory operatorSet = OperatorSet(slashingRegistryCoordinator.avs(), operatorSetId); + if (!allocationManager.isOperatorSlashable(msg.sender, operatorSet)) { + revert OperatorNotSlashable(); + } + IStrategy[] memory strategies = allocationManager.getStrategiesInOperatorSet(operatorSet); if (strategies.length == 0) { diff --git a/test/unit/SelfSlasher.t.sol b/test/unit/SelfSlasher.t.sol index 2be27f9f..e315066f 100644 --- a/test/unit/SelfSlasher.t.sol +++ b/test/unit/SelfSlasher.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.27; -import {Test, console2 as console} from "forge-std/Test.sol"; +import {Test, Vm, console2 as console} from "forge-std/Test.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {SelfSlasher} from "../../src/examples/SelfSlasher/SelfSlasher.sol"; import { @@ -44,7 +44,7 @@ import { BLSWallet, SigningKeyOperationsLib } from "../utils/OperatorWalletLib.sol"; -import {OperatorSet} from "eigenlayer-contracts/src/contracts/libraries/OperatorSetLib.sol"; +import {OperatorSet} from "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {ERC20Mock} from "@openzeppelin/contracts/mocks/ERC20Mock.sol"; import {StrategyFactory} from "eigenlayer-contracts/src/contracts/strategies/StrategyFactory.sol"; @@ -81,6 +81,14 @@ contract SelfSlasherTest is Test { uint32 constant DEALLOCATION_DELAY = 7 days; uint32 constant ALLOCATION_CONFIGURATION_DELAY = 1 days; + event OperatorSlashed( + uint256 indexed slashingRequestId, + address indexed operator, + uint32 operatorSetId, + uint256[] slashingAmounts, + string description + ); + function setUp() public { serviceManager = address(0x2); slasher = address(0x3); @@ -133,30 +141,24 @@ contract SelfSlasherTest is Test { configData.ethPOSDeposit.ethPOSDepositAddress = address(0x123); coreDeployment = CoreDeployLib.deployContracts(address(proxyAdmin), configData); - // Deploy AllocationManager - AllocationManager implAllocationManager = new AllocationManager( - IDelegationManager(coreDeployment.delegationManager), - pauserRegistry, - IPermissionController(coreDeployment.permissionController), - DEALLOCATION_DELAY, - ALLOCATION_CONFIGURATION_DELAY, - "1.0.0" - ); - bytes memory initData = abi.encodeWithSelector( - AllocationManager.initialize.selector, - proxyAdminOwner, - 0 // initialPausedStatus - ); + address strategyManagerOwner = Ownable(coreDeployment.strategyManager).owner(); + vm.stopPrank(); - TransparentUpgradeableProxy transparentProxyAllocationManager = new TransparentUpgradeableProxy( - address(implAllocationManager), address(proxyAdmin), initData + vm.startPrank(strategyManagerOwner); + IStrategyManager(coreDeployment.strategyManager).setStrategyWhitelister( + coreDeployment.strategyFactory ); + vm.stopPrank(); - AllocationManager allocationManager = - AllocationManager(address(transparentProxyAllocationManager)); + vm.startPrank(proxyAdminOwner); + mockStrategy = IStrategy( + StrategyFactory(coreDeployment.strategyFactory).deployNewStrategy( + IERC20(address(mockToken)) + ) + ); + vm.stopPrank(); - // Deploy EigenLayer middleware deployment (registries, coordinator) MiddlewareDeployLib.MiddlewareDeployConfig memory middlewareConfig; middlewareConfig.instantSlasher.initialOwner = proxyAdminOwner; middlewareConfig.instantSlasher.slasher = slasher; @@ -172,101 +174,376 @@ contract SelfSlasherTest is Test { middlewareConfig.stakeRegistry.strategyParams = 0; middlewareConfig.stakeRegistry.delegationManager = coreDeployment.delegationManager; middlewareConfig.stakeRegistry.avsDirectory = coreDeployment.avsDirectory; - + middlewareConfig.instantSlasher.slasher = slasher; { IStakeRegistryTypes.StrategyParams[] memory stratParams = new IStakeRegistryTypes.StrategyParams[](1); - stratParams[0] = IStakeRegistryTypes.StrategyParams({ - strategy: IStrategy(address(mockToken)), - multiplier: 1 ether - }); + stratParams[0] = + IStakeRegistryTypes.StrategyParams({strategy: mockStrategy, multiplier: 1 ether}); middlewareConfig.stakeRegistry.strategyParamsArray = stratParams; } - middlewareConfig.stakeRegistry.lookAheadPeriod = 0; - middlewareConfig.stakeRegistry.stakeType = IStakeRegistryTypes.StakeType(1); // TOTAL_SLASHABLE + middlewareConfig.stakeRegistry.stakeType = IStakeRegistryTypes.StakeType(1); middlewareConfig.blsApkRegistry.initialOwner = proxyAdminOwner; - MiddlewareDeployLib.MiddlewareDeployData memory middlewareData = MiddlewareDeployLib + vm.startPrank(proxyAdminOwner); + MiddlewareDeployLib.MiddlewareDeployData memory middlewareDeployments = MiddlewareDeployLib .deployMiddleware( address(proxyAdmin), coreDeployment.allocationManager, address(pauserRegistry), middlewareConfig ); + vm.stopPrank(); + + // Give serviceManager permissions + vm.startPrank(proxyAdminOwner); + PermissionController(coreDeployment.permissionController).setAppointee( + proxyAdminOwner, + address(serviceManager), + address(coreDeployment.permissionController), + PermissionController.setAppointee.selector + ); + vm.stopPrank(); + + vm.startPrank(serviceManager); slashingRegistryCoordinator = - SlashingRegistryCoordinator(payable(middlewareData.slashingRegistryCoordinator)); - stakeRegistry = StakeRegistry(middlewareData.stakeRegistry); - blsApkRegistry = BLSApkRegistry(middlewareData.blsApkRegistry); - indexRegistry = IndexRegistry(middlewareData.indexRegistry); - socketRegistry = SocketRegistry(middlewareData.socketRegistry); + SlashingRegistryCoordinator(payable(middlewareDeployments.slashingRegistryCoordinator)); + stakeRegistry = StakeRegistry(middlewareDeployments.stakeRegistry); + blsApkRegistry = BLSApkRegistry(middlewareDeployments.blsApkRegistry); + indexRegistry = IndexRegistry(middlewareDeployments.indexRegistry); + socketRegistry = SocketRegistry(middlewareDeployments.socketRegistry); + + // Setup permissions + PermissionController(coreDeployment.permissionController).setAppointee( + address(serviceManager), + address(slashingRegistryCoordinator), + coreDeployment.allocationManager, + AllocationManager.createOperatorSets.selector + ); + + PermissionController(coreDeployment.permissionController).setAppointee( + address(serviceManager), + address(slashingRegistryCoordinator), + coreDeployment.allocationManager, + AllocationManager.deregisterFromOperatorSets.selector + ); + + PermissionController(coreDeployment.permissionController).setAppointee( + address(serviceManager), + proxyAdminOwner, + coreDeployment.allocationManager, + AllocationManager.updateAVSMetadataURI.selector + ); + vm.stopPrank(); + + // Setup quorum + vm.startPrank(proxyAdminOwner); + IAllocationManager(coreDeployment.allocationManager).updateAVSMetadataURI( + serviceManager, "fake-avs-metadata" + ); + + slashingRegistryCoordinator.createSlashableStakeQuorum( + ISlashingRegistryCoordinatorTypes.OperatorSetParam({ + maxOperatorCount: 10, + kickBIPsOfOperatorStake: 0, + kickBIPsOfTotalStake: 0 + }), + 1 ether, + _getStrategyParams(), + 0 + ); // Deploy SelfSlasher selfSlasher = new SelfSlasher( - IAllocationManager(address(allocationManager)), + IAllocationManager(coreDeployment.allocationManager), ISlashingRegistryCoordinator(slashingRegistryCoordinator), slasher ); + vm.stopPrank(); + // Now grant selfSlasher permissions from serviceManager + vm.startPrank(serviceManager); + PermissionController(coreDeployment.permissionController).setAppointee( + address(serviceManager), + address(selfSlasher), + coreDeployment.allocationManager, + AllocationManager.slashOperator.selector + ); vm.stopPrank(); } - function testSelfSlashReverts_WhenInvalidWad() public { - // Test with 0 wad + function test_Initialization() public { + assertEq(selfSlasher.slasher(), slasher, "Slasher address not set correctly"); + assertEq( + address(selfSlasher.allocationManager()), + address(coreDeployment.allocationManager), + "AllocationManager address not set correctly" + ); + assertEq( + address(selfSlasher.slashingRegistryCoordinator()), + address(slashingRegistryCoordinator), + "Registry coordinator address not set correctly" + ); + assertEq(selfSlasher.nextRequestId(), 0, "Initial request ID should be 0"); + } + + function test_SelfSlash_RevertIfInvalidWadToSlash() public { + _setupOperatorForSlashing(); + + vm.prank(operatorWallet.key.addr); vm.expectRevert(SelfSlasher.InvalidWadToSlash.selector); - selfSlasher.selfSlash(1, 0, "test slash"); + selfSlasher.selfSlash(0, 0, "zero slash"); - // Test with wad > 1e18 + vm.prank(operatorWallet.key.addr); vm.expectRevert(SelfSlasher.InvalidWadToSlash.selector); - selfSlasher.selfSlash(1, 1e18 + 1, "test slash"); + selfSlasher.selfSlash(0, 1.1e18, "over 100% slash"); + } + + function test_SelfSlash_RevertIfNotRegistered() public { + address nonOperator = makeAddr("nonOperator"); + + vm.prank(nonOperator); + vm.expectRevert(SelfSlasher.OperatorNotSlashable.selector); + selfSlasher.selfSlash(0, 0.5e18, "not registered"); } - function testSelfSlashReverts_WhenNoStrategiesInOperatorSet() public { - // Mock getStrategiesInOperatorSet to return empty array + function test_SelfSlash_RevertIfNoStrategies() public { + // Setup an operator set with no strategies (this is done in the mockCall) vm.mockCall( - address(selfSlasher.allocationManager()), + address(coreDeployment.allocationManager), abi.encodeWithSelector(IAllocationManager.getStrategiesInOperatorSet.selector), abi.encode(new IStrategy[](0)) ); + _setupOperatorForSlashing(); + + vm.prank(operatorWallet.key.addr); vm.expectRevert(SelfSlasher.NoStrategiesInOperatorSet.selector); - selfSlasher.selfSlash(1, 1e18, "test slash"); + selfSlasher.selfSlash(0, 0.5e18, "no strategies"); + + // Clear the mock to not affect other tests + vm.clearMockedCalls(); } - function testSelfSlashSuccessful() public { - // Mock the AllocationManager getStrategiesInOperatorSet to return strategies - IStrategy[] memory strategies = new IStrategy[](1); - strategies[0] = IStrategy(address(0x10)); + function test_SelfSlash_PartialSlashUpdatesStake() public { + bytes32 operatorId = _setupOperatorForSlashing(); + uint96 initialStake = stakeRegistry.weightOfOperatorForQuorum(0, operatorWallet.key.addr); + uint96 initialTotalStake = stakeRegistry.getCurrentTotalStake(0); - vm.mockCall( - address(selfSlasher.allocationManager()), - abi.encodeWithSelector(IAllocationManager.getStrategiesInOperatorSet.selector), - abi.encode(strategies) + vm.prank(operatorWallet.key.addr); + selfSlasher.selfSlash(0, 0.5e18, "partial slash"); + + uint96 newStake = stakeRegistry.weightOfOperatorForQuorum(0, operatorWallet.key.addr); + uint96 newTotalStake = stakeRegistry.getCurrentTotalStake(0); + + assertEq(newStake, initialStake / 2, "Stake not reduced by 50%"); + assertEq( + newTotalStake, + initialTotalStake - (initialStake / 2), + "Total stake not reduced by operator's 50%" ); - // Mock the slashOperator call - vm.mockCall( - address(selfSlasher.allocationManager()), - abi.encodeWithSelector(IAllocationManager.slashOperator.selector), - abi.encode() + // Verify the operator is still registered + ISlashingRegistryCoordinatorTypes.OperatorStatus status = + slashingRegistryCoordinator.getOperatorStatus(operatorWallet.key.addr); + assertEq( + uint256(status), + uint256(ISlashingRegistryCoordinatorTypes.OperatorStatus.REGISTERED), + "Operator should remain registered after partial slash" ); - // Mock the updateOperators call - vm.mockCall( - address(selfSlasher.slashingRegistryCoordinator()), - abi.encodeWithSelector(ISlashingRegistryCoordinator.updateOperators.selector), - abi.encode() + // Verify the quorum bitmap still includes this operator for quorum 0 + uint192 bitmap = slashingRegistryCoordinator.getCurrentQuorumBitmap(operatorId); + assertTrue(bitmap & 1 != 0, "Operator should still be in quorum 0 after partial slash"); + } + + function test_SelfSlash_FullSlashRemovesOperator() public { + bytes32 operatorId = _setupOperatorForSlashing(); + uint96 initialStake = stakeRegistry.weightOfOperatorForQuorum(0, operatorWallet.key.addr); + uint96 initialTotalStake = stakeRegistry.getCurrentTotalStake(0); + + vm.prank(operatorWallet.key.addr); + selfSlasher.selfSlash(0, 1e18, "full slash"); + + uint96 newStake = stakeRegistry.weightOfOperatorForQuorum(0, operatorWallet.key.addr); + uint96 newTotalStake = stakeRegistry.getCurrentTotalStake(0); + + assertEq(newStake, 0, "Stake should be reduced to 0"); + assertEq( + newTotalStake, + initialTotalStake - initialStake, + "Total stake should be reduced by operator's full stake" ); + // Verify the operator is deregistered + ISlashingRegistryCoordinatorTypes.OperatorStatus status = + slashingRegistryCoordinator.getOperatorStatus(operatorWallet.key.addr); + assertEq( + uint256(status), + uint256(ISlashingRegistryCoordinatorTypes.OperatorStatus.DEREGISTERED), + "Operator should be deregistered after full slash" + ); + + // Verify the quorum bitmap no longer includes this operator for quorum 0 + uint192 bitmap = slashingRegistryCoordinator.getCurrentQuorumBitmap(operatorId); + assertTrue(bitmap & 1 == 0, "Operator should be removed from quorum 0 after full slash"); + } + + function test_SelfSlash_EmitsEvent() public { + _setupOperatorForSlashing(); + + vm.recordLogs(); + + vm.prank(operatorWallet.key.addr); + selfSlasher.selfSlash(0, 0.1e18, "test slash"); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + bool eventFound = false; + + for (uint256 i = 0; i < entries.length; i++) { + // Check for the slashing event from our contract + if ( + entries[i].topics[0] + == keccak256("OperatorSlashed(uint256,address,uint32,uint256[],string)") + ) { + eventFound = true; + break; + } + } + + assertTrue(eventFound, "OperatorSlashed event not emitted"); + } + + function test_SelfSlash_IncreasesRequestId() public { + _setupOperatorForSlashing(); uint256 requestIdBefore = selfSlasher.nextRequestId(); - // Test slashing as operator vm.prank(operatorWallet.key.addr); - selfSlasher.selfSlash(1, 1e18, "test slash"); + selfSlasher.selfSlash(0, 0.5e18, "test slash"); assertEq( selfSlasher.nextRequestId(), requestIdBefore + 1, "Request ID should be incremented" ); } + + function test_SelfSlash_MinimumAmount() public { + _setupOperatorForSlashing(); + uint96 initialStake = stakeRegistry.weightOfOperatorForQuorum(0, operatorWallet.key.addr); + + // Test with minimum possible value (1 wei of 1e18) + vm.prank(operatorWallet.key.addr); + selfSlasher.selfSlash(0, 1, "minimum slash"); + + uint96 newStake = stakeRegistry.weightOfOperatorForQuorum(0, operatorWallet.key.addr); + // Should subtract a very small amount (initial * 1/1e18) + assertTrue(newStake < initialStake, "Stake should be reduced by minimum amount"); + assertTrue(initialStake - newStake <= 3, "Reduction should be minimal"); + } + + // ----------------- + // Helper functions + // ----------------- + + function _setupOperatorForSlashing() internal returns (bytes32) { + vm.startPrank(operatorWallet.key.addr); + IDelegationManager(coreDeployment.delegationManager).registerAsOperator( + address(0), 1, "metadata" + ); + + uint256 depositAmount = 2 ether; + mockToken.mint(operatorWallet.key.addr, depositAmount); + mockToken.approve(address(coreDeployment.strategyManager), depositAmount); + IStrategyManager(coreDeployment.strategyManager).depositIntoStrategy( + mockStrategy, mockToken, depositAmount + ); + + uint32 minDelay = 1; + IAllocationManager(coreDeployment.allocationManager).setAllocationDelay( + operatorWallet.key.addr, minDelay + ); + vm.stopPrank(); + + vm.roll(block.number + ALLOCATION_CONFIGURATION_DELAY + 1); + + IStrategy[] memory allocStrategies = new IStrategy[](1); + allocStrategies[0] = mockStrategy; + + uint64[] memory magnitudes = new uint64[](1); + magnitudes[0] = uint64(1 ether); // Allocate full magnitude (2 ETH) + + OperatorSet memory operatorSet = OperatorSet({avs: address(serviceManager), id: 0}); + + vm.startPrank(serviceManager); + IAllocationManagerTypes.CreateSetParams[] memory createParams = + new IAllocationManagerTypes.CreateSetParams[](1); + createParams[0] = + IAllocationManagerTypes.CreateSetParams({operatorSetId: 0, strategies: allocStrategies}); + IAllocationManager(coreDeployment.allocationManager).setAVSRegistrar( + address(serviceManager), IAVSRegistrar(address(slashingRegistryCoordinator)) + ); + vm.stopPrank(); + + vm.startPrank(operatorWallet.key.addr); + + IAllocationManagerTypes.AllocateParams[] memory allocParams = + new IAllocationManagerTypes.AllocateParams[](1); + allocParams[0] = IAllocationManagerTypes.AllocateParams({ + operatorSet: operatorSet, + strategies: allocStrategies, + newMagnitudes: magnitudes + }); + + IAllocationManager(coreDeployment.allocationManager).modifyAllocations( + operatorWallet.key.addr, allocParams + ); + vm.roll(block.number + 100); + + uint32[] memory operatorSetIds = new uint32[](1); + operatorSetIds[0] = 0; + bytes32 messageHash = slashingRegistryCoordinator.calculatePubkeyRegistrationMessageHash( + operatorWallet.key.addr + ); + IBLSApkRegistryTypes.PubkeyRegistrationParams memory pubkeyParams = IBLSApkRegistryTypes + .PubkeyRegistrationParams({ + pubkeyRegistrationSignature: SigningKeyOperationsLib.sign( + operatorWallet.signingKey, messageHash + ), + pubkeyG1: operatorWallet.signingKey.publicKeyG1, + pubkeyG2: operatorWallet.signingKey.publicKeyG2 + }); + + bytes memory registrationData = abi.encode( + ISlashingRegistryCoordinatorTypes.RegistrationType.NORMAL, "socket", pubkeyParams + ); + + IAllocationManagerTypes.RegisterParams memory registerParams = IAllocationManagerTypes + .RegisterParams({ + avs: address(serviceManager), + operatorSetIds: operatorSetIds, + data: registrationData + }); + IAllocationManager(coreDeployment.allocationManager).registerForOperatorSets( + operatorWallet.key.addr, registerParams + ); + vm.stopPrank(); + + vm.roll(block.number + 100); + + bytes32 operatorId = slashingRegistryCoordinator.getOperatorId(operatorWallet.key.addr); + return operatorId; + } + + function _getStrategyParams() + internal + view + returns (IStakeRegistryTypes.StrategyParams[] memory) + { + IStakeRegistryTypes.StrategyParams[] memory stratParams = + new IStakeRegistryTypes.StrategyParams[](1); + stratParams[0] = + IStakeRegistryTypes.StrategyParams({strategy: mockStrategy, multiplier: 1 ether}); + return stratParams; + } } From c29698d13957dcc78c6d78a6d5a5f0855b35f63e Mon Sep 17 00:00:00 2001 From: steven Date: Mon, 24 Mar 2025 17:08:09 -0400 Subject: [PATCH 4/4] test: simplify test cases and setup --- test/unit/SelfSlasher.t.sol | 56 ------------------------------------- 1 file changed, 56 deletions(-) diff --git a/test/unit/SelfSlasher.t.sol b/test/unit/SelfSlasher.t.sol index e315066f..2113ad6f 100644 --- a/test/unit/SelfSlasher.t.sol +++ b/test/unit/SelfSlasher.t.sol @@ -309,24 +309,6 @@ contract SelfSlasherTest is Test { selfSlasher.selfSlash(0, 0.5e18, "not registered"); } - function test_SelfSlash_RevertIfNoStrategies() public { - // Setup an operator set with no strategies (this is done in the mockCall) - vm.mockCall( - address(coreDeployment.allocationManager), - abi.encodeWithSelector(IAllocationManager.getStrategiesInOperatorSet.selector), - abi.encode(new IStrategy[](0)) - ); - - _setupOperatorForSlashing(); - - vm.prank(operatorWallet.key.addr); - vm.expectRevert(SelfSlasher.NoStrategiesInOperatorSet.selector); - selfSlasher.selfSlash(0, 0.5e18, "no strategies"); - - // Clear the mock to not affect other tests - vm.clearMockedCalls(); - } - function test_SelfSlash_PartialSlashUpdatesStake() public { bytes32 operatorId = _setupOperatorForSlashing(); uint96 initialStake = stakeRegistry.weightOfOperatorForQuorum(0, operatorWallet.key.addr); @@ -391,43 +373,6 @@ contract SelfSlasherTest is Test { assertTrue(bitmap & 1 == 0, "Operator should be removed from quorum 0 after full slash"); } - function test_SelfSlash_EmitsEvent() public { - _setupOperatorForSlashing(); - - vm.recordLogs(); - - vm.prank(operatorWallet.key.addr); - selfSlasher.selfSlash(0, 0.1e18, "test slash"); - - Vm.Log[] memory entries = vm.getRecordedLogs(); - bool eventFound = false; - - for (uint256 i = 0; i < entries.length; i++) { - // Check for the slashing event from our contract - if ( - entries[i].topics[0] - == keccak256("OperatorSlashed(uint256,address,uint32,uint256[],string)") - ) { - eventFound = true; - break; - } - } - - assertTrue(eventFound, "OperatorSlashed event not emitted"); - } - - function test_SelfSlash_IncreasesRequestId() public { - _setupOperatorForSlashing(); - uint256 requestIdBefore = selfSlasher.nextRequestId(); - - vm.prank(operatorWallet.key.addr); - selfSlasher.selfSlash(0, 0.5e18, "test slash"); - - assertEq( - selfSlasher.nextRequestId(), requestIdBefore + 1, "Request ID should be incremented" - ); - } - function test_SelfSlash_MinimumAmount() public { _setupOperatorForSlashing(); uint96 initialStake = stakeRegistry.weightOfOperatorForQuorum(0, operatorWallet.key.addr); @@ -439,7 +384,6 @@ contract SelfSlasherTest is Test { uint96 newStake = stakeRegistry.weightOfOperatorForQuorum(0, operatorWallet.key.addr); // Should subtract a very small amount (initial * 1/1e18) assertTrue(newStake < initialStake, "Stake should be reduced by minimum amount"); - assertTrue(initialStake - newStake <= 3, "Reduction should be minimal"); } // -----------------