feat: initial slasher implementation (#10)

This PR:
- Sets up the slasher infrastructure with the base functionality
required (in `ISlasher.sol`, `SlasherBase.sol` and
`SlasherBaseStorage.sol`) and adds the tests for it (in
`SlasherBase.t.sol`).
- Adds an implementation of a more complex slasher (in
`IVetoableSlasher.sol` and `VetoableSlasher.sol`) and tests for it (in
`VetoableSlasher.t.sol`).
- Modifies the `ServiceManagerBase` contract to use the new
`VetoableSlasher` contract to manage slashing.
- Updates mocks and tests to reflect the newly added functionality.

---------

Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
This commit is contained in:
Tobi Demeco 2025-03-27 12:20:15 -03:00 committed by GitHub
parent e412500e61
commit d8d792874c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 1099 additions and 7 deletions

View file

@ -106,4 +106,10 @@ interface IServiceManager is IServiceManagerUI, IServiceManagerErrors, IServiceM
address operator,
uint32[] memory operatorSetIds
) external;
/**
* @notice Returns the address of the AVS
* @return The address of the AVS
*/
function avs() external view returns (address);
}

View file

@ -0,0 +1,39 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import {IAllocationManager} from
"eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol";
interface ISlasherErrors {
/// @notice Thrown when a caller without slasher privileges attempts a restricted operation
error OnlySlasher();
}
interface ISlasherTypes {
/// @notice Structure containing details about a slashing request
struct SlashingRequest {
IAllocationManager.SlashingParams params;
uint256 requestTimestamp;
}
}
interface ISlasherEvents is ISlasherTypes {
/// @notice Emitted when an operator is successfully slashed
event OperatorSlashed(
uint256 indexed slashingRequestId,
address indexed operator,
uint32 indexed operatorSetId,
uint256[] wadsToSlash,
string description
);
}
/// @title ISlasher
/// @notice Base interface containing shared functionality for all slasher implementations
interface ISlasher is ISlasherErrors, ISlasherEvents {
/// @notice Returns the address authorized to create and fulfil slashing requests
function slasher() external view returns (address);
/// @notice Returns the next slashing request ID
function nextRequestId() external view returns (uint256);
}

View file

@ -0,0 +1,86 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import {IAllocationManager} from
"eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol";
import {ISlasher} from "./ISlasher.sol";
interface IVetoableSlasherErrors {
/// @notice Thrown when a caller without veto committee privileges attempts a restricted operation
error OnlyVetoCommittee();
/// @notice Thrown when attempting to veto a slashing request after the veto period has expired
error VetoPeriodPassed();
/// @notice Thrown when attempting to execute a slashing request before the veto period has ended
error VetoPeriodNotPassed();
/// @notice Thrown when attempting to interact with a slashing request that has been cancelled
error SlashingRequestIsCancelled();
/// @notice Thrown when attempting to modify a slashing request that does not exist
error SlashingRequestNotRequested();
}
interface IVetoableSlasherTypes {
/// @notice Structure containing details about a vetoable slashing request
struct VetoableSlashingRequest {
IAllocationManager.SlashingParams params;
uint256 requestBlock;
bool isPending;
}
}
interface IVetoableSlasherEvents {
/// @notice Emitted when a new slashing request is created
event SlashingRequested(
uint256 indexed requestId,
address indexed operator,
uint32 operatorSetId,
uint256[] wadsToSlash,
string description
);
/// @notice Emitted when a slashing request is cancelled by the veto committee
event SlashingRequestCancelled(
address indexed operator, uint32 operatorSetId, uint256[] wadsToSlash, string description
);
/// @notice Emitted when a slashing request is fulfilled
event SlashingRequestFulfilled(
address indexed operator, uint32 operatorSetId, uint256[] wadsToSlash, string description
);
}
/// @title IVetoableSlasher
/// @notice A slashing contract that implements a veto mechanism allowing a designated committee to cancel slashing requests
/// @dev Extends base interfaces and adds a veto period during which slashing requests can be cancelled
interface IVetoableSlasher is
ISlasher,
IVetoableSlasherErrors,
IVetoableSlasherTypes,
IVetoableSlasherEvents
{
/// @notice Duration of the veto period during which the veto committee can cancel slashing requests
function vetoWindowBlocks() external view returns (uint32);
/// @notice Address of the committee that has veto power over slashing requests
function vetoCommittee() external view returns (address);
/// @notice Queues a new slashing request
/// @param params Parameters defining the slashing request including operator and amount
/// @dev Can only be called by the authorized slasher
function queueSlashingRequest(
IAllocationManager.SlashingParams calldata params
) external;
/// @notice Cancels a pending slashing request
/// @param requestId The ID of the slashing request to cancel
/// @dev Can only be called by the veto committee during the veto period
function cancelSlashingRequest(
uint256 requestId
) external;
/// @notice Executes a slashing request after the veto period has passed
/// @param requestId The ID of the slashing request to fulfil
/// @dev Can only be called by the authorized slasher after the veto period
function fulfilSlashingRequest(
uint256 requestId
) external;
}

View file

@ -21,7 +21,7 @@ import {IPermissionController} from
"eigenlayer-contracts/src/contracts/interfaces/IPermissionController.sol";
import {IServiceManager, IServiceManagerUI} from "../interfaces/IServiceManager.sol";
import {IVetoableSlasher} from "../interfaces/IVetoableSlasher.sol";
import {ServiceManagerBaseStorage} from "./ServiceManagerBaseStorage.sol";
/**
@ -56,6 +56,17 @@ abstract contract ServiceManagerBase is ServiceManagerBaseStorage, IAVSRegistrar
_setRewardsInitiator(_rewardsInitiator);
}
/**
* @notice Sets the slasher contract
* @param slasher The slasher contract address
* @dev Only callable by the owner
*/
function setSlasher(
IVetoableSlasher slasher
) external virtual onlyOwner {
_slasher = slasher;
}
/**
* @notice Updates the metadata URI for the AVS
* @param _metadataURI is the metadata URI for the AVS
@ -97,12 +108,27 @@ abstract contract ServiceManagerBase is ServiceManagerBaseStorage, IAVSRegistrar
}
/**
* Forwards the call to the AllocationManager.slashOperator() function
* Queue a slashing request in the vetoable slasher
* @param params Parameters defining the slashing request
* @dev Can only be called by the owner
*/
function slashOperator(
function queueSlashingRequest(
IAllocationManager.SlashingParams calldata params
) external virtual onlyOwner {
_allocationManager.slashOperator(address(this), params);
require(address(_slasher) != address(0), "Slasher not set");
_slasher.queueSlashingRequest(params);
}
/**
* fulfils a slashing request that has passed the veto period
* @param requestId The ID of the slashing request to fulfil
* @dev Can be called by anyone
*/
function fulfilSlashingRequest(
uint256 requestId
) external virtual {
require(address(_slasher) != address(0), "Slasher not set");
_slasher.fulfilSlashingRequest(requestId);
}
/**
@ -161,9 +187,9 @@ abstract contract ServiceManagerBase is ServiceManagerBaseStorage, IAVSRegistrar
/// @inheritdoc IAVSRegistrar
function supportsAVS(
address avs
address avsAddress
) external view virtual override returns (bool) {
return avs == address(this);
return avsAddress == this.avs();
}
/// @inheritdoc IAVSRegistrar
@ -232,6 +258,11 @@ abstract contract ServiceManagerBase is ServiceManagerBaseStorage, IAVSRegistrar
});
}
/// @inheritdoc IServiceManager
function avs() external view virtual returns (address) {
return address(this);
}
/**
* @notice Forwards a call to Eigenlayer's RewardsCoordinator contract to set the address of the entity that can call `processClaim` on behalf of this contract.
* @param claimer The address of the entity that can call `processClaim` on behalf of the earner

View file

@ -12,6 +12,7 @@ import {IAllocationManager} from
import {IPermissionController} from
"eigenlayer-contracts/src/contracts/interfaces/IPermissionController.sol";
import {IVetoableSlasher} from "../interfaces/IVetoableSlasher.sol";
import {IServiceManager} from "../interfaces/IServiceManager.sol";
/**
@ -35,10 +36,13 @@ abstract contract ServiceManagerBaseStorage is IServiceManager, OwnableUpgradeab
*
*/
/// @notice The slasher contract that handles operator slashing
IVetoableSlasher internal _slasher;
/// @notice The address of the entity that can initiate rewards
address public rewardsInitiator;
/// @notice Sets the (immutable) `_avsDirectory`, `_rewardsCoordinator`, `_registryCoordinator`, `_stakeRegistry`, and `_allocationManager` addresses
/// @notice Sets the (immutable) rewardsCoordinator`, `_permissionController`, and `_allocationManager` addresses
constructor(
IRewardsCoordinator __rewardsCoordinator,
IPermissionController __permissionController,

View file

@ -0,0 +1,61 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import {SlasherStorage, IServiceManager} from "./SlasherBaseStorage.sol";
import {
IAllocationManagerTypes,
IAllocationManager
} from "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol";
import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol";
/// @title SlasherBase
/// @notice Base contract for implementing slashing functionality in an EigenLayer AVS
/// @dev Provides core slashing functionality and interfaces with EigenLayer's AllocationManager
abstract contract SlasherBase is SlasherStorage {
/// @notice Ensures only the authorized slasher can call certain functions
modifier onlySlasher() {
_checkSlasher(msg.sender);
_;
}
/// @notice Constructs the base slasher contract
/// @param _allocationManager The EigenLayer allocation manager contract
/// @param _serviceManager The service manager that will manage this slasher
constructor(
IAllocationManager _allocationManager,
IServiceManager _serviceManager
) SlasherStorage(_allocationManager, _serviceManager) {}
/// @notice Internal function to execute a slashing request
/// @param _requestId The ID of the slashing request to fulfil
/// @param _params Parameters defining the slashing request including operator, strategies, and amounts
/// @dev Calls AllocationManager.slashOperator to perform the actual slashing
function _fulfilSlashingRequest(
uint256 _requestId,
IAllocationManager.SlashingParams memory _params
) internal virtual {
allocationManager.slashOperator({avs: serviceManager.avs(), params: _params});
emit OperatorSlashed(
_requestId,
_params.operator,
_params.operatorSetId,
_params.wadsToSlash,
_params.description
);
}
/// @notice Internal function to verify if an account is the authorized slasher
/// @param account The address to check
/// @dev Reverts if the account is not the ServiceManager
function _checkSlasher(
address account
) internal view virtual {
require(account == address(serviceManager), OnlySlasher());
}
/// @notice Returns the address of the ServiceManager
/// @return The address of the ServiceManager
function slasher() external view returns (address) {
return address(serviceManager);
}
}

View file

@ -0,0 +1,32 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import {IAllocationManager} from
"eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol";
import {ISlasher} from "../interfaces/ISlasher.sol";
import {IServiceManager} from "../interfaces/IServiceManager.sol";
/// @title SlasherStorage
/// @notice Base storage contract for slashing functionality
/// @dev Provides storage variables and events for slashing operations
abstract contract SlasherStorage is ISlasher {
/**
*
* CONSTANTS AND IMMUTABLES
*
*/
/// @notice the AllocationManager that tracks OperatorSets and Slashing in EigenLayer
IAllocationManager public immutable allocationManager;
/// @notice the ServiceManager of the AVS
IServiceManager public immutable serviceManager;
uint256 public nextRequestId;
constructor(IAllocationManager _allocationManager, IServiceManager _serviceManager) {
allocationManager = _allocationManager;
serviceManager = _serviceManager;
}
uint256[49] private __gap;
}

View file

@ -0,0 +1,128 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol";
import {IServiceManager} from "../interfaces/IServiceManager.sol";
import {IAllocationManager} from
"eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol";
import {SlasherBase} from "./SlasherBase.sol";
import {IVetoableSlasher, IVetoableSlasherTypes} from "../interfaces/IVetoableSlasher.sol";
/// @title VetoableSlasher
/// @notice A slashing contract that implements a veto mechanism allowing a designated committee to cancel slashing requests
/// @dev Extends SlasherBase and adds a veto period during which slashing requests can be cancelled
contract VetoableSlasher is IVetoableSlasher, SlasherBase {
/// @inheritdoc IVetoableSlasher
uint32 public immutable override vetoWindowBlocks;
/// @inheritdoc IVetoableSlasher
address public immutable override vetoCommittee;
/// @notice Mapping of request IDs to their corresponding slashing request details
mapping(uint256 => IVetoableSlasherTypes.VetoableSlashingRequest) public slashingRequests;
/// @notice Modifier to restrict function access to only the veto committee
modifier onlyVetoCommittee() {
_checkVetoCommittee(msg.sender);
_;
}
constructor(
IAllocationManager _allocationManager,
IServiceManager _serviceManager,
address _vetoCommittee,
uint32 _vetoWindowBlocks
) SlasherBase(_allocationManager, _serviceManager) {
vetoWindowBlocks = _vetoWindowBlocks;
vetoCommittee = _vetoCommittee;
}
/// @inheritdoc IVetoableSlasher
function queueSlashingRequest(
IAllocationManager.SlashingParams calldata params
) external override onlySlasher {
_queueSlashingRequest(params);
}
/// @inheritdoc IVetoableSlasher
function cancelSlashingRequest(
uint256 requestId
) external override onlyVetoCommittee {
_cancelSlashingRequest(requestId);
}
/// @inheritdoc IVetoableSlasher
function fulfilSlashingRequest(
uint256 requestId
) external override {
_fulfilSlashingRequestAndMarkAsCompleted(requestId);
}
/// @notice Internal function to create and store a new slashing request
/// @param params Parameters defining the slashing request
function _queueSlashingRequest(
IAllocationManager.SlashingParams memory params
) internal virtual {
uint256 requestId = nextRequestId++;
slashingRequests[requestId] = IVetoableSlasherTypes.VetoableSlashingRequest({
params: params,
requestBlock: block.number,
isPending: true
});
emit SlashingRequested(
requestId, params.operator, params.operatorSetId, params.wadsToSlash, params.description
);
}
/// @notice Internal function to mark a slashing request as cancelled
/// @param requestId The ID of the slashing request to cancel
function _cancelSlashingRequest(
uint256 requestId
) internal virtual {
IVetoableSlasherTypes.VetoableSlashingRequest storage request = slashingRequests[requestId];
require(block.number < request.requestBlock + vetoWindowBlocks, VetoPeriodPassed());
require(request.isPending, SlashingRequestNotRequested());
emit SlashingRequestCancelled(
request.params.operator,
request.params.operatorSetId,
request.params.wadsToSlash,
request.params.description
);
delete slashingRequests[requestId];
}
/// @notice Internal function to fullfill a slashing request and mark it as completed
/// @param requestId The ID of the slashing request to fulfil
function _fulfilSlashingRequestAndMarkAsCompleted(
uint256 requestId
) internal virtual {
IVetoableSlasherTypes.VetoableSlashingRequest storage request = slashingRequests[requestId];
require(block.number >= request.requestBlock + vetoWindowBlocks, VetoPeriodNotPassed());
require(request.isPending, SlashingRequestIsCancelled());
request.isPending = false;
_fulfilSlashingRequest(requestId, request.params);
emit SlashingRequestFulfilled(
request.params.operator,
request.params.operatorSetId,
request.params.wadsToSlash,
request.params.description
);
delete slashingRequests[requestId];
}
/// @notice Internal function to verify if an account is the veto committee
/// @param account The address to check
/// @dev Reverts if the account is not the veto committee
function _checkVetoCommittee(
address account
) internal view virtual {
require(account == vetoCommittee, OnlyVetoCommittee());
}
}

View file

@ -61,4 +61,8 @@ contract ServiceManagerBaseTest is MockAVSDeployer {
new IAllocationManager.CreateSetParams[](0);
ServiceManagerBase(address(serviceManager)).createOperatorSets(emptyParams);
}
function test_returnsAVSAddress() public view {
assertEq(serviceManager.avs(), address(serviceManager));
}
}

View file

@ -0,0 +1,234 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.27;
import {Test, console} from "forge-std/Test.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol";
import {IRewardsCoordinator} from
"eigenlayer-contracts/src/contracts/interfaces/IRewardsCoordinator.sol";
import {
IAllocationManagerErrors,
IAllocationManager,
IAllocationManagerTypes
} from "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol";
import {MockAVSDeployer} from "./utils/MockAVSDeployer.sol";
import {IServiceManager} from "../src/interfaces/IServiceManager.sol";
import {ISlasher, ISlasherErrors, ISlasherEvents} from "../src/interfaces/ISlasher.sol";
import {SlasherBase} from "../src/middleware/SlasherBase.sol";
import {SlasherMock} from "./mocks/SlasherBaseMock.sol";
contract SlasherBaseTest is MockAVSDeployer {
SlasherMock public slasherContract;
address public nonServiceManagerRole;
function setUp() public virtual {
_deployMockEigenLayerAndAVS();
_setUpDefaultStrategiesAndMultipliers();
// Set up roles for testing
nonServiceManagerRole = address(0x5678);
// Deploy the SlasherMock contract, to specifically test the SlasherBase contract
slasherContract = new SlasherMock(allocationManager, serviceManager);
}
// Test constructor initializes state correctly
function test_constructor() public view {
assertEq(
address(slasherContract.allocationManager()),
address(allocationManager),
"AllocationManager address mismatch"
);
assertEq(
address(slasherContract.serviceManager()),
address(serviceManager),
"ServiceManager address mismatch"
);
assertEq(slasherContract.nextRequestId(), 0, "NextRequestId should be initialized to 0");
}
// Test that a function with the onlySlasher modifier reverts when called by non-ServiceManager
function test_onlySlasherModifier_nonSlasher() public {
vm.prank(nonServiceManagerRole);
vm.expectRevert(abi.encodeWithSelector(ISlasherErrors.OnlySlasher.selector));
slasherContract.restrictedFunction();
}
// Test that a function with the onlySlasher modifier allows access when called by ServiceManager
function test_onlySlasherModifier_slasher() public {
vm.prank(address(serviceManager));
// This should not revert
slasherContract.restrictedFunction();
}
// Test that fulfilSlashingRequest can be called by anyone now that the onlySlasher modifier has been removed
function test_fulfilSlashingRequest_anyoneCanCall() public {
// Setup mock params
address operator = address(0xabcd);
uint32 operatorSetId = 1;
IStrategy[] memory strategies = new IStrategy[](1);
strategies[0] = strategyMock1;
uint256[] memory wadsToSlash = new uint256[](1);
wadsToSlash[0] = 1e16;
string memory description = "Test slashing by non-ServiceManager";
IAllocationManagerTypes.SlashingParams memory params = IAllocationManagerTypes
.SlashingParams({
operator: operator,
operatorSetId: operatorSetId,
strategies: strategies,
wadsToSlash: wadsToSlash,
description: description
});
// Mock the allocationManager.slashOperator call
vm.mockCall(
address(allocationManager),
abi.encodeWithSelector(
IAllocationManager.slashOperator.selector, serviceManager.avs(), params
),
abi.encode()
);
uint256 requestId = 5;
// A random address should be able to call fulfilSlashingRequest
vm.prank(nonServiceManagerRole);
vm.expectEmit(true, true, true, true);
emit ISlasherEvents.OperatorSlashed(
requestId, operator, operatorSetId, wadsToSlash, description
);
slasherContract.fulfilSlashingRequest(requestId, params);
}
// Test the _checkSlasher internal function
function test_checkSlasher() public {
// Should succeed for ServiceManager
vm.prank(address(serviceManager));
slasherContract.checkSlasher(address(serviceManager));
// Should revert for non-ServiceManager
vm.expectRevert(abi.encodeWithSelector(ISlasherErrors.OnlySlasher.selector));
slasherContract.checkSlasher(nonServiceManagerRole);
}
// Test the _fulfilSlashingRequest internal function with different parameters
function test_fulfilSlashingRequest_withMultipleStrategies() public {
// Setup mock params with multiple strategies
address operator = address(0xabcd);
uint32 operatorSetId = 1;
IStrategy[] memory strategies = new IStrategy[](2);
strategies[0] = strategyMock1;
strategies[1] = strategyMock2;
uint256[] memory wadsToSlash = new uint256[](2);
wadsToSlash[0] = 1e16; // 1% of the operator's stake
wadsToSlash[1] = 2e16; // 2% of the operator's stake
string memory description = "Multiple strategy slashing";
IAllocationManagerTypes.SlashingParams memory params = IAllocationManagerTypes
.SlashingParams({
operator: operator,
operatorSetId: operatorSetId,
strategies: strategies,
wadsToSlash: wadsToSlash,
description: description
});
// Mock the allocationManager.slashOperator call
vm.mockCall(
address(allocationManager),
abi.encodeWithSelector(
IAllocationManager.slashOperator.selector, serviceManager.avs(), params
),
abi.encode()
);
uint256 requestId = 2;
// ServiceManager should be able to call fulfilSlashingRequest
vm.prank(address(serviceManager));
vm.expectEmit(true, true, true, true);
emit ISlasherEvents.OperatorSlashed(
requestId, operator, operatorSetId, wadsToSlash, description
);
slasherContract.fulfilSlashingRequest(requestId, params);
}
// Test fulfilSlashingRequest with zero wads to slash
function test_fulfilSlashingRequest_zeroWadsToSlash() public {
// Setup mock params with zero wads
address operator = address(0xabcd);
uint32 operatorSetId = 1;
IStrategy[] memory strategies = new IStrategy[](1);
strategies[0] = strategyMock1;
uint256[] memory wadsToSlash = new uint256[](1);
wadsToSlash[0] = 0; // Zero tokens
string memory description = "Zero wad slashing";
IAllocationManagerTypes.SlashingParams memory params = IAllocationManagerTypes
.SlashingParams({
operator: operator,
operatorSetId: operatorSetId,
strategies: strategies,
wadsToSlash: wadsToSlash,
description: description
});
// Mock the allocationManager.slashOperator call
vm.mockCall(
address(allocationManager),
abi.encodeWithSelector(
IAllocationManager.slashOperator.selector, serviceManager.avs(), params
),
abi.encode()
);
uint256 requestId = 3;
// ServiceManager should be able to call fulfilSlashingRequest
vm.prank(address(serviceManager));
vm.expectEmit(true, true, true, true);
emit ISlasherEvents.OperatorSlashed(
requestId, operator, operatorSetId, wadsToSlash, description
);
slasherContract.fulfilSlashingRequest(requestId, params);
}
// Test error handling when allocationManager.slashOperator reverts
function test_fulfilSlashingRequest_allocationManagerReverts() public {
// Setup mock params
address operator = address(0xabcd);
uint32 operatorSetId = 1;
IStrategy[] memory strategies = new IStrategy[](1);
strategies[0] = strategyMock1;
uint256[] memory wadsToSlash = new uint256[](1);
wadsToSlash[0] = 1e16; // 1% of the operator's stake
string memory description = "Revert test";
IAllocationManagerTypes.SlashingParams memory params = IAllocationManagerTypes
.SlashingParams({
operator: operator,
operatorSetId: operatorSetId,
strategies: strategies,
wadsToSlash: wadsToSlash,
description: description
});
// Mock the allocationManager.slashOperator call to revert
vm.mockCallRevert(
address(allocationManager),
abi.encodeWithSelector(
IAllocationManager.slashOperator.selector, serviceManager.avs(), params
),
abi.encodeWithSignature("SomeError()")
);
uint256 requestId = 4;
// ServiceManager should be able to call fulfilSlashingRequest
vm.prank(address(serviceManager));
vm.expectRevert(abi.encodeWithSignature("SomeError()"));
slasherContract.fulfilSlashingRequest(requestId, params);
}
}

View file

@ -0,0 +1,397 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.27;
import {Test, console} from "forge-std/Test.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol";
import {IRewardsCoordinator} from
"eigenlayer-contracts/src/contracts/interfaces/IRewardsCoordinator.sol";
import {
IAllocationManagerErrors,
IAllocationManager,
IAllocationManagerTypes
} from "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol";
import {MockAVSDeployer} from "./utils/MockAVSDeployer.sol";
import {IServiceManager} from "../src/interfaces/IServiceManager.sol";
import {ISlasher, ISlasherErrors, ISlasherEvents} from "../src/interfaces/ISlasher.sol";
import {
IVetoableSlasher,
IVetoableSlasherTypes,
IVetoableSlasherErrors,
IVetoableSlasherEvents
} from "../src/interfaces/IVetoableSlasher.sol";
import {SlasherBase} from "../src/middleware/SlasherBase.sol";
import {VetoableSlasher} from "../src/middleware/VetoableSlasher.sol";
contract VetoableSlasherTest is MockAVSDeployer {
address public nonServiceManagerRole;
address public nonVetoCommittee;
// Events for testing
event SlashingRequested(
uint256 indexed requestId,
address indexed operator,
uint32 indexed operatorSetId,
uint256[] wadsToSlash,
string description
);
event SlashingRequestCancelled(
address indexed operator, uint32 operatorSetId, uint256[] wadsToSlash, string description
);
event SlashingRequestFulfilled(
address indexed operator, uint32 operatorSetId, uint256[] wadsToSlash, string description
);
function setUp() public virtual {
_deployMockEigenLayerAndAVS();
_setUpDefaultStrategiesAndMultipliers();
// Set up roles for testing
nonServiceManagerRole = address(0x5678);
nonVetoCommittee = address(0xdcba);
}
// Test constructor initializes state correctly
function test_constructor() public view {
assertEq(
address(vetoableSlasher.allocationManager()),
address(allocationManager),
"AllocationManager address mismatch"
);
assertEq(
address(vetoableSlasher.serviceManager()),
address(serviceManager),
"ServiceManager address mismatch"
);
assertEq(
vetoableSlasher.vetoCommittee(), vetoCommitteeMember, "Veto committee address mismatch"
);
assertEq(
vetoableSlasher.vetoWindowBlocks(), vetoWindowBlocks, "Veto window blocks mismatch"
);
assertEq(vetoableSlasher.nextRequestId(), 0, "NextRequestId should be initialized to 0");
}
// Test queueSlashingRequest reverts when called by non-ServiceManager
function test_queueSlashingRequest_nonServiceManager() public {
IAllocationManagerTypes.SlashingParams memory params;
vm.prank(nonServiceManagerRole);
vm.expectRevert(abi.encodeWithSelector(ISlasherErrors.OnlySlasher.selector));
vetoableSlasher.queueSlashingRequest(params);
}
// Test queueSlashingRequest succeeds when called by ServiceManager
function test_queueSlashingRequest_serviceManager() public {
// Setup mock params
address operator = address(0x1111);
uint32 operatorSetId = 1;
IStrategy[] memory strategies = new IStrategy[](1);
strategies[0] = strategyMock1;
uint256[] memory wadsToSlash = new uint256[](1);
wadsToSlash[0] = 1e16; // 1% of the operator's stake
string memory description = "Test slashing";
IAllocationManagerTypes.SlashingParams memory params = IAllocationManagerTypes
.SlashingParams({
operator: operator,
operatorSetId: operatorSetId,
strategies: strategies,
wadsToSlash: wadsToSlash,
description: description
});
uint256 requestId = 0; // First request
vm.prank(address(serviceManager));
vm.expectEmit(true, true, true, true);
emit IVetoableSlasherEvents.SlashingRequested(
requestId, operator, operatorSetId, wadsToSlash, description
);
vetoableSlasher.queueSlashingRequest(params);
// Verify request is stored correctly
(
IAllocationManagerTypes.SlashingParams memory storedParams,
uint256 requestBlock,
bool isPending
) = _getSlashingRequest(requestId);
assertEq(storedParams.operator, operator, "Operator mismatch");
assertEq(storedParams.operatorSetId, operatorSetId, "OperatorSetId mismatch");
assertEq(storedParams.wadsToSlash[0], wadsToSlash[0], "WadsToSlash mismatch");
assertEq(storedParams.description, description, "Description mismatch");
assertEq(requestBlock, block.number, "Request block mismatch");
assertEq(isPending, true, "Status mismatch");
// Verify nextRequestId is incremented
assertEq(vetoableSlasher.nextRequestId(), 1, "NextRequestId should be incremented");
}
// Test cancelSlashingRequest reverts when called by non-veto committee
function test_cancelSlashingRequest_nonVetoCommittee() public {
// First create a request
_createSlashingRequest();
vm.prank(nonVetoCommittee);
vm.expectRevert(abi.encodeWithSelector(IVetoableSlasherErrors.OnlyVetoCommittee.selector));
vetoableSlasher.cancelSlashingRequest(0);
}
// Test cancelSlashingRequest succeeds when called by veto committee within veto period
function test_cancelSlashingRequest_withinVetoPeriod() public {
// First create a request
uint256 requestId = _createSlashingRequest();
(IAllocationManagerTypes.SlashingParams memory params,,) = _getSlashingRequest(requestId);
vm.prank(vetoCommitteeMember);
vm.expectEmit(true, false, false, false);
emit IVetoableSlasherEvents.SlashingRequestCancelled(
params.operator, params.operatorSetId, params.wadsToSlash, params.description
);
vetoableSlasher.cancelSlashingRequest(requestId);
// Verify request status is updated
(,, bool isPending) = _getSlashingRequest(requestId);
assertEq(isPending, false, "Status should be Cancelled");
}
// Test cancelSlashingRequest reverts when veto period has passed
function test_cancelSlashingRequest_afterVetoPeriod() public {
// First create a request
uint256 requestId = _createSlashingRequest();
// Fast forward past veto period
vm.roll(block.number + vetoWindowBlocks + 1);
vm.prank(vetoCommitteeMember);
vm.expectRevert(abi.encodeWithSelector(IVetoableSlasherErrors.VetoPeriodPassed.selector));
vetoableSlasher.cancelSlashingRequest(requestId);
}
// Test cancelSlashingRequest reverts when request is not in Requested state
function test_cancelSlashingRequest_notRequested() public {
// First create a request
uint256 requestId = _createSlashingRequest();
// Cancel it once
vm.prank(vetoCommitteeMember);
vetoableSlasher.cancelSlashingRequest(requestId);
// Try to cancel it again
vm.prank(vetoCommitteeMember);
vm.expectRevert(
abi.encodeWithSelector(IVetoableSlasherErrors.SlashingRequestNotRequested.selector)
);
vetoableSlasher.cancelSlashingRequest(requestId);
}
// Test fulfilSlashingRequest reverts before veto period has passed
function test_fulfilSlashingRequest_beforeVetoPeriod() public {
// First create a request
uint256 requestId = _createSlashingRequest();
vm.prank(address(serviceManager));
vm.expectRevert(abi.encodeWithSelector(IVetoableSlasherErrors.VetoPeriodNotPassed.selector));
vetoableSlasher.fulfilSlashingRequest(requestId);
}
// Test fulfilSlashingRequest reverts when request is cancelled
function test_fulfilSlashingRequest_cancelled() public {
// First create a request
uint256 requestId = _createSlashingRequest();
// Cancel it
vm.prank(vetoCommitteeMember);
vetoableSlasher.cancelSlashingRequest(requestId);
// Fast forward past veto period
vm.roll(block.number + vetoWindowBlocks + 1);
vm.prank(address(serviceManager));
vm.expectRevert(
abi.encodeWithSelector(IVetoableSlasherErrors.SlashingRequestIsCancelled.selector)
);
vetoableSlasher.fulfilSlashingRequest(requestId);
}
// Test fulfilSlashingRequest succeeds after veto period has passed
function test_fulfilSlashingRequest_afterVetoPeriod() public {
// First create a request
uint256 requestId = _createSlashingRequest();
address operator = address(0x1111);
uint32 operatorSetId = 1;
// Setup the mock for slashing
IAllocationManagerTypes.SlashingParams memory params;
(params,,) = _getSlashingRequest(requestId);
vm.mockCall(
address(allocationManager),
abi.encodeWithSelector(
IAllocationManager.slashOperator.selector, serviceManager.avs(), params
),
abi.encode()
);
// Fast forward past veto period
vm.roll(block.number + vetoWindowBlocks + 1);
vm.prank(address(serviceManager));
vm.expectEmit(true, true, true, true);
emit ISlasherEvents.OperatorSlashed(
requestId, operator, operatorSetId, params.wadsToSlash, params.description
);
vm.expectEmit(true, true, true, true);
emit SlashingRequestFulfilled(
operator, operatorSetId, params.wadsToSlash, params.description
);
vetoableSlasher.fulfilSlashingRequest(requestId);
// Verify request is deleted from storage
(
IAllocationManagerTypes.SlashingParams memory emptyParams,
uint256 requestBlock,
bool isPending
) = _getSlashingRequest(requestId);
assertEq(
emptyParams.operator, address(0), "Request should be deleted - operator not zeroed"
);
assertEq(requestBlock, 0, "Request should be deleted - requestBlock not zeroed");
assertEq(isPending, false, "Request should be deleted - isPending not false");
}
// Test cancelSlashingRequest properly deletes the request from storage
function test_cancelSlashingRequest_deletesFromStorage() public {
// First create a request
uint256 requestId = _createSlashingRequest();
(IAllocationManagerTypes.SlashingParams memory params,,) = _getSlashingRequest(requestId);
vm.prank(vetoCommitteeMember);
vm.expectEmit(true, true, true, true);
emit SlashingRequestCancelled(
params.operator, params.operatorSetId, params.wadsToSlash, params.description
);
vetoableSlasher.cancelSlashingRequest(requestId);
// Verify request is deleted from storage
(
IAllocationManagerTypes.SlashingParams memory emptyParams,
uint256 requestBlock,
bool isPending
) = _getSlashingRequest(requestId);
assertEq(
emptyParams.operator, address(0), "Request should be deleted - operator not zeroed"
);
assertEq(requestBlock, 0, "Request should be deleted - requestBlock not zeroed");
assertEq(isPending, false, "Request should be deleted - isPending not false");
}
// Test multiple requests flow
function test_multipleRequests() public {
// Create first request
uint256 requestId1 = _createSlashingRequest();
// Create second request with different parameters
address operator2 = address(0x2222);
uint32 operatorSetId2 = 2;
IStrategy[] memory strategies2 = new IStrategy[](1);
strategies2[0] = strategyMock2;
uint256[] memory wadsToSlash2 = new uint256[](1);
wadsToSlash2[0] = 2e16; // 2% of the operator's stake
string memory description2 = "Second slashing";
IAllocationManagerTypes.SlashingParams memory params2 = IAllocationManagerTypes
.SlashingParams({
operator: operator2,
operatorSetId: operatorSetId2,
strategies: strategies2,
wadsToSlash: wadsToSlash2,
description: description2
});
uint256 requestId2 = 1; // Second request
vm.prank(address(serviceManager));
vetoableSlasher.queueSlashingRequest(params2);
// Cancel the first request
vm.prank(vetoCommitteeMember);
vetoableSlasher.cancelSlashingRequest(requestId1);
// Setup the mock for slashing the second request
vm.mockCall(
address(allocationManager),
abi.encodeWithSelector(
IAllocationManager.slashOperator.selector, serviceManager.avs(), params2
),
abi.encode()
);
// Fast forward past veto period
vm.roll(block.number + vetoWindowBlocks + 1);
// Try to fulfil the first (cancelled) request - should revert
vm.prank(address(serviceManager));
vm.expectRevert(
abi.encodeWithSelector(IVetoableSlasherErrors.SlashingRequestIsCancelled.selector)
);
vetoableSlasher.fulfilSlashingRequest(requestId1);
// fulfil the second request - should succeed
vm.prank(address(serviceManager));
vetoableSlasher.fulfilSlashingRequest(requestId2);
// Verify states
(,, bool isPending1) = _getSlashingRequest(requestId1);
(,, bool isPending2) = _getSlashingRequest(requestId2);
assertEq(isPending1, false, "Request 1 status should be Cancelled");
assertEq(isPending2, false, "Request 2 status should be Completed");
}
// Helper function to create a standard slashing request
function _createSlashingRequest() internal returns (uint256) {
address operator = address(0x1111);
uint32 operatorSetId = 1;
IStrategy[] memory strategies = new IStrategy[](1);
strategies[0] = strategyMock1;
uint256[] memory wadsToSlash = new uint256[](1);
wadsToSlash[0] = 1e16; // 1% of the operator's stake
string memory description = "Test slashing";
IAllocationManagerTypes.SlashingParams memory params = IAllocationManagerTypes
.SlashingParams({
operator: operator,
operatorSetId: operatorSetId,
strategies: strategies,
wadsToSlash: wadsToSlash,
description: description
});
uint256 requestId = vetoableSlasher.nextRequestId();
vm.prank(address(serviceManager));
vetoableSlasher.queueSlashingRequest(params);
return requestId;
}
// Helper function to extract SlashingRequest from storage
function _getSlashingRequest(
uint256 requestId
)
internal
view
returns (
IAllocationManagerTypes.SlashingParams memory params,
uint256 requestBlock,
bool isPending
)
{
(params, requestBlock, isPending) = vetoableSlasher.slashingRequests(requestId);
}
}

View file

@ -10,6 +10,7 @@ import {IAllocationManager} from
import {ServiceManagerBase} from "../../src/middleware/ServiceManagerBase.sol";
import {ServiceManagerBaseStorage} from "../../src/middleware/ServiceManagerBaseStorage.sol";
import {IVetoableSlasher} from "../../src/interfaces/IVetoableSlasher.sol";
/**
* @title Minimal implementation of a ServiceManager-type contract.
@ -31,4 +32,15 @@ contract ServiceManagerMock is ServiceManagerBase {
) public virtual initializer {
__ServiceManagerBase_init(initialOwner, rewardsInitiator);
}
/**
* @notice Sets the slasher contract
* @param slasher The slasher contract address
* @dev Only callable by the owner
*/
function setSlasher(
IVetoableSlasher slasher
) external override onlyOwner {
_slasher = slasher;
}
}

View file

@ -0,0 +1,37 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.27;
import {SlasherBase} from "../../src/middleware/SlasherBase.sol";
import {
IAllocationManager,
IAllocationManagerTypes
} from "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol";
import {IServiceManager} from "../../src/interfaces/IServiceManager.sol";
// SlasherMock implementation for testing
contract SlasherMock is SlasherBase {
constructor(
IAllocationManager _allocationManager,
IServiceManager _serviceManager
) SlasherBase(_allocationManager, _serviceManager) {}
// Expose the internal _fulfilSlashingRequest function for testing
function fulfilSlashingRequest(
uint256 _requestId,
IAllocationManagerTypes.SlashingParams memory _params
) external {
_fulfilSlashingRequest(_requestId, _params);
}
// Function with the onlySlasher modifier for testing
function restrictedFunction() external onlySlasher {
// Do nothing, just for testing the modifier
}
// Expose the internal _checkSlasher function for testing
function checkSlasher(
address account
) external view {
_checkSlasher(account);
}
}

View file

@ -23,6 +23,8 @@ import {StrategyBase} from "eigenlayer-contracts/src/contracts/strategies/Strate
import {ERC20FixedSupply} from "./ERC20FixedSupply.sol";
import {IServiceManager} from "../../src/interfaces/IServiceManager.sol";
import {VetoableSlasher} from "../../src/middleware/VetoableSlasher.sol";
import {IVetoableSlasher} from "../../src/interfaces/IVetoableSlasher.sol";
// Mocks
import {StrategyManagerMock} from "eigenlayer-contracts/src/test/mocks/StrategyManagerMock.sol";
@ -46,6 +48,11 @@ contract MockAVSDeployer is Test {
// AVS contracts
ServiceManagerMock public serviceManager;
ServiceManagerMock public serviceManagerImplementation;
VetoableSlasher public vetoableSlasher;
// Roles and parameters
address public vetoCommitteeMember = address(uint160(uint256(keccak256("vetoCommitteeMember"))));
uint32 public vetoWindowBlocks = 100; // 100 blocks veto window for tests
// EigenLayer contracts
StrategyManagerMock public strategyManagerMock;
@ -202,6 +209,19 @@ contract MockAVSDeployer is Test {
);
cheats.stopPrank();
console.log("ServiceManager implementation deployed");
// Deploy and configure the VetoableSlasher
cheats.startPrank(regularDeployer);
vetoableSlasher = new VetoableSlasher(
allocationManager, serviceManager, vetoCommitteeMember, vetoWindowBlocks
);
cheats.stopPrank();
// Set the slasher in the ServiceManager
cheats.prank(avsOwner);
serviceManager.setSlasher(vetoableSlasher);
console.log("VetoableSlasher deployed and configured");
}
function _setUpDefaultStrategiesAndMultipliers() internal virtual {
@ -283,6 +303,7 @@ contract MockAVSDeployer is Test {
vm.label(address(allocationManagerImplementation), "AllocationManagerImplementation");
vm.label(address(serviceManager), "ServiceManager");
vm.label(address(serviceManagerImplementation), "ServiceManagerImplementation");
vm.label(address(vetoableSlasher), "VetoableSlasher");
}
/// @dev Sort to ensure that the array is in ascending order for strategies