diff --git a/certora/applyHarness.patch b/certora/applyHarness.patch index e215a8f11..d45712d7f 100644 --- a/certora/applyHarness.patch +++ b/certora/applyHarness.patch @@ -1,17 +1,17 @@ diff -druN ../score/DelegationManager.sol core/DelegationManager.sol ---- ../score/DelegationManager.sol 2023-01-13 14:12:34 -+++ core/DelegationManager.sol 2023-01-13 14:24:43 -@@ -160,10 +160,10 @@ +--- ../score/DelegationManager.sol 2023-07-31 15:26:59 ++++ core/DelegationManager.sol 2023-07-31 15:49:22 +@@ -209,8 +209,11 @@ + * Called by the StrategyManager whenever shares are decremented from a user's share balance, for example when a new withdrawal is queued. + * @dev Callable only by the StrategyManager. */ - function decreaseDelegatedShares( - address staker, -- IStrategy[] calldata strategies, -- uint256[] calldata shares -+ IStrategy[] memory strategies, // MUNGED calldata => memory -+ uint256[] memory shares // MUNGED calldata => memory - ) +- function decreaseDelegatedShares(address staker, IStrategy[] calldata strategies, uint256[] calldata shares) - external -+ public // MUNGED external => public ++ function decreaseDelegatedShares( ++ // MUNGED calldata => memory ++ address staker, IStrategy[] memory strategies, uint256[] memory shares) ++ // MUNGED external => public ++ public onlyStrategyManager { if (isDelegated(staker)) { \ No newline at end of file diff --git a/certora/specs/core/DelegationManager.spec b/certora/specs/core/DelegationManager.spec index 388ce603b..1500fd175 100644 --- a/certora/specs/core/DelegationManager.spec +++ b/certora/specs/core/DelegationManager.spec @@ -17,13 +17,13 @@ methods { function _.withdraw(address,address,uint256) external => DISPATCHER(true); // external calls to EigenPodManager - function _.withdrawBeaconChainETH(address,address,uint256) external => DISPATCHER(true); + function _.withdrawRestakedBeaconChainETH(address,address,uint256) external => DISPATCHER(true); // external calls to EigenPod - function _.withdrawBeaconChainETH(address,uint256) external => DISPATCHER(true); + function _.withdrawRestakedBeaconChainETH(address,uint256) external => DISPATCHER(true); // external calls to PauserRegistry - function _.pauser() external => DISPATCHER(true); + function _.isPauser(address) external => DISPATCHER(true); function _.unpauser() external => DISPATCHER(true); // external calls to ERC1271 (can import OpenZeppelin mock implementation) @@ -41,12 +41,16 @@ methods { function _._delegationWithdrawnHook(address,address,address[]memory, uint256[] memory) internal => NONDET; //envfree functions - function isDelegated(address staker) external returns (bool) envfree; - function isNotDelegated(address staker) external returns (bool) envfree; - function isOperator(address operator) external returns (bool) envfree; function delegatedTo(address staker) external returns (address) envfree; - function delegationTerms(address operator) external returns (address) envfree; + function operatorDetails(address operator) external returns (IDelegationManager.OperatorDetails memory) envfree; + function earningsReceiver(address operator) external returns (address) envfree; + function delegationApprover(address operator) external returns (address) envfree; + function stakerOptOutWindowBlocks(address operator) external returns (uint256) envfree; function operatorShares(address operator, address strategy) external returns (uint256) envfree; + function isDelegated(address staker) external returns (bool) envfree; + function isOperator(address operator) external returns (bool) envfree; + function stakerNonce(address staker) external returns (uint256) envfree; + function delegationApproverNonce(address delegationApprover) external returns (uint256) envfree; function owner() external returns (address) envfree; function strategyManager() external returns (address) envfree; } @@ -54,7 +58,7 @@ methods { /* LEGAL STATE TRANSITIONS: 1) -FROM not delegated -- defined as delegatedTo(staker) == address(0), likewise returned by isNotDelegated(staker)-- +FROM not delegated -- defined as delegatedTo(staker) == address(0), likewise returned by !isDelegated(staker)-- AND not registered as an operator -- defined as isOperator(operator) == false, or equivalently, delegationTerms(operator) == 0, TO delegated but not an operator in this case, the end state is that: @@ -100,7 +104,7 @@ FORBIDDEN STATES: Combining the above, an address can be (classified as an operator) *iff* they are (delegated to themselves). The exception is the zero address, since by default an address is 'delegated to the zero address' when they are not delegated at all */ -//definition notDelegated -- defined as delegatedTo(staker) == address(0), likewise returned by isNotDelegated(staker)-- +//definition notDelegated -- defined as delegatedTo(staker) == address(0), likewise returned by !isDelegated(staker)-- // verify that anyone who is registered as an operator is also always delegated to themselves invariant operatorsAlwaysDelegatedToSelf(address operator) @@ -156,39 +160,41 @@ rule cannotChangeDelegationWithoutUndelegating(address staker) { rule canOnlyDelegateWithSpecificFunctions(address staker) { requireInvariant operatorsAlwaysDelegatedToSelf(staker); // assume the staker begins as undelegated - require(isNotDelegated(staker)); + require(!isDelegated(staker)); // perform arbitrary function call method f; env e; - if (f.selector == sig:delegateTo(address).selector) { + if (f.selector == sig:delegateTo(address, IDelegationManager.SignatureWithExpiry).selector) { address operator; - delegateTo(e, operator); + IDelegationManager.SignatureWithExpiry approverSignatureAndExpiry; + delegateTo(e, operator, approverSignatureAndExpiry); // we check against operator being the zero address here, since we view being delegated to the zero address as *not* being delegated if (e.msg.sender == staker && isOperator(operator) && operator != 0) { assert (isDelegated(staker) && delegatedTo(staker) == operator, "failure in delegateTo"); } else { - assert (isNotDelegated(staker), "staker delegated to inappropriate address?"); + assert (!isDelegated(staker), "staker delegated to inappropriate address?"); } - } else if (f.selector == sig:delegateToBySignature(address, address, uint256, bytes).selector) { + } else if (f.selector == sig:delegateToBySignature(address, address, IDelegationManager.SignatureWithExpiry, IDelegationManager.SignatureWithExpiry).selector) { address toDelegateFrom; address operator; - uint256 expiry; - bytes signature; - delegateToBySignature(e, toDelegateFrom, operator, expiry, signature); + IDelegationManager.SignatureWithExpiry stakerSignatureAndExpiry; + IDelegationManager.SignatureWithExpiry approverSignatureAndExpiry; + delegateToBySignature(e, toDelegateFrom, operator, stakerSignatureAndExpiry, approverSignatureAndExpiry); // TODO: this check could be stricter! need to filter when the block timestamp is appropriate for expiry and signature is valid - assert (isNotDelegated(staker) || delegatedTo(staker) == operator, "delegateToBySignature bug?"); - } else if (f.selector == sig:registerAsOperator(address).selector) { - address delegationTerms; - registerAsOperator(e, delegationTerms); - if (e.msg.sender == staker && delegationTerms != 0) { + assert (!isDelegated(staker) || delegatedTo(staker) == operator, "delegateToBySignature bug?"); + } else if (f.selector == sig:registerAsOperator(IDelegationManager.OperatorDetails, string).selector) { + IDelegationManager.OperatorDetails operatorDetails; + string metadataURI; + registerAsOperator(e, operatorDetails, metadataURI); + if (e.msg.sender == staker) { assert (isOperator(staker)); } else { - assert(isNotDelegated(staker)); + assert(!isDelegated(staker)); } } else { calldataarg arg; f(e,arg); - assert (isNotDelegated(staker), "staker became delegated through inappropriate function call"); + assert (!isDelegated(staker), "staker became delegated through inappropriate function call"); } } diff --git a/certora/specs/core/Slasher.spec b/certora/specs/core/Slasher.spec index 8b3397dc2..19fb330ba 100644 --- a/certora/specs/core/Slasher.spec +++ b/certora/specs/core/Slasher.spec @@ -21,17 +21,13 @@ methods { function _.withdraw(address,address,uint256) external => DISPATCHER(true); // external calls to EigenPodManager - function _.withdrawBeaconChainETH(address,address,uint256) external => DISPATCHER(true); + function _.withdrawRestakedBeaconChainETH(address,address,uint256) external => DISPATCHER(true); // external calls to EigenPod - function _.withdrawBeaconChainETH(address,uint256) external => DISPATCHER(true); - - // external calls to IDelegationTerms - function _.onDelegationWithdrawn(address,address[],uint256[]) external => CONSTANT; - function _.onDelegationReceived(address,address[],uint256[]) external => CONSTANT; - + function _.withdrawRestakedBeaconChainETH(address,uint256) external => DISPATCHER(true); + // external calls to PauserRegistry - function _.pauser() external => DISPATCHER(true); + function _.isPauser(address) external => DISPATCHER(true); function _.unpauser() external => DISPATCHER(true); //// Harnessed Functions diff --git a/certora/specs/core/StrategyManager.spec b/certora/specs/core/StrategyManager.spec index c90dfe07e..9ac38a8d0 100644 --- a/certora/specs/core/StrategyManager.spec +++ b/certora/specs/core/StrategyManager.spec @@ -38,12 +38,8 @@ methods { // external calls to DelayedWithdrawalRouter (from EigenPod) function _.createDelayedWithdrawal(address, address) external => DISPATCHER(true); - // external calls to IDelegationTerms - function _.onDelegationWithdrawn(address,address[],uint256[]) external => CONSTANT; - function _.onDelegationReceived(address,address[],uint256[]) external => CONSTANT; - // external calls to PauserRegistry - function _.pauser() external => DISPATCHER(true); + function _.isPauser(address) external => DISPATCHER(true); function _.unpauser() external => DISPATCHER(true); // external calls to ERC20 diff --git a/certora/specs/permissions/Pausable.spec b/certora/specs/permissions/Pausable.spec index fd0137927..aaeb90fe5 100644 --- a/certora/specs/permissions/Pausable.spec +++ b/certora/specs/permissions/Pausable.spec @@ -1,7 +1,7 @@ methods { // external calls to PauserRegistry - function _.pauser() external => DISPATCHER(true); + function _.isPauser(address) external => DISPATCHER(true); function _.unpauser() external => DISPATCHER(true); // envfree functions diff --git a/certora/specs/strategies/StrategyBase.spec b/certora/specs/strategies/StrategyBase.spec index e33fbf86e..0f7ab5e55 100644 --- a/certora/specs/strategies/StrategyBase.spec +++ b/certora/specs/strategies/StrategyBase.spec @@ -4,7 +4,7 @@ methods { function _.stakerStrategyShares(address, address) external => DISPATCHER(true); // external calls to PauserRegistry - function _.pauser() external => DISPATCHER(true); + function _.isPauser(address) external => DISPATCHER(true); function _.unpauser() external => DISPATCHER(true); // external calls to ERC20 diff --git a/docs/EigenLayer-delegation-flow.md b/docs/EigenLayer-delegation-flow.md index 13c775867..92fff9391 100644 --- a/docs/EigenLayer-delegation-flow.md +++ b/docs/EigenLayer-delegation-flow.md @@ -11,8 +11,8 @@ When an operator registers in EigenLayer, the following flow of calls between co ![Registering as an Operator in EigenLayer](images/EL_operator_registration.png?raw=true "Registering as an Operator in EigenLayer") -1. The would-be operator calls `DelegationManager.registerAsOperator`, providing either a `DelegationTerms`-type contract or an EOA as input. The DelegationManager contract stores the `DelegationTerms`-type contract provided by the operator, which may act as an intermediary to help facilitate the relationship between the operator and any stakers who delegate to them. -All of the remaining steps (2-4) proceed as outlined in the delegation process below; the DelegationManager contract treats things as if the operator has delegated *to themselves*. +1. The would-be operator calls `DelegationManager.registerAsOperator`, providing their `OperatorDetails` and an (optional) `metadataURI` string as an input. The DelegationManager contract stores the `OperatorDetails` provided by the operator and emits an event containing the `metadataURI`. The `OperatorDetails` help define the terms of the relationship between the operator and any stakers who delegate to them, and the `metadataURI` can provide additional details about the operator. +All of the remaining steps (2 and 3) proceed as outlined in the delegation process below; the DelegationManager contract treats things as if the operator has delegated *to themselves*. ## Staker Delegation @@ -28,4 +28,5 @@ In either case, the end result is the same, and the flow of calls between contra 1. As outlined above, either the staker themselves calls `DelegationManager.delegateTo`, or the operator (or a third party) calls `DelegationManager.delegateToBySignature`, in which case the DelegationManager contract verifies the provided ECDSA signature 2. The DelegationManager contract calls `Slasher.isFrozen` to verify that the operator being delegated to is not frozen 3. The DelegationManager contract calls `StrategyManager.getDeposits` to get the full list of the staker (who is delegating)'s deposits. It then increases the delegated share amounts of operator (who is being delegated to) appropriately -4. The DelegationManager contract makes a call into the operator's stored `DelegationTerms`-type contract, calling the `onDelegationReceived` function to inform it of the new delegation \ No newline at end of file + +TODO: complete explanation of signature-checking. For the moment, you can look at the IDelegationManager interface or the DelegationManager contract itself for more details on this. \ No newline at end of file diff --git a/docs/EigenLayer-tech-spec.md b/docs/EigenLayer-tech-spec.md index 899ddb3ac..e2805a44e 100644 --- a/docs/EigenLayer-tech-spec.md +++ b/docs/EigenLayer-tech-spec.md @@ -85,12 +85,12 @@ OR 2. a **delegator**, choosing to allow an operator to use their restaked assets in securing applications built on EigenLayer -Stakers can choose which path they’d like to take by interacting with the DelegationManager contract. Stakers who wish to delegate select an operator whom they trust to use their restaked assets to serve applications, while operators register to allow others to delegate to them, specifying a `DelegationTerms`-type contract (or EOA) which receives the funds they earn and can potentially help to mediate their relationship with any stakers who delegate to them. +Stakers can choose which path they’d like to take by interacting with the DelegationManager contract. Stakers who wish to delegate select an operator whom they trust to use their restaked assets to serve applications, while operators register to allow others to delegate to them, specifying their `OperatorDetails` and (optionally) providing a `metadataURI` to help structure and explain their relationship with any stakers who delegate to them. #### Storage in DelegationManager The `DelegationManager` contract relies heavily upon the `StrategyManager` contract. It keeps track of all active operators -- specifically by storing the `Delegation Terms` for each operator -- as well as storing what operator each staker is delegated to. -A **staker** becomes an **operator** by calling `registerAsOperator`. Once registered as an operator, the mapping entry `delegationTerms[operator]` is set **irrevocably** -- in fact we define someone as an operator if `delegationTerms[operator]` returns a nonzero address. Querying `delegationTerms(operator)` returns a `DelegationTerms`-type contract; however, the returned address may be an EOA, in which case the operator is assumed to handle payments through more "trusted" means, such as by doing off-chain computations and separate distributions. +A **staker** becomes an **operator** by calling `registerAsOperator`. By design, registered as an operator, an address can never "deregister" as an operator in EigenLayer. The mapping `delegatedTo` stores which operator each staker is delegated to. Querying `delegatedTo(staker)` will return the *address* of the operator that `staker` is delegated to. Note that operators are *always considered to be delegated to themselves*. DelegationManager defines when an operator is delegated or not, as well as defining what makes someone an operator: diff --git a/docs/images/EL_delegating.png b/docs/images/EL_delegating.png index 5bc7a1eb8..f371009f4 100644 Binary files a/docs/images/EL_delegating.png and b/docs/images/EL_delegating.png differ diff --git a/script/BecomeOperator.s.sol b/script/BecomeOperator.s.sol index ce6bfd70f..c9afb5b13 100644 --- a/script/BecomeOperator.s.sol +++ b/script/BecomeOperator.s.sol @@ -6,8 +6,14 @@ import "./EigenLayerParser.sol"; contract BecomeOperator is Script, DSTest, EigenLayerParser { //performs basic deployment before each test function run() external { + IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + earningsReceiver: msg.sender, + delegationApprover: address(0), + stakerOptOutWindowBlocks: 0 + }); parseEigenLayerParams(); vm.broadcast(msg.sender); - delegation.registerAsOperator(IDelegationTerms(msg.sender)); + string memory emptyStringForMetadataURI; + delegation.registerAsOperator(operatorDetails, emptyStringForMetadataURI); } } diff --git a/script/DepositAndDelegate.s.sol b/script/DepositAndDelegate.s.sol index b989558a3..5846fb6de 100644 --- a/script/DepositAndDelegate.s.sol +++ b/script/DepositAndDelegate.s.sol @@ -29,7 +29,8 @@ contract DepositAndDelegate is Script, DSTest, EigenLayerParser { strategyManager.depositIntoStrategy(eigenStrat, eigen, wethAmount); weth.approve(address(strategyManager), wethAmount); strategyManager.depositIntoStrategy(wethStrat, weth, wethAmount); - delegation.delegateTo(dlnAddr); + IDelegationManager.SignatureWithExpiry memory signatureWithExpiry; + delegation.delegateTo(dlnAddr, signatureWithExpiry); vm.stopBroadcast(); } } diff --git a/script/whitelist/Staker.sol b/script/whitelist/Staker.sol index 545d203ea..00622fb4c 100644 --- a/script/whitelist/Staker.sol +++ b/script/whitelist/Staker.sol @@ -21,7 +21,8 @@ contract Staker is Ownable { ) Ownable() { token.approve(address(strategyManager), type(uint256).max); strategyManager.depositIntoStrategy(strategy, token, amount); - delegation.delegateTo(operator); + IDelegationManager.SignatureWithExpiry memory signatureWithExpiry; + delegation.delegateTo(operator, signatureWithExpiry); } function callAddress(address implementation, bytes memory data) external onlyOwner returns(bytes memory) { diff --git a/src/contracts/core/DelegationManager.sol b/src/contracts/core/DelegationManager.sol index 99cd37125..1742a68d4 100644 --- a/src/contracts/core/DelegationManager.sol +++ b/src/contracts/core/DelegationManager.sol @@ -1,15 +1,12 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity =0.8.12; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts/interfaces/IERC1271.sol"; -import "@openzeppelin/contracts/utils/Address.sol"; -import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; -import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; +import "../interfaces/ISlasher.sol"; import "./DelegationManagerStorage.sol"; import "../permissions/Pausable.sol"; -import "./Slasher.sol"; +import "../libraries/EIP1271SignatureUtils.sol"; /** * @title The primary delegation contract for EigenLayer. @@ -17,19 +14,22 @@ import "./Slasher.sol"; * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service * @notice This is the contract for delegation in EigenLayer. The main functionalities of this contract are * - enabling anyone to register as an operator in EigenLayer - * - allowing new operators to provide a DelegationTerms-type contract, which may mediate their interactions with stakers who delegate to them - * - enabling any staker to delegate its stake to the operator of its choice - * - enabling a staker to undelegate its assets from an operator (performed as part of the withdrawal process, initiated through the StrategyManager) + * - allowing operators to specify parameters related to stakers who delegate to them + * - enabling any staker to delegate its stake to the operator of its choice (a given staker can only delegate to a single operator at a time) + * - enabling a staker to undelegate its assets from the operator it is delegated to (performed as part of the withdrawal process, initiated through the StrategyManager) */ contract DelegationManager is Initializable, OwnableUpgradeable, Pausable, DelegationManagerStorage { // index for flag that pauses new delegations when set uint8 internal constant PAUSED_NEW_DELEGATION = 0; - // bytes4(keccak256("isValidSignature(bytes32,bytes)") - bytes4 constant internal ERC1271_MAGICVALUE = 0x1626ba7e; // chain id at the time of contract deployment - uint256 immutable ORIGINAL_CHAIN_ID; + uint256 internal immutable ORIGINAL_CHAIN_ID; + /** + * @notice Maximum value that `_operatorDetails[operator].stakerOptOutWindowBlocks` is allowed to take, for any operator. + * @dev This is 6 months (technically 180 days) in blocks. + */ + uint256 public constant MAX_STAKER_OPT_OUT_WINDOW_BLOCKS = (180 days) / 12; /// @notice Simple permission for functions that are only callable by the StrategyManager contract. modifier onlyStrategyManager() { @@ -45,107 +45,151 @@ contract DelegationManager is Initializable, OwnableUpgradeable, Pausable, Deleg ORIGINAL_CHAIN_ID = block.chainid; } - /// @dev Emitted when a low-level call to `delegationTerms.onDelegationReceived` fails, returning `returnData` - event OnDelegationReceivedCallFailure(IDelegationTerms indexed delegationTerms, bytes32 returnData); - - /// @dev Emitted when a low-level call to `delegationTerms.onDelegationWithdrawn` fails, returning `returnData` - event OnDelegationWithdrawnCallFailure(IDelegationTerms indexed delegationTerms, bytes32 returnData); - - /// @dev Emitted when an entity registers itself as an operator in the DelegationManager - event RegisterAsOperator(address indexed operator, IDelegationTerms indexed delegationTerms); - function initialize(address initialOwner, IPauserRegistry _pauserRegistry, uint256 initialPausedStatus) external initializer { _initializePauser(_pauserRegistry, initialPausedStatus); - DOMAIN_SEPARATOR = keccak256(abi.encode(DOMAIN_TYPEHASH, keccak256(bytes("EigenLayer")), ORIGINAL_CHAIN_ID, address(this))); + _DOMAIN_SEPARATOR = _calculateDomainSeparator(); _transferOwnership(initialOwner); } // EXTERNAL FUNCTIONS /** - * @notice This will be called by an operator to register itself as an operator that stakers can choose to delegate to. - * @param dt is the `DelegationTerms` contract that the operator has for those who delegate to them. - * @dev An operator can set `dt` equal to their own address (or another EOA address), in the event that they want to split payments - * in a more 'trustful' manner. - * @dev In the present design, once set, there is no way for an operator to ever modify the address of their DelegationTerms contract. + * @notice Registers the `msg.sender` as an operator in EigenLayer, that stakers can choose to delegate to. + * @param registeringOperatorDetails is the `OperatorDetails` for the operator. + * @param metadataURI is a URI for the operator's metadata, i.e. a link providing more details on the operator. + * @dev Note that once an operator is registered, they cannot 'deregister' as an operator, and they will forever be considered "delegated to themself". + * @dev This function will revert if the caller attempts to set their `earningsReceiver` to address(0). + * @dev Note that the `metadataURI` is *never stored in storage* and is instead purely emitted in an `OperatorMetadataURIUpdated` event */ - function registerAsOperator(IDelegationTerms dt) external { + function registerAsOperator(OperatorDetails calldata registeringOperatorDetails, string calldata metadataURI) external { require( - address(delegationTerms[msg.sender]) == address(0), + _operatorDetails[msg.sender].earningsReceiver == address(0), "DelegationManager.registerAsOperator: operator has already registered" ); - // store the address of the delegation contract that the operator is providing. - delegationTerms[msg.sender] = dt; - _delegate(msg.sender, msg.sender); - emit RegisterAsOperator(msg.sender, dt); + _setOperatorDetails(msg.sender, registeringOperatorDetails); + SignatureWithExpiry memory emptySignatureAndExpiry; + // delegate from the operator to themselves + _delegate(msg.sender, msg.sender, emptySignatureAndExpiry); + // emit events + emit OperatorRegistered(msg.sender, registeringOperatorDetails); + emit OperatorMetadataURIUpdated(msg.sender, metadataURI); } /** - * @notice This will be called by a staker to delegate its assets to some operator. - * @param operator is the operator to whom staker (msg.sender) is delegating its assets + * @notice Updates the `msg.sender`'s stored `OperatorDetails`. + * @param newOperatorDetails is the updated `OperatorDetails` for the operator, to replace their current OperatorDetails`. + * @dev The `msg.sender` must have previously registered as an operator in EigenLayer via calling the `registerAsOperator` function. + * @dev This function will revert if the caller attempts to set their `earningsReceiver` to address(0). */ - function delegateTo(address operator) external { - _delegate(msg.sender, operator); + function modifyOperatorDetails(OperatorDetails calldata newOperatorDetails) external { + _setOperatorDetails(msg.sender, newOperatorDetails); } /** - * @notice Delegates from `staker` to `operator`. - * @dev requires that: - * 1) if `staker` is an EOA, then `signature` is valid ECDSA signature from `staker`, indicating their intention for this action - * 2) if `staker` is a contract, then `signature` must will be checked according to EIP-1271 + * @notice Called by an operator to emit an `OperatorMetadataURIUpdated` event, signalling that information about the operator (or at least where this + * information is stored) has changed. + * @param metadataURI is the new metadata URI for the `msg.sender`, i.e. the operator. + * @dev This function will revert if the caller is not an operator. */ - function delegateToBySignature(address staker, address operator, uint256 expiry, bytes memory signature) - external - { - require(expiry >= block.timestamp, "DelegationManager.delegateToBySignature: delegation signature expired"); + function updateOperatorMetadataURI(string calldata metadataURI) external { + require(isOperator(msg.sender), "DelegationManager.updateOperatorMetadataURI: caller must be an operator"); + emit OperatorMetadataURIUpdated(msg.sender, metadataURI); + } - // calculate struct hash, then increment `staker`'s nonce - uint256 nonce = nonces[staker]; - bytes32 structHash = keccak256(abi.encode(DELEGATION_TYPEHASH, staker, operator, nonce, expiry)); - unchecked { - nonces[staker] = nonce + 1; - } + /** + * @notice Called by a staker to delegate its assets to the @param operator. + * @param operator is the operator to whom the staker (`msg.sender`) is delegating its assets for use in serving applications built on EigenLayer. + * @param approverSignatureAndExpiry is a parameter that will be used for verifying that the operator approves of this delegation action in the event that: + * 1) the operator's `delegationApprover` address is set to a non-zero value. + * AND + * 2) neither the operator nor their `delegationApprover` is the `msg.sender`, since in the event that the operator or their delegationApprover + * is the `msg.sender`, then approval is assumed. + * @dev In the event that `approverSignatureAndExpiry` is not checked, its content is ignored entirely; it's recommended to use an empty input + * in this case to save on complexity + gas costs + */ + function delegateTo(address operator, SignatureWithExpiry memory approverSignatureAndExpiry) external { + // go through the internal delegation flow, checking the `approverSignatureAndExpiry` if applicable + _delegate(msg.sender, operator, approverSignatureAndExpiry); + } - bytes32 digestHash; - if (block.chainid != ORIGINAL_CHAIN_ID) { - bytes32 domain_separator = keccak256(abi.encode(DOMAIN_TYPEHASH, keccak256(bytes("EigenLayer")), block.chainid, address(this))); - digestHash = keccak256(abi.encodePacked("\x19\x01", domain_separator, structHash)); - } else{ - digestHash = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash)); + /** + * @notice Delegates from @param staker to @param operator. + * @notice This function will revert if the current `block.timestamp` is equal to or exceeds @param expiry + * @dev The @param stakerSignature is used as follows: + * 1) If `staker` is an EOA, then `stakerSignature` is verified to be a valid ECDSA stakerSignature from `staker`, indicating their intention for this action. + * 2) If `staker` is a contract, then `stakerSignature` will be checked according to EIP-1271. + * @param approverSignatureAndExpiry is a parameter that will be used for verifying that the operator approves of this delegation action in the event that: + * 1) the operator's `delegationApprover` address is set to a non-zero value. + * AND + * 2) neither the operator nor their `delegationApprover` is the `msg.sender`, since in the event that the operator or their delegationApprover + * is the `msg.sender`, then approval is assumed. + * @dev In the event that `approverSignatureAndExpiry` is not checked, its content is ignored entirely; it's recommended to use an empty input + * in this case to save on complexity + gas costs + */ + function delegateToBySignature( + address staker, + address operator, + SignatureWithExpiry memory stakerSignatureAndExpiry, + SignatureWithExpiry memory approverSignatureAndExpiry + ) external { + // check the signature expiry + require(stakerSignatureAndExpiry.expiry >= block.timestamp, "DelegationManager.delegateToBySignature: staker signature expired"); + + // calculate the digest hash, then increment `staker`'s nonce + uint256 currentStakerNonce = stakerNonce[staker]; + bytes32 stakerDigestHash = calculateStakerDelegationDigestHash(staker, currentStakerNonce, operator, stakerSignatureAndExpiry.expiry); + unchecked { + stakerNonce[staker] = currentStakerNonce + 1; } - /** - * check validity of signature: - * 1) if `staker` is an EOA, then `signature` must be a valid ECDSA signature from `staker`, - * indicating their intention for this action - * 2) if `staker` is a contract, then `signature` must will be checked according to EIP-1271 - */ - if (Address.isContract(staker)) { - require(IERC1271(staker).isValidSignature(digestHash, signature) == ERC1271_MAGICVALUE, - "DelegationManager.delegateToBySignature: ERC1271 signature verification failed"); - } else { - require(ECDSA.recover(digestHash, signature) == staker, - "DelegationManager.delegateToBySignature: sig not from staker"); - } + // actually check that the signature is valid + EIP1271SignatureUtils.checkSignature_EIP1271(staker, stakerDigestHash, stakerSignatureAndExpiry.signature); - _delegate(staker, operator); + // go through the internal delegation flow, checking the `approverSignatureAndExpiry` if applicable + _delegate(staker, operator, approverSignatureAndExpiry); } /** * @notice Undelegates `staker` from the operator who they are delegated to. - * @notice Callable only by the StrategyManager + * @notice Callable only by the StrategyManager. * @dev Should only ever be called in the event that the `staker` has no active deposits in EigenLayer. + * @dev Reverts if the `staker` is also an operator, since operators are not allowed to undelegate from themselves. + * @dev Does nothing (but should not revert) if the staker is already undelegated. */ function undelegate(address staker) external onlyStrategyManager { require(!isOperator(staker), "DelegationManager.undelegate: operators cannot undelegate from themselves"); - delegatedTo[staker] = address(0); + address operator = delegatedTo[staker]; + // only make storage changes + emit an event if the staker is actively delegated, otherwise do nothing + if (operator != address(0)) { + emit StakerUndelegated(staker, operator); + delegatedTo[staker] = address(0); + } } + // TODO: decide if on the right auth for this. Perhaps could be another address for the operator to specify /** - * @notice Increases the `staker`'s delegated shares in `strategy` by `shares, typically called when the staker has further deposits into EigenLayer - * @dev Callable only by the StrategyManager + * @notice Called by the operator or the operator's `delegationApprover` address, in order to forcibly undelegate a staker who is currently delegated to the operator. + * @param staker The staker to be force-undelegated. + * @dev This function will revert if the `msg.sender` is not the operator who the staker is delegated to, nor the operator's specified "delegationApprover" + * @dev This function will also revert if the `staker` is themeselves an operator; operators are considered *permanently* delegated to themselves. + * @return The root of the newly queued withdrawal. + * @dev Note that it is assumed that a staker places some trust in an operator, in paricular for the operator to not get slashed; a malicious operator can use this function + * to inconvenience a staker who is delegated to them, but the expectation is that the inconvenience is minor compared to the operator getting purposefully slashed. + */ + function forceUndelegation(address staker) external returns (bytes32) { + address operator = delegatedTo[staker]; + require(staker != operator, "DelegationManager.forceUndelegation: operators cannot be force-undelegated"); + require(msg.sender == operator || msg.sender == _operatorDetails[operator].delegationApprover, + "DelegationManager.forceUndelegation: caller must be operator or their delegationApprover"); + return strategyManager.forceTotalWithdrawal(staker); + } + + /** + * @notice *If the staker is actively delegated*, then increases the `staker`'s delegated shares in `strategy` by `shares`. Otherwise does nothing. + * Called by the StrategyManager whenever new shares are added to a user's share balance. + * @dev Callable only by the StrategyManager. */ function increaseDelegatedShares(address staker, IStrategy strategy, uint256 shares) external @@ -157,28 +201,15 @@ contract DelegationManager is Initializable, OwnableUpgradeable, Pausable, Deleg // add strategy shares to delegate's shares operatorShares[operator][strategy] += shares; - - //Calls into operator's delegationTerms contract to update weights of individual staker - IStrategy[] memory stakerStrategyList = new IStrategy[](1); - uint256[] memory stakerShares = new uint[](1); - stakerStrategyList[0] = strategy; - stakerShares[0] = shares; - - // call into hook in delegationTerms contract - IDelegationTerms dt = delegationTerms[operator]; - _delegationReceivedHook(dt, staker, stakerStrategyList, stakerShares); } } /** - * @notice Decreases the `staker`'s delegated shares in each entry of `strategies` by its respective `shares[i]`, typically called when the staker withdraws from EigenLayer - * @dev Callable only by the StrategyManager + * @notice *If the staker is actively delegated*, then decreases the `staker`'s delegated shares in each entry of `strategies` by its respective `shares[i]`. Otherwise does nothing. + * Called by the StrategyManager whenever shares are decremented from a user's share balance, for example when a new withdrawal is queued. + * @dev Callable only by the StrategyManager. */ - function decreaseDelegatedShares( - address staker, - IStrategy[] calldata strategies, - uint256[] calldata shares - ) + function decreaseDelegatedShares(address staker, IStrategy[] calldata strategies, uint256[] calldata shares) external onlyStrategyManager { @@ -193,162 +224,198 @@ contract DelegationManager is Initializable, OwnableUpgradeable, Pausable, Deleg ++i; } } - - // call into hook in delegationTerms contract - IDelegationTerms dt = delegationTerms[operator]; - _delegationWithdrawnHook(dt, staker, strategies, shares); } } // INTERNAL FUNCTIONS - - /** - * @notice Makes a low-level call to `dt.onDelegationReceived(staker, strategies, shares)`, ignoring reverts and with a gas budget - * equal to `LOW_LEVEL_GAS_BUDGET` (a constant defined in this contract). - * @dev *If* the low-level call fails, then this function emits the event `OnDelegationReceivedCallFailure(dt, returnData)`, where - * `returnData` is *only the first 32 bytes* returned by the call to `dt`. - */ - function _delegationReceivedHook( - IDelegationTerms dt, - address staker, - IStrategy[] memory strategies, - uint256[] memory shares - ) - internal - { - /** - * We use low-level call functionality here to ensure that an operator cannot maliciously make this function fail in order to prevent undelegation. - * In particular, in-line assembly is also used to prevent the copying of uncapped return data which is also a potential DoS vector. - */ - // format calldata - bytes memory lowLevelCalldata = abi.encodeWithSelector(IDelegationTerms.onDelegationReceived.selector, staker, strategies, shares); - // Prepare memory for low-level call return data. We accept a max return data length of 32 bytes - bool success; - bytes32[1] memory returnData; - // actually make the call - assembly { - success := call( - // gas provided to this context - LOW_LEVEL_GAS_BUDGET, - // address to call - dt, - // value in wei for call - 0, - // memory location to copy for calldata - add(lowLevelCalldata, 32), - // length of memory to copy for calldata - mload(lowLevelCalldata), - // memory location to copy return data - returnData, - // byte size of return data to copy to memory - 32 - ) - } - // if the call fails, we emit a special event rather than reverting - if (!success) { - emit OnDelegationReceivedCallFailure(dt, returnData[0]); - } - } - - /** - * @notice Makes a low-level call to `dt.onDelegationWithdrawn(staker, strategies, shares)`, ignoring reverts and with a gas budget - * equal to `LOW_LEVEL_GAS_BUDGET` (a constant defined in this contract). - * @dev *If* the low-level call fails, then this function emits the event `OnDelegationReceivedCallFailure(dt, returnData)`, where - * `returnData` is *only the first 32 bytes* returned by the call to `dt`. + /** + * @notice Internal function that sets the @param operator 's parameters in the `_operatorDetails` mapping to @param newOperatorDetails + * @dev This function will revert if the operator attempts to set their `earningsReceiver` to address(0). */ - function _delegationWithdrawnHook( - IDelegationTerms dt, - address staker, - IStrategy[] memory strategies, - uint256[] memory shares - ) - internal - { - /** - * We use low-level call functionality here to ensure that an operator cannot maliciously make this function fail in order to prevent undelegation. - * In particular, in-line assembly is also used to prevent the copying of uncapped return data which is also a potential DoS vector. - */ - // format calldata - bytes memory lowLevelCalldata = abi.encodeWithSelector(IDelegationTerms.onDelegationWithdrawn.selector, staker, strategies, shares); - // Prepare memory for low-level call return data. We accept a max return data length of 32 bytes - bool success; - bytes32[1] memory returnData; - // actually make the call - assembly { - success := call( - // gas provided to this context - LOW_LEVEL_GAS_BUDGET, - // address to call - dt, - // value in wei for call - 0, - // memory location to copy for calldata - add(lowLevelCalldata, 32), - // length of memory to copy for calldata - mload(lowLevelCalldata), - // memory location to copy return data - returnData, - // byte size of return data to copy to memory - 32 - ) - } - // if the call fails, we emit a special event rather than reverting - if (!success) { - emit OnDelegationWithdrawnCallFailure(dt, returnData[0]); - } + function _setOperatorDetails(address operator, OperatorDetails calldata newOperatorDetails) internal { + require( + newOperatorDetails.earningsReceiver != address(0), + "DelegationManager._setOperatorDetails: cannot set `earningsReceiver` to zero address"); + require(newOperatorDetails.stakerOptOutWindowBlocks <= MAX_STAKER_OPT_OUT_WINDOW_BLOCKS, + "DelegationManager._setOperatorDetails: stakerOptOutWindowBlocks cannot be > MAX_STAKER_OPT_OUT_WINDOW_BLOCKS"); + require(newOperatorDetails.stakerOptOutWindowBlocks >= _operatorDetails[operator].stakerOptOutWindowBlocks, + "DelegationManager._setOperatorDetails: stakerOptOutWindowBlocks cannot be decreased"); + _operatorDetails[operator] = newOperatorDetails; + emit OperatorDetailsModified(msg.sender, newOperatorDetails); } /** * @notice Internal function implementing the delegation *from* `staker` *to* `operator`. * @param staker The address to delegate *from* -- this address is delegating control of its own assets. * @param operator The address to delegate *to* -- this address is being given power to place the `staker`'s assets at risk on services - * @dev Ensures that the operator has registered as a delegate (`address(dt) != address(0)`), verifies that `staker` is not already - * delegated, and records the new delegation. + * @dev Ensures that: + * 1) the `staker` is not already delegated to an operator + * 2) the `operator` has indeed registered as an operator in EigenLayer + * 3) the `operator` is not actively frozen + * 4) if applicable, that the approver signature is valid and non-expired */ - function _delegate(address staker, address operator) internal onlyWhenNotPaused(PAUSED_NEW_DELEGATION) { - IDelegationTerms dt = delegationTerms[operator]; - require( - address(dt) != address(0), "DelegationManager._delegate: operator has not yet registered as a delegate" - ); - - require(isNotDelegated(staker), "DelegationManager._delegate: staker has existing delegation"); - // checks that operator has not been frozen + function _delegate(address staker, address operator, SignatureWithExpiry memory approverSignatureAndExpiry) internal onlyWhenNotPaused(PAUSED_NEW_DELEGATION) { + require(!isDelegated(staker), "DelegationManager._delegate: staker is already actively delegated"); + require(isOperator(operator), "DelegationManager._delegate: operator is not registered in EigenLayer"); require(!slasher.isFrozen(operator), "DelegationManager._delegate: cannot delegate to a frozen operator"); - // record delegation relation between the staker and operator - delegatedTo[staker] = operator; + // fetch the operator's `delegationApprover` address and store it in memory in case we need to use it multiple times + address _delegationApprover = _operatorDetails[operator].delegationApprover; + /** + * Check the `_delegationApprover`'s signature, if applicable. + * If the `_delegationApprover` is the zero address, then the operator allows all stakers to delegate to them and this verification is skipped. + * If the `_delegationApprover` or the `operator` themselves is the caller, then approval is assumed and signature verification is skipped as well. + */ + if (_delegationApprover != address(0) && msg.sender != _delegationApprover && msg.sender != operator) { + // check the signature expiry + require(approverSignatureAndExpiry.expiry >= block.timestamp, "DelegationManager._delegate: approver signature expired"); + + // calculate the digest hash, then increment `delegationApprover`'s nonce + uint256 currentApproverNonce = delegationApproverNonce[_delegationApprover]; + bytes32 approverDigestHash = + calculateDelegationApprovalDigestHash(staker, operator, _delegationApprover, currentApproverNonce, approverSignatureAndExpiry.expiry); + unchecked { + delegationApproverNonce[_delegationApprover] = currentApproverNonce + 1; + } + + + // actually check that the signature is valid + EIP1271SignatureUtils.checkSignature_EIP1271(_delegationApprover, approverDigestHash, approverSignatureAndExpiry.signature); + } - // retrieve list of strategies and their shares from strategy manager + // retrieve `staker`'s list of strategies and the staker's shares in each strategy from the StrategyManager (IStrategy[] memory strategies, uint256[] memory shares) = strategyManager.getDeposits(staker); - // add strategy shares to delegate's shares + // add strategy shares to delegated `operator`'s shares uint256 stratsLength = strategies.length; for (uint256 i = 0; i < stratsLength;) { - // update the share amounts for each of the operator's strategies + // update the share amounts for each of the `operator`'s strategies operatorShares[operator][strategies[i]] += shares[i]; unchecked { ++i; } } - // call into hook in delegationTerms contract - _delegationReceivedHook(dt, staker, strategies, shares); + // record the delegation relation between the staker and operator, and emit an event + delegatedTo[staker] = operator; + emit StakerDelegated(staker, operator); } // VIEW FUNCTIONS + /** + * @notice Getter function for the current EIP-712 domain separator for this contract. + * @dev The domain separator will change in the event of a fork that changes the ChainID. + */ + function domainSeparator() public view returns (bytes32) { + if (block.chainid == ORIGINAL_CHAIN_ID) { + return _DOMAIN_SEPARATOR; + } + else { + return _calculateDomainSeparator(); + } + } /// @notice Returns 'true' if `staker` *is* actively delegated, and 'false' otherwise. function isDelegated(address staker) public view returns (bool) { return (delegatedTo[staker] != address(0)); } - /// @notice Returns 'true' if `staker` is *not* actively delegated, and 'false' otherwise. - function isNotDelegated(address staker) public view returns (bool) { - return (delegatedTo[staker] == address(0)); + /// @notice Returns if an operator can be delegated to, i.e. the `operator` has previously called `registerAsOperator`. + function isOperator(address operator) public view returns (bool) { + return (_operatorDetails[operator].earningsReceiver != address(0)); } - /// @notice Returns if an operator can be delegated to, i.e. it has called `registerAsOperator`. - function isOperator(address operator) public view returns (bool) { - return (address(delegationTerms[operator]) != address(0)); + /** + * @notice returns the OperatorDetails of the `operator`. + * @notice Mapping: operator => OperatorDetails struct + */ + function operatorDetails(address operator) external view returns (OperatorDetails memory) { + return _operatorDetails[operator]; + } + + // @notice Getter function for `_operatorDetails[operator].earningsReceiver` + function earningsReceiver(address operator) external view returns (address) { + return _operatorDetails[operator].earningsReceiver; + } + + // @notice Getter function for `_operatorDetails[operator].delegationApprover` + function delegationApprover(address operator) external view returns (address) { + return _operatorDetails[operator].delegationApprover; + } + + // @notice Getter function for `_operatorDetails[operator].stakerOptOutWindowBlocks` + function stakerOptOutWindowBlocks(address operator) external view returns (uint256) { + return _operatorDetails[operator].stakerOptOutWindowBlocks; + } + + /** + * @notice External function that calculates the digestHash for a `staker` to sign in order to approve their delegation to an `operator`, + * using the staker's current nonce and specifying an expiration of `expiry` + * @param staker The signing staker + * @param operator The operator who is being delegated to + * @param expiry The desired expiry time of the staker's signature + */ + function calculateCurrentStakerDelegationDigestHash(address staker, address operator, uint256 expiry) external view returns (bytes32) { + // fetch the staker's current nonce + uint256 currentStakerNonce = stakerNonce[staker]; + // calculate the digest hash + return calculateStakerDelegationDigestHash(staker, currentStakerNonce, operator, expiry); + } + + /** + * @notice Public function for the staker signature hash calculation in the `delegateToBySignature` function + * @param staker The signing staker + * @param stakerNonce The nonce of the staker. In practice we use the staker's current nonce, stored at `stakerNonce[staker]` + * @param operator The operator who is being delegated to + * @param expiry The desired expiry time of the staker's signature + */ + function calculateStakerDelegationDigestHash(address staker, uint256 stakerNonce, address operator, uint256 expiry) public view returns (bytes32) { + // calculate the struct hash + bytes32 stakerStructHash = keccak256(abi.encode(STAKER_DELEGATION_TYPEHASH, staker, operator, stakerNonce, expiry)); + // calculate the digest hash + bytes32 stakerDigestHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator(), stakerStructHash)); + return stakerDigestHash; + } + + /** + * @notice Exneral function that calculates the digestHash for the `operator`'s "delegationApprover" to sign in order to approve the + * delegation of `staker` to the `operator`, using the approver's current nonce and specifying an expiration of `expiry` + * @param staker The staker who is delegating to the operator + * @param operator The operator who is being delegated to + * @param expiry The desired expiry time of the approver's signature + */ + function calculateCurrentDelegationApprovalDigestHash(address staker, address operator, uint256 expiry) external view returns (bytes32) { + // fetch the operator's `delegationApprover` address and store it in memory + address _delegationApprover = _operatorDetails[operator].delegationApprover; + // get the approver's current nonce and caluclate the struct hash + uint256 currentApproverNonce = delegationApproverNonce[_delegationApprover]; + return calculateDelegationApprovalDigestHash(staker, operator, _delegationApprover, currentApproverNonce, expiry); + } + + /** + * @notice Public function for the the approver signature hash calculation in the `_delegate` function + * @param staker The staker who is delegating to the operator + * @param operator The operator who is being delegated to + * @param _delegationApprover the operator's `delegationApprover` who will be signing the delegationHash (in general) + * @param approverNonce The nonce of the approver. In practice we use the approver's current nonce, stored at `delegationApproverNonce[_delegationApprover]` + * @param expiry The desired expiry time of the approver's signature + */ + function calculateDelegationApprovalDigestHash( + address staker, + address operator, + address _delegationApprover, + uint256 approverNonce, + uint256 expiry + ) public view returns (bytes32) { + // calculate the struct hash + bytes32 approverStructHash = keccak256(abi.encode(DELEGATION_APPROVAL_TYPEHASH, _delegationApprover, staker, operator, approverNonce, expiry)); + // calculate the digest hash + bytes32 approverDigestHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator(), approverStructHash)); + return approverDigestHash; + } + + // @notice Internal function for calculating the current domain separator of this contract + function _calculateDomainSeparator() internal view returns (bytes32) { + return keccak256(abi.encode(DOMAIN_TYPEHASH, keccak256(bytes("EigenLayer")), block.chainid, address(this))); } } diff --git a/src/contracts/core/DelegationManagerStorage.sol b/src/contracts/core/DelegationManagerStorage.sol index 8bc8642c5..e5bb2c272 100644 --- a/src/contracts/core/DelegationManagerStorage.sol +++ b/src/contracts/core/DelegationManagerStorage.sol @@ -2,7 +2,6 @@ pragma solidity =0.8.12; import "../interfaces/IStrategyManager.sol"; -import "../interfaces/IDelegationTerms.sol"; import "../interfaces/IDelegationManager.sol"; import "../interfaces/ISlasher.sol"; @@ -13,19 +12,24 @@ import "../interfaces/ISlasher.sol"; * @notice This storage contract is separate from the logic to simplify the upgrade process. */ abstract contract DelegationManagerStorage is IDelegationManager { - /// @notice Gas budget provided in calls to DelegationTerms contracts - uint256 internal constant LOW_LEVEL_GAS_BUDGET = 1e5; - /// @notice The EIP-712 typehash for the contract's domain bytes32 public constant DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)"); - /// @notice The EIP-712 typehash for the delegation struct used by the contract - bytes32 public constant DELEGATION_TYPEHASH = - keccak256("Delegation(address delegator,address operator,uint256 nonce,uint256 expiry)"); + /// @notice The EIP-712 typehash for the `StakerDelegation` struct used by the contract + bytes32 public constant STAKER_DELEGATION_TYPEHASH = + keccak256("StakerDelegation(address staker,address operator,uint256 nonce,uint256 expiry)"); + + /// @notice The EIP-712 typehash for the `DelegationApproval` struct used by the contract + bytes32 public constant DELEGATION_APPROVAL_TYPEHASH = + keccak256("DelegationApproval(address staker,address operator,uint256 nonce,uint256 expiry)"); - /// @notice EIP-712 Domain separator - bytes32 public DOMAIN_SEPARATOR; + /** + * @notice Original EIP-712 Domain separator for this contract. + * @dev The domain separator may change in the event of a fork that modifies the ChainID. + * Use the getter function `domainSeparator` to get the current domain separator for this contract. + */ + bytes32 internal _DOMAIN_SEPARATOR; /// @notice The StrategyManager contract for EigenLayer IStrategyManager public immutable strategyManager; @@ -33,17 +37,33 @@ abstract contract DelegationManagerStorage is IDelegationManager { /// @notice The Slasher contract for EigenLayer ISlasher public immutable slasher; - /// @notice Mapping: operator => strategy => total number of shares in the strategy delegated to the operator + /** + * @notice returns the total number of shares in `strategy` that are delegated to `operator`. + * @notice Mapping: operator => strategy => total number of shares in the strategy delegated to the operator. + */ mapping(address => mapping(IStrategy => uint256)) public operatorShares; - /// @notice Mapping: operator => delegation terms contract - mapping(address => IDelegationTerms) public delegationTerms; + /** + * @notice Mapping: operator => OperatorDetails struct + * @dev This struct is internal with an external getter so we can return an `OperatorDetails memory` object + */ + mapping(address => OperatorDetails) internal _operatorDetails; - /// @notice Mapping: staker => operator whom the staker has delegated to + /** + * @notice Mapping: staker => operator whom the staker is currently delegated to. + * @dev Note that returning address(0) indicates that the staker is not actively delegated to any operator. + */ mapping(address => address) public delegatedTo; - /// @notice Mapping: delegator => number of signed delegation nonce (used in delegateToBySignature) - mapping(address => uint256) public nonces; + /// @notice Mapping: staker => number of signed messages (used in `delegateToBySignature`) from the staker that this contract has already checked. + mapping(address => uint256) public stakerNonce; + + /** + * @notice Mapping: delegationApprover => number of signed delegation messages (used in `delegateTo` and `delegateToBySignature` from the delegationApprover + * that this contract has already checked. + * @dev Note that these functions only delegationApprover signatures if the operator being delegated to has specified a nonzero address as their `delegationApprover` + */ + mapping(address => uint256) public delegationApproverNonce; constructor(IStrategyManager _strategyManager, ISlasher _slasher) { strategyManager = _strategyManager; @@ -55,5 +75,5 @@ abstract contract DelegationManagerStorage is IDelegationManager { * variables without shifting down storage in the inheritance chain. * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps */ - uint256[46] private __gap; + uint256[44] private __gap; } \ No newline at end of file diff --git a/src/contracts/core/StrategyManager.sol b/src/contracts/core/StrategyManager.sol index a70c0bf09..29fcab0b1 100644 --- a/src/contracts/core/StrategyManager.sol +++ b/src/contracts/core/StrategyManager.sol @@ -1,16 +1,14 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity =0.8.12; -import "@openzeppelin/contracts/interfaces/IERC1271.sol"; -import "@openzeppelin/contracts/utils/Address.sol"; import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; import "@openzeppelin-upgrades/contracts/security/ReentrancyGuardUpgradeable.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import "../interfaces/IEigenPodManager.sol"; import "../permissions/Pausable.sol"; import "./StrategyManagerStorage.sol"; +import "../libraries/EIP1271SignatureUtils.sol"; /** * @title The primary entry- and exit-point for funds into and out of EigenLayer. @@ -40,10 +38,8 @@ contract StrategyManager is // index for flag that pauses withdrawals when set uint8 internal constant PAUSED_WITHDRAWALS = 1; - uint256 immutable ORIGINAL_CHAIN_ID; - - // bytes4(keccak256("isValidSignature(bytes32,bytes)") - bytes4 constant internal ERC1271_MAGICVALUE = 0x1626ba7e; + // chain id at the time of contract deployment + uint256 internal immutable ORIGINAL_CHAIN_ID; /** * @notice Emitted when a new deposit occurs on behalf of `depositor`. @@ -122,6 +118,11 @@ contract StrategyManager is _; } + modifier onlyDelegationManager { + require(msg.sender == address(delegation), "StrategyManager.onlyDelegationManager: not the DelegationManager"); + _; + } + /** * @param _delegation The delegation contract of EigenLayer. * @param _slasher The primary slashing contract of EigenLayer. @@ -149,7 +150,7 @@ contract StrategyManager is external initializer { - DOMAIN_SEPARATOR = keccak256(abi.encode(DOMAIN_TYPEHASH, keccak256(bytes("EigenLayer")), ORIGINAL_CHAIN_ID, address(this))); + _DOMAIN_SEPARATOR = _calculateDomainSeparator(); _initializePauser(_pauserRegistry, initialPausedStatus); _transferOwnership(initialOwner); _setStrategyWhitelister(initialStrategyWhitelister); @@ -274,30 +275,18 @@ contract StrategyManager is nonces[staker] = nonce + 1; } - bytes32 digestHash; - //if chainid has changed, we must re-compute the domain separator - if (block.chainid != ORIGINAL_CHAIN_ID) { - bytes32 domain_separator = keccak256(abi.encode(DOMAIN_TYPEHASH, keccak256(bytes("EigenLayer")), block.chainid, address(this))); - digestHash = keccak256(abi.encodePacked("\x19\x01", domain_separator, structHash)); - } else { - digestHash = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash)); - } - + // calculate the digest hash + bytes32 digestHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator(), structHash)); /** * check validity of signature: * 1) if `staker` is an EOA, then `signature` must be a valid ECDSA signature from `staker`, * indicating their intention for this action - * 2) if `staker` is a contract, then `signature` must will be checked according to EIP-1271 + * 2) if `staker` is a contract, then `signature` will be checked according to EIP-1271 */ - if (Address.isContract(staker)) { - require(IERC1271(staker).isValidSignature(digestHash, signature) == ERC1271_MAGICVALUE, - "StrategyManager.depositIntoStrategyWithSignature: ERC1271 signature verification failed"); - } else { - require(ECDSA.recover(digestHash, signature) == staker, - "StrategyManager.depositIntoStrategyWithSignature: signature not from staker"); - } + EIP1271SignatureUtils.checkSignature_EIP1271(staker, digestHash, signature); + // deposit the tokens (from the `msg.sender`) and credit the new shares to the `staker` shares = _depositIntoStrategy(staker, strategy, token, amount); } @@ -309,6 +298,37 @@ contract StrategyManager is _undelegate(msg.sender); } + /** + * @notice Called by the DelegationManager as part of the forced undelegation of the @param staker from their delegated operator. + * This function queues a withdrawal of all of the `staker`'s shares in EigenLayer to the staker themself, and then undelegates the staker. + * The staker will consequently be able to complete this withdrawal by calling the `completeQueuedWithdrawal` function. + * @param staker The staker to force-undelegate. + * @return The root of the newly queued withdrawal. + */ + function forceTotalWithdrawal(address staker) external + onlyDelegationManager + onlyWhenNotPaused(PAUSED_WITHDRAWALS) + onlyNotFrozen(staker) + nonReentrant + returns (bytes32) + { + uint256 strategiesLength = stakerStrategyList[staker].length; + IStrategy[] memory strategies = new IStrategy[](strategiesLength); + uint256[] memory shares = new uint256[](strategiesLength); + uint256[] memory strategyIndexes = new uint256[](strategiesLength); + + for (uint256 i = 0; i < strategiesLength;) { + uint256 index = (strategiesLength - 1) - i; + strategies[i] = stakerStrategyList[staker][index]; + shares[i] = stakerStrategyShares[staker][strategies[i]]; + strategyIndexes[i] = index; + unchecked { + ++i; + } + } + return _queueWithdrawal(staker, strategyIndexes, strategies, shares, staker, true); + } + /** * @notice Called by a staker to queue a withdrawal of the given amount of `shares` from each of the respective given `strategies`. * @dev Stakers will complete their withdrawal by calling the 'completeQueuedWithdrawal' function. @@ -347,93 +367,7 @@ contract StrategyManager is nonReentrant returns (bytes32) { - require(strategies.length == shares.length, "StrategyManager.queueWithdrawal: input length mismatch"); - require(withdrawer != address(0), "StrategyManager.queueWithdrawal: cannot withdraw to zero address"); - - // modify delegated shares accordingly, if applicable - delegation.decreaseDelegatedShares(msg.sender, strategies, shares); - - uint96 nonce = uint96(numWithdrawalsQueued[msg.sender]); - - // keeps track of the current index in the `strategyIndexes` array - uint256 strategyIndexIndex; - - /** - * Ensure that if the withdrawal includes beacon chain ETH, the specified 'withdrawer' is not different than the caller. - * This is because shares in the enshrined `beaconChainETHStrategy` ultimately represent tokens in **non-fungible** EigenPods, - * while other share in all other strategies represent purely fungible positions. - */ - for (uint256 i = 0; i < strategies.length;) { - if (strategies[i] == beaconChainETHStrategy) { - require(withdrawer == msg.sender, - "StrategyManager.queueWithdrawal: cannot queue a withdrawal of Beacon Chain ETH to a different address"); - require(strategies.length == 1, - "StrategyManager.queueWithdrawal: cannot queue a withdrawal including Beacon Chain ETH and other tokens"); - require(shares[i] % GWEI_TO_WEI == 0, - "StrategyManager.queueWithdrawal: cannot queue a withdrawal of Beacon Chain ETH for an non-whole amount of gwei"); - } - - // the internal function will return 'true' in the event the strategy was - // removed from the depositor's array of strategies -- i.e. stakerStrategyList[depositor] - if (_removeShares(msg.sender, strategyIndexes[strategyIndexIndex], strategies[i], shares[i])) { - unchecked { - ++strategyIndexIndex; - } - } - - emit ShareWithdrawalQueued(msg.sender, nonce, strategies[i], shares[i]); - - //increment the loop - unchecked { - ++i; - } - } - - // fetch the address that the `msg.sender` is delegated to - address delegatedAddress = delegation.delegatedTo(msg.sender); - - QueuedWithdrawal memory queuedWithdrawal; - - { - WithdrawerAndNonce memory withdrawerAndNonce = WithdrawerAndNonce({ - withdrawer: withdrawer, - nonce: nonce - }); - // increment the numWithdrawalsQueued of the sender - unchecked { - numWithdrawalsQueued[msg.sender] = nonce + 1; - } - - // copy arguments into struct and pull delegation info - queuedWithdrawal = QueuedWithdrawal({ - strategies: strategies, - shares: shares, - depositor: msg.sender, - withdrawerAndNonce: withdrawerAndNonce, - withdrawalStartBlock: uint32(block.number), - delegatedAddress: delegatedAddress - }); - - } - - // calculate the withdrawal root - bytes32 withdrawalRoot = calculateWithdrawalRoot(queuedWithdrawal); - - // mark withdrawal as pending - withdrawalRootPending[withdrawalRoot] = true; - - // If the `msg.sender` has withdrawn all of their funds from EigenLayer in this transaction, then they can choose to also undelegate - /** - * Checking that `stakerStrategyList[msg.sender].length == 0` is not strictly necessary here, but prevents reverting very late in logic, - * in the case that 'undelegate' is set to true but the `msg.sender` still has active deposits in EigenLayer. - */ - if (undelegateIfPossible && stakerStrategyList[msg.sender].length == 0) { - _undelegate(msg.sender); - } - - emit WithdrawalQueued(msg.sender, nonce, withdrawer, delegatedAddress, withdrawalRoot); - - return withdrawalRoot; + return _queueWithdrawal(msg.sender, strategyIndexes, strategies, shares, withdrawer, undelegateIfPossible); } /** @@ -785,6 +719,109 @@ contract StrategyManager is stakerStrategyList[depositor].pop(); } + // @notice Internal function for queuing a withdrawal from `staker` to `withdrawer` of `shares` in `strategies`. + function _queueWithdrawal( + address staker, + uint256[] memory strategyIndexes, + IStrategy[] memory strategies, + uint256[] memory shares, + address withdrawer, + bool undelegateIfPossible + ) + internal + returns (bytes32) + { + require(strategies.length == shares.length, "StrategyManager.queueWithdrawal: input length mismatch"); + require(withdrawer != address(0), "StrategyManager.queueWithdrawal: cannot withdraw to zero address"); + + // modify delegated shares accordingly, if applicable + delegation.decreaseDelegatedShares(staker, strategies, shares); + + uint96 nonce = uint96(numWithdrawalsQueued[staker]); + + // keeps track of the current index in the `strategyIndexes` array + uint256 strategyIndexIndex; + + /** + * Ensure that if the withdrawal includes beacon chain ETH, the specified 'withdrawer' is not different than the caller. + * This is because shares in the enshrined `beaconChainETHStrategy` ultimately represent tokens in **non-fungible** EigenPods, + * while other share in all other strategies represent purely fungible positions. + */ + for (uint256 i = 0; i < strategies.length;) { + if (strategies[i] == beaconChainETHStrategy) { + require(withdrawer == staker, + "StrategyManager.queueWithdrawal: cannot queue a withdrawal of Beacon Chain ETH to a different address"); + require(strategies.length == 1, + "StrategyManager.queueWithdrawal: cannot queue a withdrawal including Beacon Chain ETH and other tokens"); + require(shares[i] % GWEI_TO_WEI == 0, + "StrategyManager.queueWithdrawal: cannot queue a withdrawal of Beacon Chain ETH for an non-whole amount of gwei"); + } + + // the internal function will return 'true' in the event the strategy was + // removed from the depositor's array of strategies -- i.e. stakerStrategyList[depositor] + if (_removeShares(staker, strategyIndexes[strategyIndexIndex], strategies[i], shares[i])) { + unchecked { + ++strategyIndexIndex; + } + } + + emit ShareWithdrawalQueued(staker, nonce, strategies[i], shares[i]); + + //increment the loop + unchecked { + ++i; + } + } + + // fetch the address that the `staker` is delegated to + address delegatedAddress = delegation.delegatedTo(staker); + + QueuedWithdrawal memory queuedWithdrawal; + + { + WithdrawerAndNonce memory withdrawerAndNonce = WithdrawerAndNonce({ + withdrawer: withdrawer, + nonce: nonce + }); + // increment the numWithdrawalsQueued of the sender + unchecked { + numWithdrawalsQueued[staker] = nonce + 1; + } + + // copy arguments into struct and pull delegation info + queuedWithdrawal = QueuedWithdrawal({ + strategies: strategies, + shares: shares, + depositor: staker, + withdrawerAndNonce: withdrawerAndNonce, + withdrawalStartBlock: uint32(block.number), + delegatedAddress: delegatedAddress + }); + + } + + // calculate the withdrawal root + bytes32 withdrawalRoot = calculateWithdrawalRoot(queuedWithdrawal); + + // mark withdrawal as pending + withdrawalRootPending[withdrawalRoot] = true; + + // If the `staker` has withdrawn all of their funds from EigenLayer in this transaction, then they can choose to also undelegate + /** + * Checking that `stakerStrategyList[staker].length == 0` is not strictly necessary here, but prevents reverting very late in logic, + * in the case that 'undelegate' is set to true but the `staker` still has active deposits in EigenLayer. + */ + if (undelegateIfPossible && stakerStrategyList[staker].length == 0) { + _undelegate(staker); + } + + emit WithdrawalQueued(staker, nonce, withdrawer, delegatedAddress, withdrawalRoot); + + return withdrawalRoot; + + } + + /** * @notice Internal function for completing the given `queuedWithdrawal`. * @param queuedWithdrawal The QueuedWithdrawal to complete @@ -793,7 +830,9 @@ contract StrategyManager is * @param receiveAsTokens If marked 'true', then calls will be passed on to the `Strategy.withdraw` function for each strategy. * If marked 'false', then the shares will simply be internally transferred to the `msg.sender`. */ - function _completeQueuedWithdrawal(QueuedWithdrawal calldata queuedWithdrawal, IERC20[] calldata tokens, uint256 middlewareTimesIndex, bool receiveAsTokens) onlyNotFrozen(queuedWithdrawal.delegatedAddress) internal { + function _completeQueuedWithdrawal(QueuedWithdrawal calldata queuedWithdrawal, IERC20[] calldata tokens, uint256 middlewareTimesIndex, bool receiveAsTokens) + onlyNotFrozen(queuedWithdrawal.delegatedAddress) internal + { // find the withdrawalRoot bytes32 withdrawalRoot = calculateWithdrawalRoot(queuedWithdrawal); @@ -910,7 +949,6 @@ contract StrategyManager is } // VIEW FUNCTIONS - /** * @notice Get all details on the depositor's deposits and corresponding shares * @param depositor The staker of interest, whose deposits this function will fetch @@ -934,6 +972,19 @@ contract StrategyManager is return stakerStrategyList[staker].length; } + /** + * @notice Getter function for the current EIP-712 domain separator for this contract. + * @dev The domain separator will change in the event of a fork that changes the ChainID. + */ + function domainSeparator() public view returns (bytes32) { + if (block.chainid == ORIGINAL_CHAIN_ID) { + return _DOMAIN_SEPARATOR; + } + else { + return _calculateDomainSeparator(); + } + } + /// @notice Returns the keccak256 hash of `queuedWithdrawal`. function calculateWithdrawalRoot(QueuedWithdrawal memory queuedWithdrawal) public pure returns (bytes32) { return ( @@ -949,4 +1000,9 @@ contract StrategyManager is ) ); } + + // @notice Internal function for calculating the current domain separator of this contract + function _calculateDomainSeparator() internal view returns (bytes32) { + return keccak256(abi.encode(DOMAIN_TYPEHASH, keccak256(bytes("EigenLayer")), block.chainid, address(this))); + } } diff --git a/src/contracts/core/StrategyManagerStorage.sol b/src/contracts/core/StrategyManagerStorage.sol index ce807d6e1..fad5d799f 100644 --- a/src/contracts/core/StrategyManagerStorage.sol +++ b/src/contracts/core/StrategyManagerStorage.sol @@ -20,8 +20,12 @@ abstract contract StrategyManagerStorage is IStrategyManager { /// @notice The EIP-712 typehash for the deposit struct used by the contract bytes32 public constant DEPOSIT_TYPEHASH = keccak256("Deposit(address strategy,address token,uint256 amount,uint256 nonce,uint256 expiry)"); - /// @notice EIP-712 Domain separator - bytes32 public DOMAIN_SEPARATOR; + /** + * @notice Original EIP-712 Domain separator for this contract. + * @dev The domain separator may change in the event of a fork that modifies the ChainID. + * Use the getter function `domainSeparator` to get the current domain separator for this contract. + */ + bytes32 internal _DOMAIN_SEPARATOR; // staker => number of signed deposit nonce (used in depositIntoStrategyWithSignature) mapping(address => uint256) public nonces; diff --git a/src/contracts/interfaces/IBLSRegistryCoordinatorWithIndices.sol b/src/contracts/interfaces/IBLSRegistryCoordinatorWithIndices.sol index f5cfc79dc..7d6e64cbd 100644 --- a/src/contracts/interfaces/IBLSRegistryCoordinatorWithIndices.sol +++ b/src/contracts/interfaces/IBLSRegistryCoordinatorWithIndices.sol @@ -39,8 +39,6 @@ interface IBLSRegistryCoordinatorWithIndices is IRegistryCoordinator { // EVENTS - event OperatorSocketUpdate(bytes32 operatorId, string socket); - event OperatorSetParamsUpdated(uint8 indexed quorumNumber, OperatorSetParam operatorSetParams); /// @notice Returns the operator set params for the given `quorumNumber` diff --git a/src/contracts/interfaces/IBLSSignatureChecker.sol b/src/contracts/interfaces/IBLSSignatureChecker.sol new file mode 100644 index 000000000..52b4e4273 --- /dev/null +++ b/src/contracts/interfaces/IBLSSignatureChecker.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity =0.8.12; + +import "../interfaces/IBLSRegistryCoordinatorWithIndices.sol"; +import "../libraries/MiddlewareUtils.sol"; +import "../libraries/BN254.sol"; +import "../libraries/BitmapUtils.sol"; + +/** + * @title Used for checking BLS aggregate signatures from the operators of a EigenLayer AVS with the RegistryCoordinator/BLSPubkeyRegistry/StakeRegistry architechture. + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + * @notice This is the contract for checking the validity of aggregate operator signatures. + */ +interface IBLSSignatureChecker { + // DATA STRUCTURES + + struct NonSignerStakesAndSignature { + uint32[] nonSignerQuorumBitmapIndices; + BN254.G1Point[] nonSignerPubkeys; + BN254.G1Point[] quorumApks; + BN254.G2Point apkG2; + BN254.G1Point sigma; + uint32[] quorumApkIndices; + uint32[] totalStakeIndices; + uint32[][] nonSignerStakeIndices; // nonSignerStakeIndices[quorumNumberIndex][nonSignerIndex] + } + + /** + * @notice this data structure is used for recording the details on the total stake of the registered + * operators and those operators who are part of the quorum for a particular taskNumber + */ + + struct QuorumStakeTotals { + // total stake of the operators in each quorum + uint96[] signedStakeForQuorum; + // total amount staked by all operators in each quorum + uint96[] totalStakeForQuorum; + } + + // CONSTANTS & IMMUTABLES + + function registryCoordinator() external view returns (IRegistryCoordinator); + function stakeRegistry() external view returns (IStakeRegistry); + function blsPubkeyRegistry() external view returns (IBLSPubkeyRegistry); + + /** + * @notice This function is called by disperser when it has aggregated all the signatures of the operators + * that are part of the quorum for a particular taskNumber and is asserting them into onchain. The function + * checks that the claim for aggregated signatures are valid. + * + * The thesis of this procedure entails: + * - getting the aggregated pubkey of all registered nodes at the time of pre-commit by the + * disperser (represented by apk in the parameters), + * - subtracting the pubkeys of all the signers not in the quorum (nonSignerPubkeys) and storing + * the output in apk to get aggregated pubkey of all operators that are part of quorum. + * - use this aggregated pubkey to verify the aggregated signature under BLS scheme. + * + * @dev Before signature verification, the function verifies operator stake information. This includes ensuring that the provided `referenceBlockNumber` + * is correct, i.e., ensure that the stake returned from the specified block number is recent enough and that the stake is either the most recent update + * for the total stake (or the operator) or latest before the referenceBlockNumber. + */ + function checkSignatures( + bytes32 msgHash, + bytes calldata quorumNumbers, + uint32 referenceBlockNumber, + NonSignerStakesAndSignature memory nonSignerStakesAndSignature + ) + external + view + returns ( + QuorumStakeTotals memory, + bytes32 + ); +} \ No newline at end of file diff --git a/src/contracts/interfaces/IDelegationManager.sol b/src/contracts/interfaces/IDelegationManager.sol index 2992beb1b..6802a9e34 100644 --- a/src/contracts/interfaces/IDelegationManager.sol +++ b/src/contracts/interfaces/IDelegationManager.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity >=0.5.0; -import "./IDelegationTerms.sol"; +import "./IStrategy.sol"; /** * @title The interface for the primary delegation contract for EigenLayer. @@ -9,73 +9,283 @@ import "./IDelegationTerms.sol"; * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service * @notice This is the contract for delegation in EigenLayer. The main functionalities of this contract are * - enabling anyone to register as an operator in EigenLayer - * - allowing new operators to provide a DelegationTerms-type contract, which may mediate their interactions with stakers who delegate to them - * - enabling any staker to delegate its stake to the operator of its choice - * - enabling a staker to undelegate its assets from an operator (performed as part of the withdrawal process, initiated through the StrategyManager) + * - allowing operators to specify parameters related to stakers who delegate to them + * - enabling any staker to delegate its stake to the operator of its choice (a given staker can only delegate to a single operator at a time) + * - enabling a staker to undelegate its assets from the operator it is delegated to (performed as part of the withdrawal process, initiated through the StrategyManager) */ interface IDelegationManager { + // @notice Struct used for storing information about a single operator who has registered with EigenLayer + struct OperatorDetails { + // @notice address to receive the rewards that the operator earns via serving applications built on EigenLayer. + address earningsReceiver; + /** + * @notice Address to verify signatures when a staker wishes to delegate to the operator, as well as controlling "forced undelegations". + * @dev Signature verification follows these rules: + * 1) If this address is left as address(0), then any staker will be free to delegate to the operator, i.e. no signature verification will be performed. + * 2) If this address is an EOA (i.e. it has no code), then we follow standard ECDSA signature verification for delegations to the operator. + * 3) If this address is a contract (i.e. it has code) then we forward a call to the contract and verify that it returns the correct EIP-1271 "magic value". + */ + address delegationApprover; + /** + * @notice A minimum delay -- measured in blocks -- enforced between: + * 1) the operator signalling their intent to register for a service, via calling `Slasher.optIntoSlashing` + * and + * 2) the operator completing registration for the service, via the service ultimately calling `Slasher.recordFirstStakeUpdate` + * @dev note that for a specific operator, this value *cannot decrease*, i.e. if the operator wishes to modify their OperatorDetails, + * then they are only allowed to either increase this value or keep it the same. + */ + uint32 stakerOptOutWindowBlocks; + } /** - * @notice This will be called by an operator to register itself as an operator that stakers can choose to delegate to. - * @param dt is the `DelegationTerms` contract that the operator has for those who delegate to them. - * @dev An operator can set `dt` equal to their own address (or another EOA address), in the event that they want to split payments - * in a more 'trustful' manner. - * @dev In the present design, once set, there is no way for an operator to ever modify the address of their DelegationTerms contract. + * @notice Abstract struct used in calculating an EIP712 signature for a staker to approve that they (the staker themselves) delegate to a specific operator. + * @dev Used in computing the `STAKER_DELEGATION_TYPEHASH` and as a reference in the computation of the stakerDigestHash in the `delegateToBySignature` function. */ - function registerAsOperator(IDelegationTerms dt) external; + struct StakerDelegation { + // the staker who is delegating + address staker; + // the operator being delegated to + address operator; + // the staker's nonce + uint256 nonce; + // the expiration timestamp (UTC) of the signature + uint256 expiry; + } /** - * @notice This will be called by a staker to delegate its assets to some operator. - * @param operator is the operator to whom staker (msg.sender) is delegating its assets + * @notice Abstract struct used in calculating an EIP712 signature for an operator's delegationApprover to approve that a specific staker delegate to the operator. + * @dev Used in computing the `DELEGATION_APPROVAL_TYPEHASH` and as a reference in the computation of the approverDigestHash in the `_delegate` function. */ - function delegateTo(address operator) external; + struct DelegationApproval { + // the staker who is delegating + address staker; + // the operator being delegated to + address operator; + // the operator's nonce + uint256 nonce; + // the expiration timestamp (UTC) of the signature + uint256 expiry; + } + + // @notice Struct that bundles together a signature and an expiration time for the signature. Used primarily for stack management. + struct SignatureWithExpiry { + // the signature itself, formatted as a single bytes object + bytes signature; + // the expiration timestamp (UTC) of the signature + uint256 expiry; + } + + // @notice Emitted when a new operator registers in EigenLayer and provides their OperatorDetails. + event OperatorRegistered(address indexed operator, OperatorDetails operatorDetails); + + // @notice Emitted when an operator updates their OperatorDetails to @param newOperatorDetails + event OperatorDetailsModified(address indexed operator, OperatorDetails newOperatorDetails); + + /** + * @notice Emitted when @param operator indicates that they are updating their MetadataURI string + * @dev Note that these strings are *never stored in storage* and are instead purely emitted in events for off-chain indexing + */ + event OperatorMetadataURIUpdated(address indexed operator, string metadataURI); + + // @notice Emitted when @param staker delegates to @param operator. + event StakerDelegated(address indexed staker, address indexed operator); + + // @notice Emitted when @param staker undelegates from @param operator. + event StakerUndelegated(address indexed staker, address indexed operator); + + /** + * @notice Registers the `msg.sender` as an operator in EigenLayer, that stakers can choose to delegate to. + * @param registeringOperatorDetails is the `OperatorDetails` for the operator. + * @param metadataURI is a URI for the operator's metadata, i.e. a link providing more details on the operator. + * @dev Note that once an operator is registered, they cannot 'deregister' as an operator, and they will forever be considered "delegated to themself". + * @dev This function will revert if the caller attempts to set their `earningsReceiver` to address(0). + * @dev Note that the `metadataURI` is *never stored in storage* and is instead purely emitted in an `OperatorMetadataURIUpdated` event + */ + function registerAsOperator(OperatorDetails calldata registeringOperatorDetails, string calldata metadataURI) external; + + /** + * @notice Updates the `msg.sender`'s stored `OperatorDetails`. + * @param newOperatorDetails is the updated `OperatorDetails` for the operator, to replace their current OperatorDetails`. + * @dev The `msg.sender` must have previously registered as an operator in EigenLayer via calling the `registerAsOperator` function. + * @dev This function will revert if the caller attempts to set their `earningsReceiver` to address(0). + */ + function modifyOperatorDetails(OperatorDetails calldata newOperatorDetails) external; + + /** + * @notice Called by an operator to emit an `OperatorMetadataURIUpdated` event, signalling that information about the operator (or at least where this + * information is stored) has changed. + * @param metadataURI is the new metadata URI for the `msg.sender`, i.e. the operator. + * @dev This function will revert if the caller is not an operator. + */ + function updateOperatorMetadataURI(string calldata metadataURI) external; + + /** + * @notice Called by a staker to delegate its assets to the @param operator. + * @param operator is the operator to whom the staker (`msg.sender`) is delegating its assets for use in serving applications built on EigenLayer. + * @param approverSignatureAndExpiry is a parameter that will be used for verifying that the operator approves of this delegation action in the event that: + * 1) the operator's `delegationApprover` address is set to a non-zero value. + * AND + * 2) neither the operator nor their `delegationApprover` is the `msg.sender`, since in the event that the operator or their delegationApprover + * is the `msg.sender`, then approval is assumed. + * @dev In the event that `approverSignatureAndExpiry` is not checked, its content is ignored entirely; it's recommended to use an empty input + * in this case to save on complexity + gas costs + */ + function delegateTo(address operator, SignatureWithExpiry memory approverSignatureAndExpiry) external; /** - * @notice Delegates from `staker` to `operator`. - * @dev requires that: - * 1) if `staker` is an EOA, then `signature` is valid ECDSA signature from `staker`, indicating their intention for this action - * 2) if `staker` is a contract, then `signature` must will be checked according to EIP-1271 + * @notice Delegates from @param staker to @param operator. + * @notice This function will revert if the current `block.timestamp` is equal to or exceeds @param expiry + * @dev The @param stakerSignature is used as follows: + * 1) If `staker` is an EOA, then `stakerSignature` is verified to be a valid ECDSA stakerSignature from `staker`, indicating their intention for this action. + * 2) If `staker` is a contract, then `stakerSignature` will be checked according to EIP-1271. + * @param approverSignatureAndExpiry is a parameter that will be used for verifying that the operator approves of this delegation action in the event that: + * 1) the operator's `delegationApprover` address is set to a non-zero value. + * AND + * 2) neither the operator nor their `delegationApprover` is the `msg.sender`, since in the event that the operator or their delegationApprover + * is the `msg.sender`, then approval is assumed. + * @dev In the event that `approverSignatureAndExpiry` is not checked, its content is ignored entirely; it's recommended to use an empty input + * in this case to save on complexity + gas costs */ - function delegateToBySignature(address staker, address operator, uint256 expiry, bytes memory signature) external; + function delegateToBySignature( + address staker, + address operator, + SignatureWithExpiry memory stakerSignatureAndExpiry, + SignatureWithExpiry memory approverSignatureAndExpiry + ) external; /** * @notice Undelegates `staker` from the operator who they are delegated to. - * @notice Callable only by the StrategyManager + * @notice Callable only by the StrategyManager. * @dev Should only ever be called in the event that the `staker` has no active deposits in EigenLayer. + * @dev Reverts if the `staker` is also an operator, since operators are not allowed to undelegate from themselves. + * @dev Does nothing (but should not revert) if the staker is already undelegated. */ function undelegate(address staker) external; - /// @notice returns the address of the operator that `staker` is delegated to. + /** + * @notice Called by the operator or the operator's `delegationApprover` address, in order to forcibly undelegate a staker who is currently delegated to the operator. + * @param staker The staker to be force-undelegated. + * @dev This function will revert if the `msg.sender` is not the operator who the staker is delegated to, nor the operator's specified "delegationApprover" + * @dev This function will also revert if the `staker` is themeselves an operator; operators are considered *permanently* delegated to themselves. + * @return The root of the newly queued withdrawal. + * @dev Note that it is assumed that a staker places some trust in an operator, in paricular for the operator to not get slashed; a malicious operator can use this function + * to inconvenience a staker who is delegated to them, but the expectation is that the inconvenience is minor compared to the operator getting purposefully slashed. + */ + function forceUndelegation(address staker) external returns (bytes32); + + /** + * @notice *If the staker is actively delegated*, then increases the `staker`'s delegated shares in `strategy` by `shares`. Otherwise does nothing. + * Called by the StrategyManager whenever new shares are added to a user's share balance. + * @dev Callable only by the StrategyManager. + */ + function increaseDelegatedShares(address staker, IStrategy strategy, uint256 shares) external; + + /** + * @notice *If the staker is actively delegated*, then decreases the `staker`'s delegated shares in each entry of `strategies` by its respective `shares[i]`. Otherwise does nothing. + * Called by the StrategyManager whenever shares are decremented from a user's share balance, for example when a new withdrawal is queued. + * @dev Callable only by the StrategyManager. + */ + function decreaseDelegatedShares(address staker, IStrategy[] calldata strategies, uint256[] calldata shares) external; + + /** + * @notice returns the address of the operator that `staker` is delegated to. + * @notice Mapping: staker => operator whom the staker is currently delegated to. + * @dev Note that returning address(0) indicates that the staker is not actively delegated to any operator. + */ function delegatedTo(address staker) external view returns (address); - /// @notice returns the DelegationTerms of the `operator`, which may mediate their interactions with stakers who delegate to them. - function delegationTerms(address operator) external view returns (IDelegationTerms); + /** + * @notice returns the OperatorDetails of the `operator`. + * @notice Mapping: operator => OperatorDetails struct + */ + function operatorDetails(address operator) external view returns (OperatorDetails memory); + + // @notice Getter function for `_operatorDetails[operator].earningsReceiver` + function earningsReceiver(address operator) external view returns (address); + + // @notice Getter function for `_operatorDetails[operator].delegationApprover` + function delegationApprover(address operator) external view returns (address); - /// @notice returns the total number of shares in `strategy` that are delegated to `operator`. + // @notice Getter function for `_operatorDetails[operator].stakerOptOutWindowBlocks` + function stakerOptOutWindowBlocks(address operator) external view returns (uint256); + + /** + * @notice returns the total number of shares in `strategy` that are delegated to `operator`. + * @notice Mapping: operator => strategy => total number of shares in the strategy delegated to the operator. + */ function operatorShares(address operator, IStrategy strategy) external view returns (uint256); + /// @notice Returns 'true' if `staker` *is* actively delegated, and 'false' otherwise. + function isDelegated(address staker) external view returns (bool); + + /// @notice Returns if an operator can be delegated to, i.e. the `operator` has previously called `registerAsOperator`. + function isOperator(address operator) external view returns (bool); + + /// @notice Mapping: staker => number of signed delegation nonces (used in `delegateToBySignature`) from the staker that the contract has already checked + function stakerNonce(address staker) external view returns (uint256); + /** - * @notice Increases the `staker`'s delegated shares in `strategy` by `shares, typically called when the staker has further deposits into EigenLayer - * @dev Callable only by the StrategyManager + * @notice Mapping: delegationApprover => number of signed delegation messages (used in `delegateTo` and `delegateToBySignature` from the delegationApprover + * that this contract has already checked. + * @dev Note that these functions only delegationApprover signatures if the operator being delegated to has specified a nonzero address as their `delegationApprover` */ - function increaseDelegatedShares(address staker, IStrategy strategy, uint256 shares) external; + function delegationApproverNonce(address delegationApprover) external view returns (uint256); /** - * @notice Decreases the `staker`'s delegated shares in each entry of `strategies` by its respective `shares[i]`, typically called when the staker withdraws from EigenLayer - * @dev Callable only by the StrategyManager + * @notice External function that calculates the digestHash for a `staker` to sign in order to approve their delegation to an `operator`, + * using the staker's current nonce and specifying an expiration of `expiry` + * @param staker The signing staker + * @param operator The operator who is being delegated to + * @param expiry The desired expiry time of the staker's signature */ - function decreaseDelegatedShares( + function calculateCurrentStakerDelegationDigestHash(address staker, address operator, uint256 expiry) external view returns (bytes32); + + /** + * @notice Public function for the staker signature hash calculation in the `delegateToBySignature` function + * @param staker The signing staker + * @param stakerNonce The nonce of the staker. In practice we use the staker's current nonce, stored at `stakerNonce[staker]` + * @param operator The operator who is being delegated to + * @param expiry The desired expiry time of the staker's signature + */ + function calculateStakerDelegationDigestHash(address staker, uint256 stakerNonce, address operator, uint256 expiry) external view returns (bytes32); + + /** + * @notice Exneral function that calculates the digestHash for the `operator`'s "delegationApprover" to sign in order to approve the + * delegation of `staker` to the `operator`, using the approver's current nonce and specifying an expiration of `expiry` + * @param staker The staker who is delegating to the operator + * @param operator The operator who is being delegated to + * @param expiry The desired expiry time of the approver's signature + */ + function calculateCurrentDelegationApprovalDigestHash(address staker, address operator, uint256 expiry) external view returns (bytes32); + + /** + * @notice Public function for the the approver signature hash calculation in the `_delegate` function + * @param staker The staker who is delegating to the operator + * @param operator The operator who is being delegated to + * @param _delegationApprover the operator's `delegationApprover` who will be signing the delegationHash (in general) + * @param approverNonce The nonce of the approver. In practice we use the approver's current nonce, stored at `delegationApproverNonce[_delegationApprover]` + * @param expiry The desired expiry time of the approver's signature + */ + function calculateDelegationApprovalDigestHash( address staker, - IStrategy[] calldata strategies, - uint256[] calldata shares - ) external; + address operator, + address _delegationApprover, + uint256 approverNonce, + uint256 expiry + ) external view returns (bytes32); - /// @notice Returns 'true' if `staker` *is* actively delegated, and 'false' otherwise. - function isDelegated(address staker) external view returns (bool); + /// @notice The EIP-712 typehash for the contract's domain + function DOMAIN_TYPEHASH() external view returns (bytes32); - /// @notice Returns 'true' if `staker` is *not* actively delegated, and 'false' otherwise. - function isNotDelegated(address staker) external view returns (bool); + /// @notice The EIP-712 typehash for the StakerDelegation struct used by the contract + function STAKER_DELEGATION_TYPEHASH() external view returns (bytes32); - /// @notice Returns if an operator can be delegated to, i.e. it has called `registerAsOperator`. - function isOperator(address operator) external view returns (bool); -} + /// @notice The EIP-712 typehash for the DelegationApproval struct used by the contract + function DELEGATION_APPROVAL_TYPEHASH() external view returns (bytes32); + + /** + * @notice Getter function for the current EIP-712 domain separator for this contract. + * @dev The domain separator will change in the event of a fork that changes the ChainID. + */ + function domainSeparator() external view returns (bytes32); +} \ No newline at end of file diff --git a/src/contracts/interfaces/IDelegationTerms.sol b/src/contracts/interfaces/IDelegationTerms.sol deleted file mode 100644 index 6b70c7784..000000000 --- a/src/contracts/interfaces/IDelegationTerms.sol +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity >=0.5.0; - -import "./IStrategy.sol"; - -/** - * @title Abstract interface for a contract that helps structure the delegation relationship. - * @author Layr Labs, Inc. - * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service - * @notice The gas budget provided to this contract in calls from EigenLayer contracts is limited. - */ -interface IDelegationTerms { - function payForService(IERC20 token, uint256 amount) external payable; - - function onDelegationWithdrawn( - address delegator, - IStrategy[] memory stakerStrategyList, - uint256[] memory stakerShares - ) external returns(bytes memory); - - function onDelegationReceived( - address delegator, - IStrategy[] memory stakerStrategyList, - uint256[] memory stakerShares - ) external returns(bytes memory); -} diff --git a/src/contracts/interfaces/IPaymentManager.sol b/src/contracts/interfaces/IPaymentManager.sol deleted file mode 100644 index a1cb4cee0..000000000 --- a/src/contracts/interfaces/IPaymentManager.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity >=0.5.0; - -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -/** - * @title Interface for a `PaymentManager` contract. - * @author Layr Labs, Inc. - * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service - */ -interface IPaymentManager { - /** - * @notice deposit one-time fees by the `msg.sender` with this contract to pay for future tasks of this middleware - * @param depositFor could be the `msg.sender` themselves, or a different address for whom `msg.sender` is depositing these future fees - * @param amount is amount of futures fees being deposited - */ - function depositFutureFees(address depositFor, uint256 amount) external; - - /// @notice Allows the `allowed` address to spend up to `amount` of the `msg.sender`'s funds that have been deposited in this contract - function setAllowance(address allowed, uint256 amount) external; - - /// @notice Used for deducting the fees from the payer to the middleware - function takeFee(address initiator, address payer, uint256 feeAmount) external; - - /// @notice the ERC20 token that will be used by the disperser to pay the service fees to middleware nodes. - function paymentToken() external view returns (IERC20); -} diff --git a/src/contracts/interfaces/ISlasher.sol b/src/contracts/interfaces/ISlasher.sol index c15b2ee47..f4cb91417 100644 --- a/src/contracts/interfaces/ISlasher.sol +++ b/src/contracts/interfaces/ISlasher.sol @@ -18,6 +18,8 @@ interface ISlasher { // struct used to store details relevant to a single middleware that an operator has opted-in to serving struct MiddlewareDetails { + // the block at which the contract begins being able to finalize the operator's registration with the service via calling `recordFirstStakeUpdate` + uint32 registrationMayBeginAtBlock; // the block before which the contract is allowed to slash the user uint32 contractCanSlashOperatorUntilBlock; // the block at which the middleware's view of the operator's stake was most recently updated diff --git a/src/contracts/interfaces/ISocketUpdater.sol b/src/contracts/interfaces/ISocketUpdater.sol new file mode 100644 index 000000000..500633c41 --- /dev/null +++ b/src/contracts/interfaces/ISocketUpdater.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity =0.8.12; + +import "./IRegistryCoordinator.sol"; +import "./IStakeRegistry.sol"; +import "./IBLSPubkeyRegistry.sol"; +import "./IIndexRegistry.sol"; + +/** + * @title Interface for an `ISocketUpdater` where operators can update their sockets. + * @author Layr Labs, Inc. + */ +interface ISocketUpdater { + // EVENTS + + event OperatorSocketUpdate(bytes32 indexed operatorId, string socket); + + // FUNCTIONS + + /** + * @notice Updates the socket of the msg.sender given they are a registered operator + * @param socket is the new socket of the operator + */ + function updateSocket(string memory socket) external; +} \ No newline at end of file diff --git a/src/contracts/interfaces/IStrategyManager.sol b/src/contracts/interfaces/IStrategyManager.sol index 48b1c623b..edde538fa 100644 --- a/src/contracts/interfaces/IStrategyManager.sol +++ b/src/contracts/interfaces/IStrategyManager.sol @@ -223,14 +223,20 @@ interface IStrategyManager { function slashQueuedWithdrawal(address recipient, QueuedWithdrawal calldata queuedWithdrawal, IERC20[] calldata tokens, uint256[] calldata indicesToSkip) external; - /// @notice Returns the keccak256 hash of `queuedWithdrawal`. - function calculateWithdrawalRoot( - QueuedWithdrawal memory queuedWithdrawal - ) - external - pure - returns (bytes32); + /** + * @notice Called by a staker to undelegate entirely from EigenLayer. The staker must first withdraw all of their existing deposits + * (through use of the `queueWithdrawal` function), or else otherwise have never deposited in EigenLayer prior to delegating. + */ + function undelegate() external; + /** + * @notice Called by the DelegationManager as part of the forced undelegation of the @param staker from their delegated operator. + * This function queues a withdrawal of all of the `staker`'s shares in EigenLayer to the staker themself, and then undelegates the staker. + * The staker will consequently be able to complete this withdrawal by calling the `completeQueuedWithdrawal` function. + * @param staker The staker to force-undelegate. + * @return The root of the newly queued withdrawal. + */ + function forceTotalWithdrawal(address staker) external returns (bytes32); /** * @notice Owner-only function that adds the provided Strategies to the 'whitelist' of strategies that stakers can deposit into * @param strategiesToWhitelist Strategies that will be added to the `strategyIsWhitelistedForDeposit` mapping (if they aren't in it already) @@ -243,6 +249,14 @@ interface IStrategyManager { */ function removeStrategiesFromDepositWhitelist(IStrategy[] calldata strategiesToRemoveFromWhitelist) external; + /// @notice Returns the keccak256 hash of `queuedWithdrawal`. + function calculateWithdrawalRoot( + QueuedWithdrawal memory queuedWithdrawal + ) + external + pure + returns (bytes32); + /// @notice Returns the single, central Delegation contract of EigenLayer function delegation() external view returns (IDelegationManager); diff --git a/src/contracts/libraries/EIP1271SignatureUtils.sol b/src/contracts/libraries/EIP1271SignatureUtils.sol new file mode 100644 index 000000000..21166cca4 --- /dev/null +++ b/src/contracts/libraries/EIP1271SignatureUtils.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity =0.8.12; + +import "@openzeppelin/contracts/interfaces/IERC1271.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; + +/** + * @title Library of utilities for making EIP1271-compliant signature checks. + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + */ +library EIP1271SignatureUtils { + // bytes4(keccak256("isValidSignature(bytes32,bytes)") + bytes4 internal constant EIP1271_MAGICVALUE = 0x1626ba7e; + + /** + * @notice Checks @param signature is a valid signature of @param digestHash from @param signer. + * If the `signer` contains no code -- i.e. it is not (yet, at least) a contract address, then checks using standard ECDSA logic + * Otherwise, passes on the signature to the signer to verify the signature and checks that it returns the `EIP1271_MAGICVALUE`. + */ + function checkSignature_EIP1271(address signer, bytes32 digestHash, bytes memory signature) internal view { + /** + * check validity of signature: + * 1) if `signer` is an EOA, then `signature` must be a valid ECDSA signature from `signer`, + * indicating their intention for this action + * 2) if `signer` is a contract, then `signature` must will be checked according to EIP-1271 + */ + if (Address.isContract(signer)) { + require(IERC1271(signer).isValidSignature(digestHash, signature) == EIP1271_MAGICVALUE, + "EIP1271SignatureUtils.checkSignature_EIP1271: ERC1271 signature verification failed"); + } else { + require(ECDSA.recover(digestHash, signature) == signer, + "EIP1271SignatureUtils.checkSignature_EIP1271: signature not from signer"); + } + } +} diff --git a/src/contracts/middleware/BLSOperatorStateRetriever.sol b/src/contracts/middleware/BLSOperatorStateRetriever.sol index 36b3001a8..3cc8d87a8 100644 --- a/src/contracts/middleware/BLSOperatorStateRetriever.sol +++ b/src/contracts/middleware/BLSOperatorStateRetriever.sol @@ -100,16 +100,19 @@ contract BLSOperatorStateRetriever { IStakeRegistry stakeRegistry = registryCoordinator.stakeRegistry(); CheckSignaturesIndices memory checkSignaturesIndices; + // get the indices of the quorumBitmap updates for each of the operators in the nonSignerOperatorIds array checkSignaturesIndices.nonSignerQuorumBitmapIndices = registryCoordinator.getQuorumBitmapIndicesByOperatorIdsAtBlockNumber(referenceBlockNumber, nonSignerOperatorIds); + // get the indices of the totalStake updates for each of the quorums in the quorumNumbers array checkSignaturesIndices.totalStakeIndices = stakeRegistry.getTotalStakeIndicesByQuorumNumbersAtBlockNumber(referenceBlockNumber, quorumNumbers); checkSignaturesIndices.nonSignerStakeIndices = new uint32[][](quorumNumbers.length); for (uint8 quorumNumberIndex = 0; quorumNumberIndex < quorumNumbers.length; quorumNumberIndex++) { uint256 numNonSignersForQuorum = 0; - // this array's length will be at most the number of nonSignerOperatorIds + // this array's length will be at most the number of nonSignerOperatorIds, this will be trimmed after it is filled checkSignaturesIndices.nonSignerStakeIndices[quorumNumberIndex] = new uint32[](nonSignerOperatorIds.length); for (uint i = 0; i < nonSignerOperatorIds.length; i++) { + // get the quorumBitmap for the operator at the given blocknumber and index uint192 nonSignerQuorumBitmap = registryCoordinator.getQuorumBitmapByOperatorIdAtBlockNumberByIndex( nonSignerOperatorIds[i], @@ -118,7 +121,8 @@ contract BLSOperatorStateRetriever { ); // if the operator was a part of the quorum and the quorum is a part of the provided quorumNumbers - if (nonSignerQuorumBitmap >> uint8(quorumNumbers[quorumNumberIndex]) & 1 == 1) { + if ((nonSignerQuorumBitmap >> uint8(quorumNumbers[quorumNumberIndex])) & 1 == 1) { + // get the index of the stake update for the operator at the given blocknumber and quorum number checkSignaturesIndices.nonSignerStakeIndices[quorumNumberIndex][numNonSignersForQuorum] = stakeRegistry.getStakeUpdateIndexForOperatorIdForQuorumAtBlockNumber( nonSignerOperatorIds[i], uint8(quorumNumbers[quorumNumberIndex]), @@ -137,6 +141,7 @@ contract BLSOperatorStateRetriever { } IBLSPubkeyRegistry blsPubkeyRegistry = registryCoordinator.blsPubkeyRegistry(); + // get the indices of the quorum apks for each of the provided quorums at the given blocknumber checkSignaturesIndices.quorumApkIndices = blsPubkeyRegistry.getApkIndicesForQuorumsAtBlockNumber(quorumNumbers, referenceBlockNumber); return checkSignaturesIndices; diff --git a/src/contracts/middleware/BLSPubkeyRegistry.sol b/src/contracts/middleware/BLSPubkeyRegistry.sol index a7df73be0..aefb4b7ac 100644 --- a/src/contracts/middleware/BLSPubkeyRegistry.sol +++ b/src/contracts/middleware/BLSPubkeyRegistry.sol @@ -8,10 +8,7 @@ import "../interfaces/IBLSPublicKeyCompendium.sol"; import "../libraries/BN254.sol"; -import "forge-std/Test.sol"; - - -contract BLSPubkeyRegistry is IBLSPubkeyRegistry, Test { +contract BLSPubkeyRegistry is IBLSPubkeyRegistry { using BN254 for BN254.G1Point; /// @notice the hash of the zero pubkey aka BN254.G1Point(0,0) diff --git a/src/contracts/middleware/BLSRegistryCoordinatorWithIndices.sol b/src/contracts/middleware/BLSRegistryCoordinatorWithIndices.sol index 5493ac422..089a78b42 100644 --- a/src/contracts/middleware/BLSRegistryCoordinatorWithIndices.sol +++ b/src/contracts/middleware/BLSRegistryCoordinatorWithIndices.sol @@ -4,6 +4,7 @@ pragma solidity =0.8.12; import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; import "../interfaces/IBLSRegistryCoordinatorWithIndices.sol"; +import "../interfaces/ISocketUpdater.sol"; import "../interfaces/IServiceManager.sol"; import "../interfaces/IBLSPubkeyRegistry.sol"; import "../interfaces/IVoteWeigher.sol"; @@ -23,7 +24,7 @@ import "forge-std/Test.sol"; * * @author Layr Labs, Inc. */ -contract BLSRegistryCoordinatorWithIndices is Initializable, IBLSRegistryCoordinatorWithIndices, Test { +contract BLSRegistryCoordinatorWithIndices is Initializable, IBLSRegistryCoordinatorWithIndices, ISocketUpdater { using BN254 for BN254.G1Point; uint16 internal constant BIPS_DENOMINATOR = 10000; diff --git a/src/contracts/middleware/PaymentManager.sol b/src/contracts/middleware/PaymentManager.sol deleted file mode 100644 index 0a2e4ade9..000000000 --- a/src/contracts/middleware/PaymentManager.sol +++ /dev/null @@ -1,88 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity =0.8.12; - -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; -import "../interfaces/IServiceManager.sol"; -import "../interfaces/IDelegationManager.sol"; -import "../interfaces/IPaymentManager.sol"; -import "../permissions/Pausable.sol"; - -/** - * @title Controls middleware payments. - * @author Layr Labs, Inc. - * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service - */ -contract PaymentManager is Initializable, IPaymentManager, Pausable { - using SafeERC20 for IERC20; - - uint8 constant internal PAUSED_NEW_PAYMENT_COMMIT = 0; - uint8 constant internal PAUSED_REDEEM_PAYMENT = 1; - - // DATA STRUCTURES - - /// @notice The ServiceManager contract for this middleware, where tasks are created / initiated. - IServiceManager public immutable serviceManager; - - /// @notice the ERC20 token that will be used by the disperser to pay the service fees to middleware nodes. - IERC20 public immutable paymentToken; - - /// @notice Deposits of future fees to be drawn against when paying for service from the middleware - mapping(address => uint256) public depositsOf; - - /// @notice depositors => addresses approved to spend deposits => allowance - mapping(address => mapping(address => uint256)) public allowances; - - /// @notice when applied to a function, ensures that the function is only callable by the `serviceManager` - modifier onlyServiceManager() { - require(msg.sender == address(serviceManager), "onlyServiceManager"); - _; - } - - /// @notice when applied to a function, ensures that the function is only callable by the owner of the `serviceManager` - modifier onlyServiceManagerOwner() { - require(msg.sender == serviceManager.owner(), "onlyServiceManagerOwner"); - _; - } - - constructor( - IServiceManager _serviceManager, - IERC20 _paymentToken - ) { - serviceManager = _serviceManager; - paymentToken = _paymentToken; - _disableInitializers(); - } - - function initialize(IPauserRegistry _pauserReg) public initializer { - _initializePauser(_pauserReg, UNPAUSE_ALL); - } - - /** - * @notice deposit one-time fees by the `msg.sender` with this contract to pay for future tasks of this middleware - * @param depositFor could be the `msg.sender` themselves, or a different address for whom `msg.sender` is depositing these future fees - * @param amount is amount of futures fees being deposited - */ - function depositFutureFees(address depositFor, uint256 amount) external { - paymentToken.safeTransferFrom(msg.sender, address(this), amount); - depositsOf[depositFor] += amount; - } - - /// @notice Allows the `allowed` address to spend up to `amount` of the `msg.sender`'s funds that have been deposited in this contract - function setAllowance(address allowed, uint256 amount) external { - allowances[msg.sender][allowed] = amount; - } - - /// @notice Used for deducting the fees from the payer to the middleware - function takeFee(address initiator, address payer, uint256 feeAmount) external virtual onlyServiceManager { - if (initiator != payer) { - if (allowances[payer][initiator] != type(uint256).max) { - allowances[payer][initiator] -= feeAmount; - } - } - - // decrement `payer`'s stored deposits - depositsOf[payer] -= feeAmount; - } -} diff --git a/src/contracts/operators/MerkleDelegationTerms.sol b/src/contracts/operators/MerkleDelegationTerms.sol deleted file mode 100644 index ffa40586a..000000000 --- a/src/contracts/operators/MerkleDelegationTerms.sol +++ /dev/null @@ -1,154 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 - -pragma solidity =0.8.12; - -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts/access/Ownable.sol"; -import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; - -import "../interfaces/IDelegationTerms.sol"; -import "../libraries/Merkle.sol"; - -/** - * @title A 'Delegation Terms' contract that an operator can use to distribute earnings to stakers by periodically posting Merkle roots - * @author Layr Labs, Inc. - * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service - * @notice This contract specifies the delegation terms of a given operator. When a staker delegates its stake to an operator, - * it has to agrees to the terms set in the operator's 'Delegation Terms' contract. Payments to an operator are routed through - * their specified 'Delegation Terms' contract for subsequent distribution of earnings to individual stakers. - * There are also hooks that call into an operator's DelegationTerms contract when a staker delegates to or undelegates from - * the operator. - * @dev This contract uses a system in which the operator posts roots of a *sparse Merkle tree*. Each leaf of the tree is expected - * to contain the **cumulative** earnings of a staker. This will reduce the total number of actions that stakers who claim only rarely - * have to take, while allowing stakers to claim their earnings as often as new Merkle roots are posted. - */ -contract MerkleDelegationTerms is Ownable, IDelegationTerms { - using SafeERC20 for IERC20; - - struct TokenAndAmount { - IERC20 token; - uint256 amount; - } - - struct MerkleRootAndTreeHeight { - bytes32 root; - uint256 height; - } - - // sanity-check parameter on Merkle tree height - uint256 internal constant MAX_HEIGHT = 256; - - /// @notice staker => token => cumulative amount *claimed* - mapping(address => mapping(IERC20 => uint256)) public cumulativeClaimedByStakerOfToken; - - /// @notice Array of Merkle roots with heights, each posted by the operator (contract owner) - MerkleRootAndTreeHeight[] public merkleRoots; - - // TODO: more events? - event NewMerkleRootPosted(bytes32 newRoot, uint256 height); - - /** - * @notice Used by the operator to withdraw tokens directly from this contract. - * @param tokensAndAmounts ERC20 tokens to withdraw and the amount of each respective ERC20 token to withdraw. - */ - function operatorWithdrawal(TokenAndAmount[] calldata tokensAndAmounts) external onlyOwner { - uint256 tokensAndAmountsLength = tokensAndAmounts.length; - for (uint256 i; i < tokensAndAmountsLength;) { - tokensAndAmounts[i].token.safeTransfer(msg.sender, tokensAndAmounts[i].amount); - cumulativeClaimedByStakerOfToken[msg.sender][tokensAndAmounts[i].token] += tokensAndAmounts[i].amount; - unchecked { - ++i; - } - } - } - - /// @notice Used by the operator to post an updated root of the stakers' all-time earnings - function postMerkleRoot(bytes32 newRoot, uint256 height) external onlyOwner { - // sanity check - require(height <= MAX_HEIGHT, "MerkleDelegationTerms.postMerkleRoot: height input too large"); - merkleRoots.push( - MerkleRootAndTreeHeight({ - root: newRoot, - height: height - }) - ); - emit NewMerkleRootPosted(newRoot, height); - } - - /** - * @notice Called by a staker to prove the inclusion of their earnings in a Merkle root (posted by the operator) and claim them. - * @param tokensAndAmounts ERC20 tokens to withdraw and the amount of each respective ERC20 token to withdraw. - * @param proof Merkle proof showing that a leaf containing `(msg.sender, tokensAndAmounts)` was included in the `rootIndex`-th - * Merkle root posted by the operator. - * @param nodeIndex Specifies the node inside the Merkle tree corresponding to the specified root, `merkleRoots[rootIndex].root`. - * @param rootIndex Specifies the Merkle root to look up, using `merkleRoots[rootIndex]` - */ - function proveEarningsAndWithdraw( - TokenAndAmount[] calldata tokensAndAmounts, - bytes memory proof, - uint256 nodeIndex, - uint256 rootIndex - ) external { - // calculate the leaf that the `msg.sender` is claiming - bytes32 leafHash = calculateLeafHash(msg.sender, tokensAndAmounts); - - // verify that the proof length is appropriate for the chosen root - require(proof.length == 32 * merkleRoots[rootIndex].height, "MerkleDelegationTerms.proveEarningsAndWithdraw: incorrect proof length"); - - // check inclusion of the leafHash in the tree corresponding to `merkleRoots[rootIndex]` - require( - Merkle.verifyInclusionKeccak( - proof, - merkleRoots[rootIndex].root, - leafHash, - nodeIndex - ), - "MerkleDelegationTerms.proveEarningsAndWithdraw: proof of inclusion failed" - ); - - uint256 tokensAndAmountsLength = tokensAndAmounts.length; - for (uint256 i; i < tokensAndAmountsLength;) { - // calculate amount to send - uint256 amountToSend = tokensAndAmounts[i].amount - cumulativeClaimedByStakerOfToken[msg.sender][tokensAndAmounts[i].token]; - - if (amountToSend != 0) { - // update claimed amount in storage - cumulativeClaimedByStakerOfToken[msg.sender][tokensAndAmounts[i].token] = tokensAndAmounts[i].amount; - - // actually send the tokens - tokensAndAmounts[i].token.safeTransfer(msg.sender, amountToSend); - } - unchecked { - ++i; - } - } - } - - /// @notice Helper function for calculating a leaf in a Merkle tree formatted as `(address staker, TokenAndAmount[] calldata tokensAndAmounts)` - function calculateLeafHash(address staker, TokenAndAmount[] calldata tokensAndAmounts) public pure returns (bytes32) { - return keccak256(abi.encode(staker, tokensAndAmounts)); - } - - // FUNCTIONS FROM INTERFACE - function payForService(IERC20, uint256) external payable - // solhint-disable-next-line no-empty-blocks - {} - - /// @notice Hook for receiving new delegation - function onDelegationReceived( - address, - IStrategy[] memory, - uint256[] memory - ) external pure returns(bytes memory) - // solhint-disable-next-line no-empty-blocks - {} - - /// @notice Hook for withdrawing delegation - function onDelegationWithdrawn( - address, - IStrategy[] memory, - uint256[] memory - ) external pure returns(bytes memory) - // solhint-disable-next-line no-empty-blocks - {} -} \ No newline at end of file diff --git a/src/contracts/utils/UpgradeableSignatureCheckingUtils.sol b/src/contracts/utils/UpgradeableSignatureCheckingUtils.sol new file mode 100644 index 000000000..d38476798 --- /dev/null +++ b/src/contracts/utils/UpgradeableSignatureCheckingUtils.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity =0.8.12; + +import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; +import "../libraries/EIP1271SignatureUtils.sol"; + +/** + * @title Abstract contract that implements minimal signature-related storage & functionality for upgradeable contracts. + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + */ +abstract contract UpgradeableSignatureCheckingUtils is Initializable { + /// @notice The EIP-712 typehash for the contract's domain + bytes32 public constant DOMAIN_TYPEHASH = + keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)"); + + // chain id at the time of contract deployment + uint256 internal immutable ORIGINAL_CHAIN_ID; + + /** + * @notice Original EIP-712 Domain separator for this contract. + * @dev The domain separator may change in the event of a fork that modifies the ChainID. + * Use the getter function `domainSeparator` to get the current domain separator for this contract. + */ + bytes32 internal _DOMAIN_SEPARATOR; + + // INITIALIZING FUNCTIONS + constructor() { + ORIGINAL_CHAIN_ID = block.chainid; + } + + function _initializeSignatureCheckingUtils() internal + onlyInitializing + { + _DOMAIN_SEPARATOR = _calculateDomainSeparator(); + } + + // VIEW FUNCTIONS + /** + * @notice Getter function for the current EIP-712 domain separator for this contract. + * @dev The domain separator will change in the event of a fork that changes the ChainID. + */ + function domainSeparator() public view returns (bytes32) { + if (block.chainid == ORIGINAL_CHAIN_ID) { + return _DOMAIN_SEPARATOR; + } + else { + return _calculateDomainSeparator(); + } + } + + // @notice Internal function for calculating the current domain separator of this contract + function _calculateDomainSeparator() internal view returns (bytes32) { + return keccak256(abi.encode(DOMAIN_TYPEHASH, keccak256(bytes("EigenLayer")), block.chainid, address(this))); + } +} diff --git a/src/test/Delegation.t.sol b/src/test/Delegation.t.sol index 4b4a4044e..d51bb7db8 100644 --- a/src/test/Delegation.t.sol +++ b/src/test/Delegation.t.sol @@ -1,5 +1,5 @@ -// // SPDX-License-Identifier: BUSL-1.1 -// pragma solidity =0.8.12; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity =0.8.12; // import "@openzeppelin/contracts/utils/math/Math.sol"; // import "@openzeppelin/contracts/utils/Address.sol"; @@ -81,13 +81,13 @@ // _testRegisterAdditionalOperator(0, serveUntil); // } -// /// @notice testing if an operator can delegate to themselves. -// /// @param sender is the address of the operator. -// function testSelfOperatorDelegate(address sender) public { -// cheats.assume(sender != address(0)); -// cheats.assume(sender != address(eigenLayerProxyAdmin)); -// _testRegisterAsOperator(sender, IDelegationTerms(sender)); -// } + /// @notice testing if an operator can delegate to themselves. + /// @param sender is the address of the operator. + // function testSelfOperatorDelegate(address sender) public { + // cheats.assume(sender != address(0)); + // cheats.assume(sender != address(eigenLayerProxyAdmin)); + // _testRegisterAsOperator(sender, IDelegationTerms(sender)); + // } // function testTwoSelfOperatorsRegister() public { // _testRegisterAdditionalOperator(0, serveUntil); @@ -123,26 +123,26 @@ // cheats.assume(ethAmount >= 1); // cheats.assume(eigenAmount >= 1); -// // use storage to solve stack-too-deep -// operator = _operator; + // use storage to solve stack-too-deep + // operator = _operator; -// SigPDelegationTerms dt = new SigPDelegationTerms(); + // SigPDelegationTerms dt = new SigPDelegationTerms(); -// if (!delegation.isOperator(operator)) { -// _testRegisterAsOperator(operator, dt); -// } + // if (!delegation.isOperator(operator)) { + // _testRegisterAsOperator(operator, dt); + // } // uint256[3] memory amountsBefore; // amountsBefore[0] = voteWeigher.weightOfOperator(0, operator); // amountsBefore[1] = voteWeigher.weightOfOperator(1, operator); // amountsBefore[2] = delegation.operatorShares(operator, wethStrat); -// //making additional deposits to the strategies -// assertTrue(delegation.isNotDelegated(staker) == true, "testDelegation: staker is not delegate"); -// _testDepositWeth(staker, ethAmount); -// _testDepositEigen(staker, eigenAmount); -// _testDelegateToOperator(staker, operator); -// assertTrue(delegation.isDelegated(staker) == true, "testDelegation: staker is not delegate"); + //making additional deposits to the strategies + // assertTrue(delegation.isNotDelegated(staker) == true, "testDelegation: staker is not delegate"); + // _testDepositWeth(staker, ethAmount); + // _testDepositEigen(staker, eigenAmount); + // _testDelegateToOperator(staker, operator); + // assertTrue(delegation.isDelegated(staker) == true, "testDelegation: staker is not delegate"); // (IStrategy[] memory updatedStrategies, uint256[] memory updatedShares) = // strategyManager.getDeposits(staker); @@ -175,14 +175,14 @@ // cheats.startPrank(address(strategyManager)); -// IDelegationTerms expectedDt = delegation.delegationTerms(operator); -// assertTrue(address(expectedDt) == address(dt), "failed to set dt"); -// delegation.increaseDelegatedShares(staker, _strat, 1); + // IDelegationTerms expectedDt = delegation.delegationTerms(operator); + // assertTrue(address(expectedDt) == address(dt), "failed to set dt"); + // delegation.increaseDelegatedShares(staker, _strat, 1); -// // dt.delegate(); -// assertTrue(keccak256(dt.isDelegationReceived()) == keccak256(bytes("received")), "failed to fire expected onDelegationReceived callback"); -// } -// } + // // dt.delegate(); + // assertTrue(keccak256(dt.isDelegationReceived()) == keccak256(bytes("received")), "failed to fire expected onDelegationReceived callback"); + // } + // } // /// @notice tests that a when an operator is undelegated from, that the staker is properly classified as undelegated. // function testUndelegation(address operator, address staker, uint96 ethAmount, uint96 eigenAmount) @@ -212,25 +212,25 @@ // address staker = cheats.addr(PRIVATE_KEY); // _registerOperatorAndDepositFromStaker(operator, staker, ethAmount, eigenAmount); -// uint256 nonceBefore = delegation.nonces(staker); + // uint256 nonceBefore = delegation.nonces(staker); -// bytes32 structHash = keccak256(abi.encode(delegation.DELEGATION_TYPEHASH(), staker, operator, nonceBefore, expiry)); -// bytes32 digestHash = keccak256(abi.encodePacked("\x19\x01", delegation.DOMAIN_SEPARATOR(), structHash)); + // bytes32 structHash = keccak256(abi.encode(delegation.DELEGATION_TYPEHASH(), staker, operator, nonceBefore, expiry)); + // bytes32 digestHash = keccak256(abi.encodePacked("\x19\x01", delegation.DOMAIN_SEPARATOR(), structHash)); -// (uint8 v, bytes32 r, bytes32 s) = cheats.sign(PRIVATE_KEY, digestHash); + // (uint8 v, bytes32 r, bytes32 s) = cheats.sign(PRIVATE_KEY, digestHash); -// bytes memory signature = abi.encodePacked(r, s, v); + // bytes memory signature = abi.encodePacked(r, s, v); -// if (expiry < block.timestamp) { -// cheats.expectRevert("DelegationManager.delegateToBySignature: delegation signature expired"); -// } -// delegation.delegateToBySignature(staker, operator, expiry, signature); -// if (expiry >= block.timestamp) { -// assertTrue(delegation.isDelegated(staker) == true, "testDelegation: staker is not delegate"); -// assertTrue(nonceBefore + 1 == delegation.nonces(staker), "nonce not incremented correctly"); -// assertTrue(delegation.delegatedTo(staker) == operator, "staker delegated to wrong operator"); -// } -// } + // if (expiry < block.timestamp) { + // cheats.expectRevert("DelegationManager.delegateToBySignature: delegation signature expired"); + // } + // delegation.delegateToBySignature(staker, operator, expiry, signature); + // if (expiry >= block.timestamp) { + // assertTrue(delegation.isDelegated(staker) == true, "testDelegation: staker is not delegate"); + // assertTrue(nonceBefore + 1 == delegation.nonces(staker), "nonce not incremented correctly"); + // assertTrue(delegation.delegatedTo(staker) == operator, "staker delegated to wrong operator"); + // } + // } // /// @notice tries delegating using a signature and an EIP 1271 compliant wallet // function testDelegateToBySignature_WithContractWallet_Successfully(address operator, uint96 ethAmount, uint96 eigenAmount) @@ -247,20 +247,20 @@ // _registerOperatorAndDepositFromStaker(operator, staker, ethAmount, eigenAmount); -// uint256 nonceBefore = delegation.nonces(staker); + // uint256 nonceBefore = delegation.nonces(staker); -// bytes32 structHash = keccak256(abi.encode(delegation.DELEGATION_TYPEHASH(), staker, operator, nonceBefore, type(uint256).max)); -// bytes32 digestHash = keccak256(abi.encodePacked("\x19\x01", delegation.DOMAIN_SEPARATOR(), structHash)); + // bytes32 structHash = keccak256(abi.encode(delegation.DELEGATION_TYPEHASH(), staker, operator, nonceBefore, type(uint256).max)); + // bytes32 digestHash = keccak256(abi.encodePacked("\x19\x01", delegation.DOMAIN_SEPARATOR(), structHash)); -// (uint8 v, bytes32 r, bytes32 s) = cheats.sign(PRIVATE_KEY, digestHash); + // (uint8 v, bytes32 r, bytes32 s) = cheats.sign(PRIVATE_KEY, digestHash); -// bytes memory signature = abi.encodePacked(r, s, v); + // bytes memory signature = abi.encodePacked(r, s, v); -// delegation.delegateToBySignature(staker, operator, type(uint256).max, signature); -// assertTrue(delegation.isDelegated(staker) == true, "testDelegation: staker is not delegate"); -// assertTrue(nonceBefore + 1 == delegation.nonces(staker), "nonce not incremented correctly"); -// assertTrue(delegation.delegatedTo(staker) == operator, "staker delegated to wrong operator"); -// } + // delegation.delegateToBySignature(staker, operator, type(uint256).max, signature); + // assertTrue(delegation.isDelegated(staker) == true, "testDelegation: staker is not delegate"); + // assertTrue(nonceBefore + 1 == delegation.nonces(staker), "nonce not incremented correctly"); + // assertTrue(delegation.delegatedTo(staker) == operator, "staker delegated to wrong operator"); + // } // /// @notice tries delegating using a signature and an EIP 1271 compliant wallet, *but* providing a bad signature // function testDelegateToBySignature_WithContractWallet_BadSignature(address operator, uint96 ethAmount, uint96 eigenAmount) @@ -277,20 +277,20 @@ // _registerOperatorAndDepositFromStaker(operator, staker, ethAmount, eigenAmount); -// uint256 nonceBefore = delegation.nonces(staker); + // uint256 nonceBefore = delegation.nonces(staker); -// bytes32 structHash = keccak256(abi.encode(delegation.DELEGATION_TYPEHASH(), staker, operator, nonceBefore, type(uint256).max)); -// bytes32 digestHash = keccak256(abi.encodePacked("\x19\x01", delegation.DOMAIN_SEPARATOR(), structHash)); + // bytes32 structHash = keccak256(abi.encode(delegation.DELEGATION_TYPEHASH(), staker, operator, nonceBefore, type(uint256).max)); + // bytes32 digestHash = keccak256(abi.encodePacked("\x19\x01", delegation.DOMAIN_SEPARATOR(), structHash)); -// (uint8 v, bytes32 r, bytes32 s) = cheats.sign(PRIVATE_KEY, digestHash); -// // mess up the signature by flipping v's parity -// v = (v == 27 ? 28 : 27); + // (uint8 v, bytes32 r, bytes32 s) = cheats.sign(PRIVATE_KEY, digestHash); + // // mess up the signature by flipping v's parity + // v = (v == 27 ? 28 : 27); -// bytes memory signature = abi.encodePacked(r, s, v); + // bytes memory signature = abi.encodePacked(r, s, v); -// cheats.expectRevert(bytes("DelegationManager.delegateToBySignature: ERC1271 signature verification failed")); -// delegation.delegateToBySignature(staker, operator, type(uint256).max, signature); -// } + // cheats.expectRevert(bytes("DelegationManager.delegateToBySignature: ERC1271 signature verification failed")); + // delegation.delegateToBySignature(staker, operator, type(uint256).max, signature); + // } // /// @notice tries delegating using a wallet that does not comply with EIP 1271 // function testDelegateToBySignature_WithContractWallet_NonconformingWallet(address operator, uint96 ethAmount, uint96 eigenAmount, uint8 v, bytes32 r, bytes32 s) @@ -311,9 +311,9 @@ // bytes memory signature = abi.encodePacked(r, s, v); -// cheats.expectRevert(); -// delegation.delegateToBySignature(staker, operator, type(uint256).max, signature); -// } + // cheats.expectRevert(); + // delegation.delegateToBySignature(staker, operator, type(uint256).max, signature); + // } // /// @notice tests delegation to EigenLayer via an ECDSA signatures with invalid signature // /// @param operator is the operator being delegated to. @@ -334,9 +334,9 @@ // bytes memory signature = abi.encodePacked(r, s, v); -// cheats.expectRevert(); -// delegation.delegateToBySignature(staker, operator, type(uint256).max, signature); -// } + // cheats.expectRevert(); + // delegation.delegateToBySignature(staker, operator, type(uint256).max, signature); + // } // /// @notice registers a fixed address as a delegate, delegates to it from a second address, // /// and checks that the delegate's voteWeights increase properly @@ -349,11 +349,11 @@ // { // cheats.assume(staker != operator); -// cheats.assume(numStratsToAdd > 0 && numStratsToAdd <= 20); -// uint96 operatorEthWeightBefore = voteWeigher.weightOfOperator(0, operator); -// uint96 operatorEigenWeightBefore = voteWeigher.weightOfOperator(1, operator); -// _testRegisterAsOperator(operator, IDelegationTerms(operator)); -// _testDepositStrategies(staker, 1e18, numStratsToAdd); + // cheats.assume(numStratsToAdd > 0 && numStratsToAdd <= 20); + // uint96 operatorEthWeightBefore = voteWeigher.weightOfOperator(operator, 0); + // uint96 operatorEigenWeightBefore = voteWeigher.weightOfOperator(operator, 1); + // _testRegisterAsOperator(operator, IDelegationTerms(operator)); + // _testDepositStrategies(staker, 1e18, numStratsToAdd); // // add strategies to voteWeigher // uint96 multiplier = 1e18; @@ -388,13 +388,13 @@ // delegation.initialize(address(this), eigenLayerPauserReg, 0); // } -// /// @notice This function tests to ensure that a you can't register as a delegate multiple times -// /// @param operator is the operator being delegated to. -// function testRegisterAsOperatorMultipleTimes(address operator) public fuzzedAddress(operator) { -// _testRegisterAsOperator(operator, IDelegationTerms(operator)); -// cheats.expectRevert(bytes("DelegationManager.registerAsOperator: operator has already registered")); -// _testRegisterAsOperator(operator, IDelegationTerms(operator)); -// } + /// @notice This function tests to ensure that a you can't register as a delegate multiple times + /// @param operator is the operator being delegated to. + // function testRegisterAsOperatorMultipleTimes(address operator) public fuzzedAddress(operator) { + // _testRegisterAsOperator(operator, IDelegationTerms(operator)); + // cheats.expectRevert(bytes("DelegationManager.registerAsOperator: operator has already registered")); + // _testRegisterAsOperator(operator, IDelegationTerms(operator)); + // } // /// @notice This function tests to ensure that a staker cannot delegate to an unregistered operator // /// @param delegate is the unregistered operator @@ -403,11 +403,11 @@ // _testDepositStrategies(getOperatorAddress(1), 1e18, 1); // _testDepositEigen(getOperatorAddress(1), 1e18); -// cheats.expectRevert(bytes("DelegationManager._delegate: operator has not yet registered as a delegate")); -// cheats.startPrank(getOperatorAddress(1)); -// delegation.delegateTo(delegate); -// cheats.stopPrank(); -// } + // cheats.expectRevert(bytes("DelegationManager._delegate: operator has not yet registered as a delegate")); + // cheats.startPrank(getOperatorAddress(1)); + // delegation.delegateTo(delegate); + // cheats.stopPrank(); + // } // /// @notice This function tests to ensure that a delegation contract @@ -420,30 +420,30 @@ // delegation.initialize(_attacker, eigenLayerPauserReg, 0); // } -// /// @notice This function tests that the delegationTerms cannot be set to address(0) -// function testCannotSetDelegationTermsZeroAddress() public{ -// cheats.expectRevert(bytes("DelegationManager._delegate: operator has not yet registered as a delegate")); -// delegation.registerAsOperator(IDelegationTerms(address(0))); -// } - -// /// @notice This function tests to ensure that an address can only call registerAsOperator() once -// function testCannotRegisterAsOperatorTwice(address _operator, address _dt) public fuzzedAddress(_operator) fuzzedAddress(_dt) { -// vm.assume(_dt != address(0)); -// vm.startPrank(_operator); -// delegation.registerAsOperator(IDelegationTerms(_dt)); -// vm.expectRevert("DelegationManager.registerAsOperator: operator has already registered"); -// delegation.registerAsOperator(IDelegationTerms(_dt)); -// cheats.stopPrank(); -// } - -// /// @notice This function checks that you can only delegate to an address that is already registered. -// function testDelegateToInvalidOperator(address _staker, address _unregisteredOperator) public fuzzedAddress(_staker) { -// vm.startPrank(_staker); -// cheats.expectRevert(bytes("DelegationManager._delegate: operator has not yet registered as a delegate")); -// delegation.delegateTo(_unregisteredOperator); -// cheats.expectRevert(bytes("DelegationManager._delegate: operator has not yet registered as a delegate")); -// delegation.delegateTo(_staker); -// cheats.stopPrank(); + /// @notice This function tests that the delegationTerms cannot be set to address(0) + // function testCannotSetDelegationTermsZeroAddress() public{ + // cheats.expectRevert(bytes("DelegationManager._delegate: operator has not yet registered as a delegate")); + // delegation.registerAsOperator(IDelegationTerms(address(0))); + // } + + /// @notice This function tests to ensure that an address can only call registerAsOperator() once + // function testCannotRegisterAsOperatorTwice(address _operator, address _dt) public fuzzedAddress(_operator) fuzzedAddress(_dt) { + // vm.assume(_dt != address(0)); + // vm.startPrank(_operator); + // delegation.registerAsOperator(IDelegationTerms(_dt)); + // vm.expectRevert("DelegationManager.registerAsOperator: operator has already registered"); + // delegation.registerAsOperator(IDelegationTerms(_dt)); + // cheats.stopPrank(); + // } + + /// @notice This function checks that you can only delegate to an address that is already registered. + // function testDelegateToInvalidOperator(address _staker, address _unregisteredOperator) public fuzzedAddress(_staker) { + // vm.startPrank(_staker); + // cheats.expectRevert(bytes("DelegationManager._delegate: operator has not yet registered as a delegate")); + // delegation.delegateTo(_unregisteredOperator); + // cheats.expectRevert(bytes("DelegationManager._delegate: operator has not yet registered as a delegate")); + // delegation.delegateTo(_staker); + // cheats.stopPrank(); // } @@ -456,11 +456,11 @@ // vm.assume(_operator != address(eigenLayerProxyAdmin)); // vm.assume(_staker != address(eigenLayerProxyAdmin)); -// //setup delegation -// vm.prank(_operator); -// delegation.registerAsOperator(IDelegationTerms(_dt)); -// vm.prank(_staker); -// delegation.delegateTo(_operator); + //setup delegation + // vm.prank(_operator); + // delegation.registerAsOperator(IDelegationTerms(_dt)); + // vm.prank(_staker); + // delegation.delegateTo(_operator); // //operators cannot undelegate from themselves // vm.prank(address(strategyManager)); @@ -477,30 +477,30 @@ // cheats.expectRevert(); // delegation.undelegate(_operator); -// //assert still delegated -// assertTrue(delegation.isDelegated(_staker)); -// assertFalse(delegation.isNotDelegated(_staker)); -// assertTrue(delegation.isOperator(_operator)); + //assert still delegated + // assertTrue(delegation.isDelegated(_staker)); + // assertFalse(delegation.isNotDelegated(_staker)); + // assertTrue(delegation.isOperator(_operator)); -// //strategyManager can undelegate _staker -// vm.prank(address(strategyManager)); -// delegation.undelegate(_staker); -// assertFalse(delegation.isDelegated(_staker)); -// assertTrue(delegation.isNotDelegated(_staker)); + // //strategyManager can undelegate _staker + // vm.prank(address(strategyManager)); + // delegation.undelegate(_staker); + // assertFalse(delegation.isDelegated(_staker)); + // assertTrue(delegation.isNotDelegated(_staker)); // } // function _testRegisterAdditionalOperator(uint256 index, uint32 _serveUntil) internal { // address sender = getOperatorAddress(index); -// //register as both ETH and EIGEN operator -// uint256 wethToDeposit = 1e18; -// uint256 eigenToDeposit = 1e10; -// _testDepositWeth(sender, wethToDeposit); -// _testDepositEigen(sender, eigenToDeposit); -// _testRegisterAsOperator(sender, IDelegationTerms(sender)); + //register as both ETH and EIGEN operator + // uint256 wethToDeposit = 1e18; + // uint256 eigenToDeposit = 1e10; + // _testDepositWeth(sender, wethToDeposit); + // _testDepositEigen(sender, eigenToDeposit); + // _testRegisterAsOperator(sender, IDelegationTerms(sender)); -// cheats.startPrank(sender); + // cheats.startPrank(sender); // //whitelist the serviceManager to slash the operator // slasher.optIntoSlashing(address(serviceManager)); diff --git a/src/test/DepositWithdraw.t.sol b/src/test/DepositWithdraw.t.sol index 984907608..4585f98b8 100644 --- a/src/test/DepositWithdraw.t.sol +++ b/src/test/DepositWithdraw.t.sol @@ -111,7 +111,12 @@ contract DepositWithdrawTests is EigenLayerTestHelper { { assertTrue(!delegation.isDelegated(staker), "_createQueuedWithdrawal: staker is already delegated"); - _testRegisterAsOperator(staker, IDelegationTerms(staker)); + IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + earningsReceiver: staker, + delegationApprover: address(0), + stakerOptOutWindowBlocks: 0 + }); + _testRegisterAsOperator(staker, operatorDetails); assertTrue( delegation.isDelegated(staker), "_createQueuedWithdrawal: staker isn't delegated when they should be" ); diff --git a/src/test/EigenLayerTestHelper.t.sol b/src/test/EigenLayerTestHelper.t.sol index a24ffb498..73b24d877 100644 --- a/src/test/EigenLayerTestHelper.t.sol +++ b/src/test/EigenLayerTestHelper.t.sol @@ -34,7 +34,12 @@ contract EigenLayerTestHelper is EigenLayerDeployer { address operator = getOperatorAddress(operatorIndex); //setting up operator's delegation terms - _testRegisterAsOperator(operator, IDelegationTerms(operator)); + IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + earningsReceiver: operator, + delegationApprover: address(0), + stakerOptOutWindowBlocks: 0 + }); + _testRegisterAsOperator(operator, operatorDetails); for (uint256 i; i < stakers.length; i++) { //initialize weth, eigen and eth balances for staker @@ -63,20 +68,22 @@ contract EigenLayerTestHelper is EigenLayerDeployer { } /** - * @notice Register 'sender' as an operator, setting their 'DelegationTerms' contract in DelegationManager to 'dt', verifies + * @notice Register 'sender' as an operator, setting their 'OperatorDetails' in DelegationManager to 'operatorDetails', verifies * that the storage of DelegationManager contract is updated appropriately * * @param sender is the address being registered as an operator - * @param dt is the sender's DelegationTerms contract + * @param operatorDetails is the `sender`'s OperatorDetails struct */ - function _testRegisterAsOperator(address sender, IDelegationTerms dt) internal { + function _testRegisterAsOperator(address sender, IDelegationManager.OperatorDetails memory operatorDetails) internal { cheats.startPrank(sender); - delegation.registerAsOperator(dt); + string memory emptyStringForMetadataURI; + delegation.registerAsOperator(operatorDetails, emptyStringForMetadataURI); assertTrue(delegation.isOperator(sender), "testRegisterAsOperator: sender is not a operator"); - assertTrue( - delegation.delegationTerms(sender) == dt, "_testRegisterAsOperator: delegationTerms not set appropriately" - ); + // TODO: FIX THIS + // assertTrue( + // delegation.delegationTerms(sender) == dt, "_testRegisterAsOperator: delegationTerms not set appropriately" + // ); assertTrue(delegation.isDelegated(sender), "_testRegisterAsOperator: sender not marked as actively delegated"); cheats.stopPrank(); @@ -188,7 +195,8 @@ contract EigenLayerTestHelper is EigenLayerDeployer { } cheats.startPrank(staker); - delegation.delegateTo(operator); + IDelegationManager.SignatureWithExpiry memory signatureWithExpiry; + delegation.delegateTo(operator, signatureWithExpiry); cheats.stopPrank(); assertTrue( @@ -278,7 +286,12 @@ contract EigenLayerTestHelper is EigenLayerDeployer { // we do this here to ensure that `staker` is delegated if `registerAsOperator` is true if (registerAsOperator) { assertTrue(!delegation.isDelegated(staker), "_createQueuedWithdrawal: staker is already delegated"); - _testRegisterAsOperator(staker, IDelegationTerms(staker)); + IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + earningsReceiver: staker, + delegationApprover: address(0), + stakerOptOutWindowBlocks: 0 + }); + _testRegisterAsOperator(staker, operatorDetails); assertTrue( delegation.isDelegated(staker), "_createQueuedWithdrawal: staker isn't delegated when they should be" ); @@ -340,7 +353,12 @@ contract EigenLayerTestHelper is EigenLayerDeployer { internal { if (!delegation.isOperator(operator)) { - _testRegisterAsOperator(operator, IDelegationTerms(operator)); + IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + earningsReceiver: operator, + delegationApprover: address(0), + stakerOptOutWindowBlocks: 0 + }); + _testRegisterAsOperator(operator, operatorDetails); } uint256[3] memory amountsBefore; @@ -349,7 +367,7 @@ contract EigenLayerTestHelper is EigenLayerDeployer { amountsBefore[2] = delegation.operatorShares(operator, wethStrat); //making additional deposits to the strategies - assertTrue(delegation.isNotDelegated(staker) == true, "testDelegation: staker is not delegate"); + assertTrue(!delegation.isDelegated(staker) == true, "testDelegation: staker is not delegate"); _testDepositWeth(staker, ethAmount); _testDepositEigen(staker, eigenAmount); _testDelegateToOperator(staker, operator); diff --git a/src/test/EigenPod.t.sol b/src/test/EigenPod.t.sol index 80a942f07..70038fe16 100644 --- a/src/test/EigenPod.t.sol +++ b/src/test/EigenPod.t.sol @@ -859,17 +859,18 @@ contract EigenPodTests is ProofParsing, EigenPodPausingConstants { cheats.stopPrank(); } - // simply tries to register 'sender' as a delegate, setting their 'DelegationTerms' contract in DelegationManager to 'dt' + // simply tries to register 'sender' as an operator, setting their 'OperatorDetails' in DelegationManager to 'operatorDetails' // verifies that the storage of DelegationManager contract is updated appropriately - function _testRegisterAsOperator(address sender, IDelegationTerms dt) internal { + function _testRegisterAsOperator(address sender, IDelegationManager.OperatorDetails memory operatorDetails) internal { cheats.startPrank(sender); - - delegation.registerAsOperator(dt); + string memory emptyStringForMetadataURI; + delegation.registerAsOperator(operatorDetails, emptyStringForMetadataURI); assertTrue(delegation.isOperator(sender), "testRegisterAsOperator: sender is not a delegate"); - assertTrue( - delegation.delegationTerms(sender) == dt, "_testRegisterAsOperator: delegationTerms not set appropriately" - ); + // TODO: FIX THIS + // assertTrue( + // delegation.delegationTerms(sender) == dt, "_testRegisterAsOperator: delegationTerms not set appropriately" + // ); assertTrue(delegation.isDelegated(sender), "_testRegisterAsOperator: sender not marked as actively delegated"); cheats.stopPrank(); @@ -888,7 +889,8 @@ contract EigenPodTests is ProofParsing, EigenPodPausingConstants { } cheats.startPrank(sender); - delegation.delegateTo(operator); + IDelegationManager.SignatureWithExpiry memory signatureWithExpiry; + delegation.delegateTo(operator, signatureWithExpiry); cheats.stopPrank(); assertTrue( @@ -913,11 +915,16 @@ contract EigenPodTests is ProofParsing, EigenPodPausingConstants { internal { if (!delegation.isOperator(operator)) { - _testRegisterAsOperator(operator, IDelegationTerms(operator)); + IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + earningsReceiver: operator, + delegationApprover: address(0), + stakerOptOutWindowBlocks: 0 + }); + _testRegisterAsOperator(operator, operatorDetails); } //making additional deposits to the strategies - assertTrue(delegation.isNotDelegated(staker) == true, "testDelegation: staker is not delegate"); + assertTrue(!delegation.isDelegated(staker) == true, "testDelegation: staker is not delegate"); _testDelegateToOperator(staker, operator); assertTrue(delegation.isDelegated(staker) == true, "testDelegation: staker is not delegate"); diff --git a/src/test/Registration.t.sol b/src/test/Registration.t.sol index 0eaf16372..23707a5b8 100644 --- a/src/test/Registration.t.sol +++ b/src/test/Registration.t.sol @@ -1,5 +1,5 @@ -// // SPDX-License-Identifier: BUSL-1.1 -// pragma solidity =0.8.12; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity =0.8.12; // import "./EigenLayerTestHelper.t.sol"; @@ -83,12 +83,17 @@ // cheats.assume(operatorIndex < 15); // BN254.G1Point memory pk = getOperatorPubkeyG1(operatorIndex); -// //register as both ETH and EIGEN operator -// uint256 wethToDeposit = 1e18; -// uint256 eigenToDeposit = 1e18; -// _testDepositWeth(operator, wethToDeposit); -// _testDepositEigen(operator, eigenToDeposit); -// _testRegisterAsOperator(operator, IDelegationTerms(operator)); + //register as both ETH and EIGEN operator + // uint256 wethToDeposit = 1e18; + // uint256 eigenToDeposit = 1e18; + // _testDepositWeth(operator, wethToDeposit); + // _testDepositEigen(operator, eigenToDeposit); + // IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + // earningsReceiver: operator, + // delegationApprover: address(0), + // stakerOptOutWindowBlocks: 0 + // }); + // _testRegisterAsOperator(operator, operatorDetails); // cheats.startPrank(operator); // slasher.optIntoSlashing(address(dlsm)); diff --git a/src/test/SigP/DelegationTerms.sol b/src/test/SigP/DelegationTerms.sol index dec54629f..466e288cc 100644 --- a/src/test/SigP/DelegationTerms.sol +++ b/src/test/SigP/DelegationTerms.sol @@ -4,7 +4,7 @@ import "../../contracts/strategies/StrategyBase.sol"; import "../../contracts/interfaces/IDelegationManager.sol"; -contract SigPDelegationTerms is IDelegationTerms { +contract SigPDelegationTerms { uint256 public paid; bytes public isDelegationWithdrawn; bytes public isDelegationReceived; diff --git a/src/test/Slasher.t.sol b/src/test/Slasher.t.sol index 3ed511f78..68f8dde39 100644 --- a/src/test/Slasher.t.sol +++ b/src/test/Slasher.t.sol @@ -3,7 +3,6 @@ pragma solidity =0.8.12; import "./EigenLayerDeployer.t.sol"; import "./EigenLayerTestHelper.t.sol"; -import "../contracts/operators/MerkleDelegationTerms.sol"; contract SlasherTests is EigenLayerTestHelper { ISlasher instance; @@ -12,12 +11,10 @@ contract SlasherTests is EigenLayerTestHelper { address middleware_2 = address(0x009849); address middleware_3 = address(0x001000); address middleware_4 = address(0x002000); - MerkleDelegationTerms delegationTerms; //performs basic deployment before each test function setUp() public override { super.setUp(); - delegationTerms = new MerkleDelegationTerms(); } /** @@ -40,7 +37,12 @@ contract SlasherTests is EigenLayerTestHelper { // have `_operator` make deposits in WETH strategy _testDepositWeth(_operator, amountToDeposit); // register `_operator` as an operator - _testRegisterAsOperator(_operator, IDelegationTerms(_operator)); + IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + earningsReceiver: _operator, + delegationApprover: address(0), + stakerOptOutWindowBlocks: 0 + }); + _testRegisterAsOperator(_operator, operatorDetails); // make deposit in WETH strategy from each of `accounts`, then delegate them to `_operator` for (uint256 i = 0; i < accounts.length; i++) { @@ -93,7 +95,13 @@ contract SlasherTests is EigenLayerTestHelper { function testRecursiveCallRevert() public { //Register and opt into slashing with operator cheats.startPrank(operator); - delegation.registerAsOperator(delegationTerms); + IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + earningsReceiver: operator, + delegationApprover: address(0), + stakerOptOutWindowBlocks: 0 + }); + string memory emptyStringForMetadataURI; + delegation.registerAsOperator(operatorDetails, emptyStringForMetadataURI); slasher.optIntoSlashing(middleware); slasher.optIntoSlashing(middleware_2); slasher.optIntoSlashing(middleware_3); @@ -131,7 +139,13 @@ contract SlasherTests is EigenLayerTestHelper { //Register and opt into slashing with operator cheats.startPrank(operator); - delegation.registerAsOperator(delegationTerms); + IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + earningsReceiver: operator, + delegationApprover: address(0), + stakerOptOutWindowBlocks: 0 + }); + string memory emptyStringForMetadataURI; + delegation.registerAsOperator(operatorDetails, emptyStringForMetadataURI); slasher.optIntoSlashing(middleware); slasher.optIntoSlashing(middleware_3); cheats.stopPrank(); @@ -163,7 +177,13 @@ contract SlasherTests is EigenLayerTestHelper { function testRecordStakeUpdate() public { ///Register and opt into slashing with operator cheats.startPrank(operator); - delegation.registerAsOperator(delegationTerms); + IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + earningsReceiver: operator, + delegationApprover: address(0), + stakerOptOutWindowBlocks: 0 + }); + string memory emptyStringForMetadataURI; + delegation.registerAsOperator(operatorDetails, emptyStringForMetadataURI); slasher.optIntoSlashing(middleware); slasher.optIntoSlashing(middleware_3); cheats.stopPrank(); @@ -202,7 +222,13 @@ contract SlasherTests is EigenLayerTestHelper { function testOrderingRecordStakeUpdateVuln() public { ///Register and opt into slashing with operator cheats.startPrank(operator); - delegation.registerAsOperator(delegationTerms); + IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + earningsReceiver: operator, + delegationApprover: address(0), + stakerOptOutWindowBlocks: 0 + }); + string memory emptyStringForMetadataURI; + delegation.registerAsOperator(operatorDetails, emptyStringForMetadataURI); slasher.optIntoSlashing(middleware); slasher.optIntoSlashing(middleware_3); cheats.stopPrank(); @@ -230,7 +256,13 @@ contract SlasherTests is EigenLayerTestHelper { function testOnlyRegisteredForService(address _slasher, uint32 _serveUntilBlock) public fuzzedAddress(_slasher) { cheats.prank(operator); - delegation.registerAsOperator(delegationTerms); + IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + earningsReceiver: operator, + delegationApprover: address(0), + stakerOptOutWindowBlocks: 0 + }); + string memory emptyStringForMetadataURI; + delegation.registerAsOperator(operatorDetails, emptyStringForMetadataURI); //slasher cannot call stake update unless operator has oped in cheats.prank(_slasher); @@ -255,14 +287,26 @@ contract SlasherTests is EigenLayerTestHelper { //can opt in after registered as operator cheats.startPrank(_operator); - delegation.registerAsOperator(delegationTerms); + IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + earningsReceiver: _operator, + delegationApprover: address(0), + stakerOptOutWindowBlocks: 0 + }); + string memory emptyStringForMetadataURI; + delegation.registerAsOperator(operatorDetails, emptyStringForMetadataURI); slasher.optIntoSlashing(_slasher); cheats.stopPrank(); } function testFreezeOperator() public { cheats.prank(operator); - delegation.registerAsOperator(delegationTerms); + IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + earningsReceiver: operator, + delegationApprover: address(0), + stakerOptOutWindowBlocks: 0 + }); + string memory emptyStringForMetadataURI; + delegation.registerAsOperator(operatorDetails, emptyStringForMetadataURI); //cannot freeze until operator has oped in cheats.prank(middleware); @@ -284,7 +328,13 @@ contract SlasherTests is EigenLayerTestHelper { cheats.assume(_attacker != slasher.owner()); cheats.prank(operator); - delegation.registerAsOperator(delegationTerms); + IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + earningsReceiver: operator, + delegationApprover: address(0), + stakerOptOutWindowBlocks: 0 + }); + string memory emptyStringForMetadataURI; + delegation.registerAsOperator(operatorDetails, emptyStringForMetadataURI); cheats.prank(operator); slasher.optIntoSlashing(middleware); @@ -311,7 +361,13 @@ contract SlasherTests is EigenLayerTestHelper { function testRecordLastStakeUpdateAndRevokeSlashingAbility() public { ///Register and opt into slashing with operator cheats.startPrank(operator); - delegation.registerAsOperator(delegationTerms); + IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + earningsReceiver: operator, + delegationApprover: address(0), + stakerOptOutWindowBlocks: 0 + }); + string memory emptyStringForMetadataURI; + delegation.registerAsOperator(operatorDetails, emptyStringForMetadataURI); slasher.optIntoSlashing(middleware); cheats.stopPrank(); diff --git a/src/test/Whitelister.t.sol b/src/test/Whitelister.t.sol index a3f1401a4..1a4720c40 100644 --- a/src/test/Whitelister.t.sol +++ b/src/test/Whitelister.t.sol @@ -1,5 +1,5 @@ -// // SPDX-License-Identifier: BUSL-1.1 -// pragma solidity =0.8.12; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity =0.8.12; // import "../../src/contracts/interfaces/IStrategyManager.sol"; // import "../../src/contracts/interfaces/IStrategy.sol"; @@ -134,11 +134,16 @@ // cheats.stopPrank(); // } -// function testWhitelistingOperator(address operator) public fuzzedAddress(operator) { -// cheats.startPrank(operator); -// IDelegationTerms dt = IDelegationTerms(address(89)); -// delegation.registerAsOperator(dt); -// cheats.stopPrank(); + // function testWhitelistingOperator(address operator) public fuzzedAddress(operator) { + // cheats.startPrank(operator); + // IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + // earningsReceiver: operator, + // delegationApprover: address(0), + // stakerOptOutWindowBlocks: 0 + // }); + // string memory emptyStringForMetadataURI; + // delegation.registerAsOperator(operatorDetails, emptyStringForMetadataURI); + // cheats.stopPrank(); // cheats.startPrank(theMultiSig); // whiteLister.whitelist(operator); @@ -168,11 +173,16 @@ // Staker(staker).callAddress(address(strategyManager), data); // } -// function testNonWhitelistedOperatorRegistration(BN254.G1Point memory pk, string memory socket ) external { -// cheats.startPrank(operator); -// IDelegationTerms dt = IDelegationTerms(address(89)); -// delegation.registerAsOperator(dt); -// cheats.stopPrank(); + // function testNonWhitelistedOperatorRegistration(BN254.G1Point memory pk, string memory socket ) external { + // cheats.startPrank(operator); + // IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + // earningsReceiver: operator, + // delegationApprover: address(0), + // stakerOptOutWindowBlocks: 0 + // }); + // string memory emptyStringForMetadataURI; + // delegation.registerAsOperator(operatorDetails, emptyStringForMetadataURI); + // cheats.stopPrank(); // cheats.expectRevert(bytes("BLSRegistry._registerOperator: not whitelisted")); // blsRegistry.registerOperator(1, pk, socket); @@ -186,9 +196,14 @@ // public fuzzedAddress(operator) // { -// address staker = whiteLister.getStaker(operator); -// cheats.assume(staker!=operator); -// _testRegisterAsOperator(operator, IDelegationTerms(operator)); + // address staker = whiteLister.getStaker(operator); + // cheats.assume(staker != operator); + // IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + // earningsReceiver: operator, + // delegationApprover: address(0), + // stakerOptOutWindowBlocks: 0 + // }); + // _testRegisterAsOperator(operator, operatorDetails); // { // cheats.startPrank(theMultiSig); diff --git a/src/test/mocks/DelegationMock.sol b/src/test/mocks/DelegationMock.sol index 3d5ae1020..372146876 100644 --- a/src/test/mocks/DelegationMock.sol +++ b/src/test/mocks/DelegationMock.sol @@ -20,32 +20,90 @@ contract DelegationMock is IDelegationManager, Test { mapping (address => address) public delegatedTo; - function registerAsOperator(IDelegationTerms /*dt*/) external {} + function registerAsOperator(OperatorDetails calldata /*registeringOperatorDetails*/, string calldata /*metadataURI*/) external pure {} + + function updateOperatorMetadataURI(string calldata /*metadataURI*/) external pure {} - function delegateTo(address operator) external { + function delegateTo(address operator, SignatureWithExpiry memory /*approverSignatureAndExpiry*/) external { delegatedTo[msg.sender] = operator; } - function delegateToBySignature(address /*staker*/, address /*operator*/, uint256 /*expiry*/, bytes memory /*signature*/) external {} + function modifyOperatorDetails(OperatorDetails calldata /*newOperatorDetails*/) external pure {} + + function delegateToBySignature( + address /*staker*/, + address /*operator*/, + SignatureWithExpiry memory /*stakerSignatureAndExpiry*/, + SignatureWithExpiry memory /*approverSignatureAndExpiry*/ + ) external pure {} function undelegate(address staker) external { delegatedTo[staker] = address(0); } - /// @notice returns the DelegationTerms of the `operator`, which may mediate their interactions with stakers who delegate to them. - function delegationTerms(address /*operator*/) external view returns (IDelegationTerms) {} + function forceUndelegation(address /*staker*/) external pure returns (bytes32) {} - function increaseDelegatedShares(address /*staker*/, IStrategy /*strategy*/, uint256 /*shares*/) external {} + function increaseDelegatedShares(address /*staker*/, IStrategy /*strategy*/, uint256 /*shares*/) external pure {} - function decreaseDelegatedShares( - address /*staker*/, - IStrategy[] calldata /*strategies*/, - uint256[] calldata /*shares*/ - ) external {} + function decreaseDelegatedShares(address /*staker*/, IStrategy[] calldata /*strategies*/, uint256[] calldata /*shares*/) external pure {} + + function operatorDetails(address operator) external pure returns (OperatorDetails memory) { + OperatorDetails memory returnValue = OperatorDetails({ + earningsReceiver: operator, + delegationApprover: operator, + stakerOptOutWindowBlocks: 0 + }); + return returnValue; + } + + function earningsReceiver(address operator) external pure returns (address) { + return operator; + } + + function delegationApprover(address operator) external pure returns (address) { + return operator; + } + + function stakerOptOutWindowBlocks(address /*operator*/) external pure returns (uint256) { + return 0; + } function isDelegated(address staker) external view returns (bool) { return (delegatedTo[staker] != address(0)); } function isNotDelegated(address /*staker*/) external pure returns (bool) {} + + // function isOperator(address /*operator*/) external pure returns (bool) {} + + function stakerNonce(address /*staker*/) external pure returns (uint256) {} + + function delegationApproverNonce(address /*operator*/) external pure returns (uint256) {} + + function calculateCurrentStakerDelegationDigestHash(address /*staker*/, address /*operator*/, uint256 /*expiry*/) external view returns (bytes32) {} + + function calculateStakerDelegationDigestHash(address /*staker*/, uint256 /*stakerNonce*/, address /*operator*/, uint256 /*expiry*/) external view returns (bytes32) {} + + function calculateCurrentDelegationApprovalDigestHash(address /*staker*/, address /*operator*/, uint256 /*expiry*/) external view returns (bytes32) {} + + function calculateDelegationApprovalDigestHash( + address /*staker*/, + address /*operator*/, + address /*_delegationApprover*/, + uint256 /*approverNonce*/, + uint256 /*expiry*/ + ) external view returns (bytes32) {} + + function calculateStakerDigestHash(address /*staker*/, address /*operator*/, uint256 /*expiry*/) + external pure returns (bytes32 stakerDigestHash) {} + + function calculateApproverDigestHash(address /*staker*/, address /*operator*/, uint256 /*expiry*/) external pure returns (bytes32 approverDigestHash) {} + + function DOMAIN_TYPEHASH() external view returns (bytes32) {} + + function STAKER_DELEGATION_TYPEHASH() external view returns (bytes32) {} + + function DELEGATION_APPROVAL_TYPEHASH() external view returns (bytes32) {} + + function domainSeparator() external view returns (bytes32) {} } \ No newline at end of file diff --git a/src/test/mocks/DelegationTermsMock.sol b/src/test/mocks/DelegationTermsMock.sol deleted file mode 100644 index b600fb6fc..000000000 --- a/src/test/mocks/DelegationTermsMock.sol +++ /dev/null @@ -1,59 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.9; - -import "forge-std/Test.sol"; -import "../../contracts/interfaces/IDelegationTerms.sol"; - -contract DelegationTermsMock is IDelegationTerms, Test { - - bool public shouldRevert; - bool public shouldReturnData; - - function setShouldRevert(bool _shouldRevert) external { - shouldRevert = _shouldRevert; - } - - function setShouldReturnData(bool _shouldReturnData) external { - shouldReturnData = _shouldReturnData; - } - - function payForService(IERC20 /*token*/, uint256 /*amount*/) external payable { - - } - - function onDelegationWithdrawn( - address /*delegator*/, - IStrategy[] memory /*stakerStrategyList*/, - uint256[] memory /*stakerShares*/ - ) external view returns (bytes memory) { - if (shouldRevert) { - revert("reverting as intended"); - } - - if (shouldReturnData) { - bytes32[5] memory returnData = [bytes32(0), bytes32(0), bytes32(0), bytes32(0), bytes32(0)]; - return abi.encodePacked(returnData); - } - - bytes memory emptyReturnData; - return emptyReturnData; - } - - function onDelegationReceived( - address /*delegator*/, - IStrategy[] memory /*stakerStrategyList*/, - uint256[] memory /*stakerShares*/ - ) external view returns (bytes memory) { - if (shouldRevert) { - revert("reverting as intended"); - } - if (shouldReturnData) { - bytes32[5] memory returnData = [bytes32(0), bytes32(0), bytes32(0), bytes32(0), bytes32(0)]; - return abi.encodePacked(returnData); - } - - bytes memory emptyReturnData; - return emptyReturnData; - } - -} \ No newline at end of file diff --git a/src/test/mocks/StrategyManagerMock.sol b/src/test/mocks/StrategyManagerMock.sol index edb0beab9..dfe198ab6 100644 --- a/src/test/mocks/StrategyManagerMock.sol +++ b/src/test/mocks/StrategyManagerMock.sol @@ -136,5 +136,15 @@ contract StrategyManagerMock is function addStrategiesToDepositWhitelist(IStrategy[] calldata /*strategiesToWhitelist*/) external pure {} - function removeStrategiesFromDepositWhitelist(IStrategy[] calldata /*strategiesToRemoveFromWhitelist*/) external pure {} + function removeStrategiesFromDepositWhitelist(IStrategy[] calldata /*strategiesToRemoveFromWhitelist*/) external pure {} + + function undelegate() external pure {} + + event ForceTotalWithdrawalCalled(address staker); + + function forceTotalWithdrawal(address staker) external returns (bytes32) { + bytes32 emptyReturnValue; + emit ForceTotalWithdrawalCalled(staker); + return emptyReturnValue; + } } \ No newline at end of file diff --git a/src/test/unit/BLSOperatorStateRetrieverUnit.t.sol b/src/test/unit/BLSOperatorStateRetrieverUnit.t.sol index a93ea223a..405efb142 100644 --- a/src/test/unit/BLSOperatorStateRetrieverUnit.t.sol +++ b/src/test/unit/BLSOperatorStateRetrieverUnit.t.sol @@ -104,16 +104,16 @@ contract BLSOperatorStateRetrieverUnitTests is MockAVSDeployer { nonSignerOperatorIds ); - assertEq(checkSignaturesIndices.nonSignerQuorumBitmapIndices.length, 0); - assertEq(checkSignaturesIndices.quorumApkIndices.length, allInclusiveQuorumNumbers.length); - assertEq(checkSignaturesIndices.totalStakeIndices.length, allInclusiveQuorumNumbers.length); - assertEq(checkSignaturesIndices.nonSignerStakeIndices.length, allInclusiveQuorumNumbers.length); + assertEq(checkSignaturesIndices.nonSignerQuorumBitmapIndices.length, 0, "nonSignerQuorumBitmapIndices should be empty if no nonsigners"); + assertEq(checkSignaturesIndices.quorumApkIndices.length, allInclusiveQuorumNumbers.length, "quorumApkIndices should be the number of quorums queried for"); + assertEq(checkSignaturesIndices.totalStakeIndices.length, allInclusiveQuorumNumbers.length, "totalStakeIndices should be the number of quorums queried for"); + assertEq(checkSignaturesIndices.nonSignerStakeIndices.length, allInclusiveQuorumNumbers.length, "nonSignerStakeIndices should be the number of quorums queried for"); // assert the indices are the number of registered operators for the quorum minus 1 for (uint8 i = 0; i < allInclusiveQuorumNumbers.length; i++) { uint8 quorumNumber = uint8(allInclusiveQuorumNumbers[i]); - assertEq(checkSignaturesIndices.quorumApkIndices[i], expectedOperatorOverallIndices[quorumNumber].length - 1); - assertEq(checkSignaturesIndices.totalStakeIndices[i], expectedOperatorOverallIndices[quorumNumber].length - 1); + assertEq(checkSignaturesIndices.quorumApkIndices[i], expectedOperatorOverallIndices[quorumNumber].length - 1, "quorumApkIndex should be the number of registered operators for the quorum minus 1"); + assertEq(checkSignaturesIndices.totalStakeIndices[i], expectedOperatorOverallIndices[quorumNumber].length - 1, "totalStakeIndex should be the number of registered operators for the quorum minus 1"); } } @@ -147,25 +147,25 @@ contract BLSOperatorStateRetrieverUnitTests is MockAVSDeployer { nonSignerOperatorIds ); - assertEq(checkSignaturesIndices.nonSignerQuorumBitmapIndices.length, nonSignerOperatorIds.length); - assertEq(checkSignaturesIndices.quorumApkIndices.length, allInclusiveQuorumNumbers.length); - assertEq(checkSignaturesIndices.totalStakeIndices.length, allInclusiveQuorumNumbers.length); - assertEq(checkSignaturesIndices.nonSignerStakeIndices.length, allInclusiveQuorumNumbers.length); + assertEq(checkSignaturesIndices.nonSignerQuorumBitmapIndices.length, nonSignerOperatorIds.length, "nonSignerQuorumBitmapIndices should be the number of nonsigners"); + assertEq(checkSignaturesIndices.quorumApkIndices.length, allInclusiveQuorumNumbers.length, "quorumApkIndices should be the number of quorums queried for"); + assertEq(checkSignaturesIndices.totalStakeIndices.length, allInclusiveQuorumNumbers.length, "totalStakeIndices should be the number of quorums queried for"); + assertEq(checkSignaturesIndices.nonSignerStakeIndices.length, allInclusiveQuorumNumbers.length, "nonSignerStakeIndices should be the number of quorums queried for"); // assert the indices are the number of registered operators for the quorum minus 1 for (uint8 i = 0; i < allInclusiveQuorumNumbers.length; i++) { uint8 quorumNumber = uint8(allInclusiveQuorumNumbers[i]); - assertEq(checkSignaturesIndices.quorumApkIndices[i], expectedOperatorOverallIndices[quorumNumber].length - 1); - assertEq(checkSignaturesIndices.totalStakeIndices[i], expectedOperatorOverallIndices[quorumNumber].length - 1); + assertEq(checkSignaturesIndices.quorumApkIndices[i], expectedOperatorOverallIndices[quorumNumber].length - 1, "quorumApkIndex should be the number of registered operators for the quorum minus 1"); + assertEq(checkSignaturesIndices.totalStakeIndices[i], expectedOperatorOverallIndices[quorumNumber].length - 1, "totalStakeIndex should be the number of registered operators for the quorum minus 1"); } // assert the quorum bitmap and stake indices are zero because there have been no kicks or stake updates for (uint i = 0; i < nonSignerOperatorIds.length; i++) { - assertEq(checkSignaturesIndices.nonSignerQuorumBitmapIndices[i], 0); + assertEq(checkSignaturesIndices.nonSignerQuorumBitmapIndices[i], 0, "nonSignerQuorumBitmapIndices should be zero because there have been no kicks"); } for (uint i = 0; i < checkSignaturesIndices.nonSignerStakeIndices.length; i++) { for (uint j = 0; j < checkSignaturesIndices.nonSignerStakeIndices[i].length; j++) { - assertEq(checkSignaturesIndices.nonSignerStakeIndices[i][j], 0); + assertEq(checkSignaturesIndices.nonSignerStakeIndices[i][j], 0, "nonSignerStakeIndices should be zero because there have been no stake updates past the first one"); } } } diff --git a/src/test/unit/BLSRegistryCoordinatorWithIndicesUnit.t.sol b/src/test/unit/BLSRegistryCoordinatorWithIndicesUnit.t.sol index f0188f612..4c261d259 100644 --- a/src/test/unit/BLSRegistryCoordinatorWithIndicesUnit.t.sol +++ b/src/test/unit/BLSRegistryCoordinatorWithIndicesUnit.t.sol @@ -368,7 +368,7 @@ contract BLSRegistryCoordinatorWithIndicesUnit is MockAVSDeployer { uint256[] memory quorumBitmaps = new uint256[](numOperators); for (uint i = 0; i < numOperators; i++) { // limit to maxQuorumsToRegisterFor quorums via mask so we don't run out of gas, make them all register for quorum 0 as well - quorumBitmaps[i] = uint256(keccak256(abi.encodePacked("quorumBitmap", pseudoRandomNumber, i))) & (maxQuorumsToRegisterFor << 1 - 1) | 1; + quorumBitmaps[i] = uint256(keccak256(abi.encodePacked("quorumBitmap", pseudoRandomNumber, i))) & (1 << maxQuorumsToRegisterFor - 1) | 1; } cheats.roll(registrationBlockNumber); @@ -651,31 +651,12 @@ contract BLSRegistryCoordinatorWithIndicesUnit is MockAVSDeployer { registryCoordinator.registerOperatorWithCoordinator(quorumNumbers, operatorToRegisterPubKey, defaultSocket, operatorKickParams); } - function testRegisterOperatorWithCoordinator_PP() public { - //create the quorum numbers + function testUpdateSocket() public { bytes memory quorumNumbers = new bytes(1); - quorumNumbers[0] = bytes1(0); - - //create the g1 point - BN254.G1Point memory pubKey; - pubKey.X = 6005246670872149419078671036095145476947522049019278055157211161021967739575; - pubKey.Y = 18964291258812839254497845242312850792612198462429467760274856936515915651672; - // pubKey = pubKey.scalar_mul(12279165382821919694974402004679820771477260886196601546024512883505555857144); - - emit log_named_uint("pubkey.X", pubKey.X); - emit log_named_uint("pubkey.Y", pubKey.Y); - - string memory socket = "localhost:32003"; - - pubkeyCompendium.setBLSPublicKey(defaultOperator, pubKey); - stakeRegistry.setOperatorWeight(0, defaultOperator, 1 ether); - - cheats.prank(defaultOperator); - registryCoordinator.registerOperatorWithCoordinator(quorumNumbers, pubKey, socket); - } + quorumNumbers[0] = bytes1(defaultQuorumNumber); - function testUpdateSocket() public { - testRegisterOperatorWithCoordinator_PP(); + uint256 quorumBitmap = BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers); + _registerOperatorWithCoordinator(defaultOperator, quorumBitmap, defaultPubKey); cheats.prank(defaultOperator); cheats.expectEmit(true, true, true, true, address(registryCoordinator)); diff --git a/src/test/unit/BLSSignatureCheckerUnit.t.sol b/src/test/unit/BLSSignatureCheckerUnit.t.sol index 386156e4c..8585d6ab7 100644 --- a/src/test/unit/BLSSignatureCheckerUnit.t.sol +++ b/src/test/unit/BLSSignatureCheckerUnit.t.sol @@ -15,6 +15,9 @@ contract BLSSignatureCheckerUnitTests is BLSMockAVSDeployer { blsSignatureChecker = new BLSSignatureChecker(registryCoordinator); } + // this test checks that a valid signature from maxOperatorsToRegister with a random number of nonsigners is checked + // correctly on the BLSSignatureChecker contract when all operators are only regsitered for a single quorum and + // the signature is only checked for stakes on that quorum function testBLSSignatureChecker_SingleQuorum_Valid(uint256 pseudoRandomNumber) public { uint256 numNonSigners = pseudoRandomNumber % (maxOperatorsToRegister - 1); @@ -25,7 +28,10 @@ contract BLSSignatureCheckerUnitTests is BLSMockAVSDeployer { _registerSignatoriesAndGetNonSignerStakeAndSignatureRandom(pseudoRandomNumber, numNonSigners, quorumBitmap); uint256 gasBefore = gasleft(); - blsSignatureChecker.checkSignatures( + ( + BLSSignatureChecker.QuorumStakeTotals memory quorumStakeTotals, + bytes32 signatoryRecordHash + ) = blsSignatureChecker.checkSignatures( msgHash, quorumNumbers, referenceBlockNumber, @@ -33,12 +39,16 @@ contract BLSSignatureCheckerUnitTests is BLSMockAVSDeployer { ); uint256 gasAfter = gasleft(); emit log_named_uint("gasUsed", gasBefore - gasAfter); + assertTrue(quorumStakeTotals.signedStakeForQuorum[0] > 0); // 0 nonSigners: 159908 // 1 nonSigner: 178683 // 2 nonSigners: 197410 } + // this test checks that a valid signature from maxOperatorsToRegister with a random number of nonsigners is checked + // correctly on the BLSSignatureChecker contract when all operators are registered for the first 100 quorums + // and the signature is only checked for stakes on those quorums function testBLSSignatureChecker_100Quorums_Valid(uint256 pseudoRandomNumber) public { uint256 numNonSigners = pseudoRandomNumber % (maxOperatorsToRegister - 1); @@ -218,6 +228,7 @@ contract BLSSignatureCheckerUnitTests is BLSMockAVSDeployer { // set the sigma to a different value nonSignerStakesAndSignature.sigma.X++; + // expect a non-specific low-level revert, since this call will ultimately fail as part of the precompile call cheats.expectRevert(); blsSignatureChecker.checkSignatures( msgHash, diff --git a/src/test/unit/DelegationUnit.t.sol b/src/test/unit/DelegationUnit.t.sol index b14183611..30a098d4b 100644 --- a/src/test/unit/DelegationUnit.t.sol +++ b/src/test/unit/DelegationUnit.t.sol @@ -1,14 +1,15 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.9; +import "@openzeppelin/contracts/mocks/ERC1271WalletMock.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; + import "forge-std/Test.sol"; import "../mocks/StrategyManagerMock.sol"; - import "../mocks/SlasherMock.sol"; import "../EigenLayerTestHelper.t.sol"; import "../mocks/ERC20Mock.sol"; -import "../mocks/DelegationTermsMock.sol"; import "../Delegation.t.sol"; contract DelegationUnitTests is EigenLayerTestHelper { @@ -16,23 +17,45 @@ contract DelegationUnitTests is EigenLayerTestHelper { StrategyManagerMock strategyManagerMock; SlasherMock slasherMock; DelegationManager delegationManager; - DelegationTermsMock delegationTermsMock; DelegationManager delegationManagerImplementation; StrategyBase strategyImplementation; StrategyBase strategyMock; + uint256 delegationSignerPrivateKey = uint256(0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80); + uint256 stakerPrivateKey = uint256(123456789); + + // empty string reused across many tests + string emptyStringForMetadataURI; + + // @notice Emitted when a new operator registers in EigenLayer and provides their OperatorDetails. + event OperatorRegistered(address indexed operator, IDelegationManager.OperatorDetails operatorDetails); + + // @notice Emitted when an operator updates their OperatorDetails to @param newOperatorDetails + event OperatorDetailsModified(address indexed operator, IDelegationManager.OperatorDetails newOperatorDetails); - uint256 GWEI_TO_WEI = 1e9; + /** + * @notice Emitted when @param operator indicates that they are updating their MetadataURI string + * @dev Note that these strings are *never stored in storage* and are instead purely emitted in events for off-chain indexing + */ + event OperatorMetadataURIUpdated(address indexed operator, string metadataURI); - event OnDelegationReceivedCallFailure(IDelegationTerms indexed delegationTerms, bytes32 returnData); - event OnDelegationWithdrawnCallFailure(IDelegationTerms indexed delegationTerms, bytes32 returnData); + // @notice Emitted when @param staker delegates to @param operator. + event StakerDelegated(address indexed staker, address indexed operator); + // @notice Emitted when @param staker undelegates from @param operator. + event StakerUndelegated(address indexed staker, address indexed operator); + + // @notice reuseable modifier + associated mapping for filtering out weird fuzzed inputs, like making calls from the ProxyAdmin or the zero address + mapping(address => bool) public addressIsExcludedFromFuzzedInputs; + modifier filterFuzzedAddressInputs(address fuzzedAddress) { + cheats.assume(!addressIsExcludedFromFuzzedInputs[fuzzedAddress]); + _; + } function setUp() override virtual public{ EigenLayerDeployer.setUp(); slasherMock = new SlasherMock(); - delegationTermsMock = new DelegationTermsMock(); delegationManager = DelegationManager(address(new TransparentUpgradeableProxy(address(emptyContract), address(eigenLayerProxyAdmin), ""))); strategyManagerMock = new StrategyManagerMock(); @@ -42,9 +65,11 @@ contract DelegationUnitTests is EigenLayerTestHelper { eigenLayerProxyAdmin.upgrade(TransparentUpgradeableProxy(payable(address(delegationManager))), address(delegationManagerImplementation)); cheats.stopPrank(); - delegationManager.initialize(address(this), eigenLayerPauserReg, 0); + address initalOwner = address(this); + uint256 initialPausedStatus = 0; + delegationManager.initialize(initalOwner, eigenLayerPauserReg, initialPausedStatus); - strategyImplementation = new StrategyBase(strategyManager); + strategyImplementation = new StrategyBase(strategyManagerMock); strategyMock = StrategyBase( address( @@ -56,224 +81,1201 @@ contract DelegationUnitTests is EigenLayerTestHelper { ) ); + // excude the zero address and the proxyAdmin from fuzzed inputs + addressIsExcludedFromFuzzedInputs[address(0)] = true; + addressIsExcludedFromFuzzedInputs[address(eigenLayerProxyAdmin)] = true; + + // check setup (constructor + initializer) + require(delegationManager.strategyManager() == strategyManagerMock, + "constructor / initializer incorrect, strategyManager set wrong"); + require(delegationManager.slasher() == slasherMock, + "constructor / initializer incorrect, slasher set wrong"); + require(delegationManager.pauserRegistry() == eigenLayerPauserReg, + "constructor / initializer incorrect, pauserRegistry set wrong"); + require(delegationManager.owner() == initalOwner, + "constructor / initializer incorrect, owner set wrong"); + require(delegationManager.paused() == initialPausedStatus, + "constructor / initializer incorrect, paused status set wrong"); } - function testReinitializeDelegation() public{ + // @notice Verifies that the DelegationManager cannot be iniitalized multiple times + function testCannotReinitializeDelegationManager() public { cheats.expectRevert(bytes("Initializable: contract is already initialized")); delegationManager.initialize(address(this), eigenLayerPauserReg, 0); } - function testBadECDSASignatureExpiry(address staker, address operator, uint256 expiry, bytes memory signature) public{ - cheats.assume(expiry < block.timestamp); - cheats.expectRevert(bytes("DelegationManager.delegateToBySignature: delegation signature expired")); - delegationManager.delegateToBySignature(staker, operator, expiry, signature); + /** + * @notice `operator` registers via calling `DelegationManager.registerAsOperator(operatorDetails, metadataURI)` + * Should be able to set any parameters, other than setting their `earningsReceiver` to the zero address or too high value for `stakerOptOutWindowBlocks` + * The set parameters should match the desired parameters (correct storage update) + * Operator becomes delegated to themselves + * Properly emits events – especially the `OperatorRegistered` event, but also `StakerDelegated` & `OperatorDetailsModified` events + * Reverts appropriately if operator was already delegated to someone (including themselves, i.e. they were already an operator) + * @param operator and @param operatorDetails are fuzzed inputs + */ + function testRegisterAsOperator(address operator, IDelegationManager.OperatorDetails memory operatorDetails, string memory metadataURI) public + filterFuzzedAddressInputs(operator) + { + // filter out zero address since people can't delegate to the zero address and operators are delegated to themselves + cheats.assume(operator != address(0)); + // filter out zero address since people can't set their earningsReceiver address to the zero address (special test case to verify) + cheats.assume(operatorDetails.earningsReceiver != address(0)); + // filter out disallowed stakerOptOutWindowBlocks values + cheats.assume(operatorDetails.stakerOptOutWindowBlocks <= delegationManager.MAX_STAKER_OPT_OUT_WINDOW_BLOCKS()); + + cheats.startPrank(operator); + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit OperatorDetailsModified(operator, operatorDetails); + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit StakerDelegated(operator, operator); + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit OperatorRegistered(operator, operatorDetails); + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit OperatorMetadataURIUpdated(operator, metadataURI); + + delegationManager.registerAsOperator(operatorDetails, metadataURI); + + require(operatorDetails.earningsReceiver == delegationManager.earningsReceiver(operator), "earningsReceiver not set correctly"); + require(operatorDetails.delegationApprover == delegationManager.delegationApprover(operator), "delegationApprover not set correctly"); + require(operatorDetails.stakerOptOutWindowBlocks == delegationManager.stakerOptOutWindowBlocks(operator), "stakerOptOutWindowBlocks not set correctly"); + require(delegationManager.delegatedTo(operator) == operator, "operator not delegated to self"); + cheats.stopPrank(); } - function testUndelegateFromNonStrategyManagerAddress(address undelegator) public fuzzedAddress(undelegator) { - cheats.assume(undelegator != address(strategyManagerMock)); - cheats.expectRevert(bytes("onlyStrategyManager")); - cheats.startPrank(undelegator); - delegationManager.undelegate(address(this)); + /** + * @notice Verifies that an operator cannot register with `stakerOptOutWindowBlocks` set larger than `MAX_STAKER_OPT_OUT_WINDOW_BLOCKS` + * @param operatorDetails is a fuzzed input + */ + function testCannotRegisterAsOperatorWithDisallowedStakerOptOutWindowBlocks(IDelegationManager.OperatorDetails memory operatorDetails) public { + // filter out zero address since people can't set their earningsReceiver address to the zero address (special test case to verify) + cheats.assume(operatorDetails.earningsReceiver != address(0)); + // filter out *allowed* stakerOptOutWindowBlocks values + cheats.assume(operatorDetails.stakerOptOutWindowBlocks > delegationManager.MAX_STAKER_OPT_OUT_WINDOW_BLOCKS()); + + cheats.startPrank(operator); + cheats.expectRevert(bytes("DelegationManager._setOperatorDetails: stakerOptOutWindowBlocks cannot be > MAX_STAKER_OPT_OUT_WINDOW_BLOCKS")); + delegationManager.registerAsOperator(operatorDetails, emptyStringForMetadataURI); + cheats.stopPrank(); + } + + /** + * @notice Verifies that an operator cannot register with `earningsReceiver` set to the zero address + * @dev This is an important check since we check `earningsReceiver != address(0)` to check if an address is an operator! + */ + function testCannotRegisterAsOperatorWithZeroAddressAsEarningsReceiver() public { + cheats.startPrank(operator); + IDelegationManager.OperatorDetails memory operatorDetails; + cheats.expectRevert(bytes("DelegationManager._setOperatorDetails: cannot set `earningsReceiver` to zero address")); + delegationManager.registerAsOperator(operatorDetails, emptyStringForMetadataURI); + cheats.stopPrank(); } - function testUndelegateByOperatorFromThemselves(address operator) public fuzzedAddress(operator) { + // @notice Verifies that someone cannot successfully call `DelegationManager.registerAsOperator(operatorDetails)` again after registering for the first time + function testCannotRegisterAsOperatorMultipleTimes(address operator, IDelegationManager.OperatorDetails memory operatorDetails) public + filterFuzzedAddressInputs(operator) + { + testRegisterAsOperator(operator, operatorDetails, emptyStringForMetadataURI); cheats.startPrank(operator); - delegationManager.registerAsOperator(IDelegationTerms(address(this))); + cheats.expectRevert(bytes("DelegationManager.registerAsOperator: operator has already registered")); + delegationManager.registerAsOperator(operatorDetails, emptyStringForMetadataURI); cheats.stopPrank(); - cheats.expectRevert(bytes("DelegationManager.undelegate: operators cannot undelegate from themselves")); - - cheats.startPrank(address(strategyManagerMock)); - delegationManager.undelegate(operator); + } + + // @notice Verifies that a staker who is actively delegated to an operator cannot register as an operator (without first undelegating, at least) + function testCannotRegisterAsOperatorWhileDelegated(address staker, IDelegationManager.OperatorDetails memory operatorDetails) public + filterFuzzedAddressInputs(staker) + { + // filter out disallowed stakerOptOutWindowBlocks values + cheats.assume(operatorDetails.stakerOptOutWindowBlocks <= delegationManager.MAX_STAKER_OPT_OUT_WINDOW_BLOCKS()); + // filter out zero address since people can't set their earningsReceiver address to the zero address (special test case to verify) + cheats.assume(operatorDetails.earningsReceiver != address(0)); + + // register *this contract* as an operator + address operator = address(this); + IDelegationManager.OperatorDetails memory _operatorDetails = IDelegationManager.OperatorDetails({ + earningsReceiver: operator, + delegationApprover: address(0), + stakerOptOutWindowBlocks: 0 + }); + testRegisterAsOperator(operator, _operatorDetails, emptyStringForMetadataURI); + + // delegate from the `staker` to the operator + cheats.startPrank(staker); + IDelegationManager.SignatureWithExpiry memory approverSignatureAndExpiry; + delegationManager.delegateTo(operator, approverSignatureAndExpiry); + + cheats.expectRevert(bytes("DelegationManager._delegate: staker is already actively delegated")); + delegationManager.registerAsOperator(operatorDetails, emptyStringForMetadataURI); + cheats.stopPrank(); } - function testIncreaseDelegatedSharesFromNonStrategyManagerAddress(address operator, uint256 shares) public fuzzedAddress(operator) { - cheats.assume(operator != address(strategyManagerMock)); - cheats.expectRevert(bytes("onlyStrategyManager")); + /** + * @notice Tests that an operator can modify their OperatorDetails by calling `DelegationManager.modifyOperatorDetails` + * Should be able to set any parameters, other than setting their `earningsReceiver` to the zero address or too high value for `stakerOptOutWindowBlocks` + * The set parameters should match the desired parameters (correct storage update) + * Properly emits an `OperatorDetailsModified` event + * Reverts appropriately if the caller is not an operator + * Reverts if operator tries to decrease their `stakerOptOutWindowBlocks` parameter + * @param initialOperatorDetails and @param modifiedOperatorDetails are fuzzed inputs + */ + function testModifyOperatorParameters( + IDelegationManager.OperatorDetails memory initialOperatorDetails, + IDelegationManager.OperatorDetails memory modifiedOperatorDetails + ) public { + address operator = address(this); + testRegisterAsOperator(operator, initialOperatorDetails, emptyStringForMetadataURI); + // filter out zero address since people can't set their earningsReceiver address to the zero address (special test case to verify) + cheats.assume(modifiedOperatorDetails.earningsReceiver != address(0)); + cheats.startPrank(operator); - delegationManager.increaseDelegatedShares(operator, strategyMock, shares); + + // either it fails for trying to set the stakerOptOutWindowBlocks + if (modifiedOperatorDetails.stakerOptOutWindowBlocks > delegationManager.MAX_STAKER_OPT_OUT_WINDOW_BLOCKS()) { + cheats.expectRevert(bytes("DelegationManager._setOperatorDetails: stakerOptOutWindowBlocks cannot be > MAX_STAKER_OPT_OUT_WINDOW_BLOCKS")); + delegationManager.modifyOperatorDetails(modifiedOperatorDetails); + // or the transition is allowed, + } else if (modifiedOperatorDetails.stakerOptOutWindowBlocks >= initialOperatorDetails.stakerOptOutWindowBlocks) { + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit OperatorDetailsModified(operator, modifiedOperatorDetails); + delegationManager.modifyOperatorDetails(modifiedOperatorDetails); + + require(modifiedOperatorDetails.earningsReceiver == delegationManager.earningsReceiver(operator), "earningsReceiver not set correctly"); + require(modifiedOperatorDetails.delegationApprover == delegationManager.delegationApprover(operator), "delegationApprover not set correctly"); + require(modifiedOperatorDetails.stakerOptOutWindowBlocks == delegationManager.stakerOptOutWindowBlocks(operator), "stakerOptOutWindowBlocks not set correctly"); + require(delegationManager.delegatedTo(operator) == operator, "operator not delegated to self"); + // or else the transition is disallowed + } else { + cheats.expectRevert(bytes("DelegationManager._setOperatorDetails: stakerOptOutWindowBlocks cannot be decreased")); + delegationManager.modifyOperatorDetails(modifiedOperatorDetails); + } + + cheats.stopPrank(); } - function testDecreaseDelegatedSharesFromNonStrategyManagerAddress( - address operator, - IStrategy[] memory strategies, - uint256[] memory shareAmounts - ) public fuzzedAddress(operator) { - cheats.assume(operator != address(strategyManagerMock)); - cheats.expectRevert(bytes("onlyStrategyManager")); + // @notice Tests that an operator who calls `updateOperatorMetadataURI` will correctly see an `OperatorMetadataURIUpdated` event emitted with their input + function testUpdateOperatorMetadataURI(string memory metadataURI) public { + // register *this contract* as an operator + address operator = address(this); + IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + earningsReceiver: operator, + delegationApprover: address(0), + stakerOptOutWindowBlocks: 0 + }); + testRegisterAsOperator(operator, operatorDetails, emptyStringForMetadataURI); + + // call `updateOperatorMetadataURI` and check for event cheats.startPrank(operator); - delegationManager.decreaseDelegatedShares(operator, strategies, shareAmounts); + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit OperatorMetadataURIUpdated(operator, metadataURI); + delegationManager.updateOperatorMetadataURI(metadataURI); + cheats.stopPrank(); } - function testDelegateWhenOperatorIsFrozen(address operator, address staker) public fuzzedAddress(operator) fuzzedAddress(staker) { - cheats.assume(operator != staker); - + // @notice Tests that an address which is not an operator cannot successfully call `updateOperatorMetadataURI`. + function testCannotUpdateOperatorMetadataURIWithoutRegisteringFirst() public { + address operator = address(this); + require(!delegationManager.isOperator(operator), "bad test setup"); + cheats.startPrank(operator); - delegationManager.registerAsOperator(IDelegationTerms(address(this))); + cheats.expectRevert(bytes("DelegationManager.updateOperatorMetadataURI: caller must be an operator")); + delegationManager.updateOperatorMetadataURI(emptyStringForMetadataURI); cheats.stopPrank(); + } - slasherMock.setOperatorFrozenStatus(operator, true); - cheats.expectRevert(bytes("DelegationManager._delegate: cannot delegate to a frozen operator")); + /** + * @notice Verifies that an operator cannot modify their `earningsReceiver` address to set it to the zero address + * @dev This is an important check since we check `earningsReceiver != address(0)` to check if an address is an operator! + */ + function testCannotModifyEarningsReceiverAddressToZeroAddress() public { + // register *this contract* as an operator + address operator = address(this); + IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + earningsReceiver: operator, + delegationApprover: address(0), + stakerOptOutWindowBlocks: 0 + }); + testRegisterAsOperator(operator, operatorDetails, emptyStringForMetadataURI); + + operatorDetails.earningsReceiver = address(0); + cheats.expectRevert(bytes("DelegationManager._setOperatorDetails: cannot set `earningsReceiver` to zero address")); + delegationManager.modifyOperatorDetails(operatorDetails); + } + + /** + * @notice `staker` delegates to an operator who does not require any signature verification (i.e. the operator’s `delegationApprover` address is set to the zero address) + * via the `staker` calling `DelegationManager.delegateTo` + * The function should pass with any `operatorSignature` input (since it should be unused) + * Properly emits a `StakerDelegated` event + * Staker is correctly delegated after the call (i.e. correct storage update) + * Reverts if the staker is already delegated (to the operator or to anyone else) + * Reverts if the ‘operator’ is not actually registered as an operator + */ + function testDelegateToOperatorWhoAcceptsAllStakers(address staker, IDelegationManager.SignatureWithExpiry memory approverSignatureAndExpiry) public + filterFuzzedAddressInputs(staker) + { + // register *this contract* as an operator + address operator = address(this); + // filter inputs, since this will fail when the staker is already registered as an operator + cheats.assume(staker != operator); + + IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + earningsReceiver: operator, + delegationApprover: address(0), + stakerOptOutWindowBlocks: 0 + }); + testRegisterAsOperator(operator, operatorDetails, emptyStringForMetadataURI); + + // fetch the delegationApprover's current nonce + uint256 currentApproverNonce = delegationManager.delegationApproverNonce(delegationManager.delegationApprover(operator)); + + // delegate from the `staker` to the operator cheats.startPrank(staker); - delegationManager.delegateTo(operator); + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit StakerDelegated(staker, operator); + delegationManager.delegateTo(operator, approverSignatureAndExpiry); cheats.stopPrank(); + + require(delegationManager.isDelegated(staker), "staker not delegated correctly"); + require(delegationManager.delegatedTo(staker) == operator, "staker delegated to the wrong address"); + require(!delegationManager.isOperator(staker), "staker incorrectly registered as operator"); + + require(delegationManager.delegationApproverNonce(delegationManager.delegationApprover(operator)) == currentApproverNonce, + "delegationApprover nonce incremented inappropriately"); } - function testDelegateWhenStakerHasExistingDelegation(address staker, address operator, address operator2) public - fuzzedAddress(staker) - fuzzedAddress(operator) - fuzzedAddress(operator2) + /** + * @notice Delegates from `staker` to an operator, then verifies that the `staker` cannot delegate to another `operator` (at least without first undelegating) + */ + function testCannotDelegateWhileDelegated(address staker, address operator, IDelegationManager.SignatureWithExpiry memory approverSignatureAndExpiry) public + filterFuzzedAddressInputs(staker) + filterFuzzedAddressInputs(operator) { - cheats.assume(operator != operator2); + // filter out input since if the staker tries to delegate again after registering as an operator, we will revert earlier than this test is designed to check cheats.assume(staker != operator); - cheats.assume(staker != operator2); - cheats.startPrank(operator); - delegationManager.registerAsOperator(IDelegationTerms(address(11))); - cheats.stopPrank(); + // delegate from the staker to an operator + testDelegateToOperatorWhoAcceptsAllStakers(staker, approverSignatureAndExpiry); - cheats.startPrank(operator2); - delegationManager.registerAsOperator(IDelegationTerms(address(10))); - cheats.stopPrank(); + // register another operator + // filter out this contract, since we already register it as an operator in the above step + cheats.assume(operator != address(this)); + IDelegationManager.OperatorDetails memory _operatorDetails = IDelegationManager.OperatorDetails({ + earningsReceiver: operator, + delegationApprover: address(0), + stakerOptOutWindowBlocks: 0 + }); + testRegisterAsOperator(operator, _operatorDetails, emptyStringForMetadataURI); + // try to delegate again and check that the call reverts cheats.startPrank(staker); - delegationManager.delegateTo(operator); + cheats.expectRevert(bytes("DelegationManager._delegate: staker is already actively delegated")); + delegationManager.delegateTo(operator, approverSignatureAndExpiry); cheats.stopPrank(); + } + // @notice Verifies that `staker` cannot delegate to an unregistered `operator` + function testCannotDelegateToUnregisteredOperator(address staker, address operator) public + filterFuzzedAddressInputs(staker) + filterFuzzedAddressInputs(operator) + { + require(!delegationManager.isOperator(operator), "incorrect test input?"); + + // try to delegate and check that the call reverts cheats.startPrank(staker); - cheats.expectRevert(bytes("DelegationManager._delegate: staker has existing delegation")); - delegationManager.delegateTo(operator2); + cheats.expectRevert(bytes("DelegationManager._delegate: operator is not registered in EigenLayer")); + IDelegationManager.SignatureWithExpiry memory approverSignatureAndExpiry; + delegationManager.delegateTo(operator, approverSignatureAndExpiry); cheats.stopPrank(); } - function testDelegationToUnregisteredOperator(address operator) public{ - cheats.expectRevert(bytes("DelegationManager._delegate: operator has not yet registered as a delegate")); - delegationManager.delegateTo(operator); + /** + * @notice `staker` delegates to an operator who requires signature verification through an EOA (i.e. the operator’s `delegationApprover` address is set to a nonzero EOA) + * via the `staker` calling `DelegationManager.delegateTo` + * The function should pass *only with a valid ECDSA signature from the `delegationApprover`, OR if called by the operator or their delegationApprover themselves + * Properly emits a `StakerDelegated` event + * Staker is correctly delegated after the call (i.e. correct storage update) + * Reverts if the staker is already delegated (to the operator or to anyone else) + * Reverts if the ‘operator’ is not actually registered as an operator + */ + function testDelegateToOperatorWhoRequiresECDSASignature(address staker, uint256 expiry) public + filterFuzzedAddressInputs(staker) + { + // filter to only valid `expiry` values + cheats.assume(expiry >= block.timestamp); + + address delegationApprover = cheats.addr(delegationSignerPrivateKey); + + // register *this contract* as an operator + address operator = address(this); + // filter inputs, since this will fail when the staker is already registered as an operator + cheats.assume(staker != operator); + + IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + earningsReceiver: operator, + delegationApprover: delegationApprover, + stakerOptOutWindowBlocks: 0 + }); + testRegisterAsOperator(operator, operatorDetails, emptyStringForMetadataURI); + + // fetch the delegationApprover's current nonce + uint256 currentApproverNonce = delegationManager.delegationApproverNonce(delegationManager.delegationApprover(operator)); + // calculate the delegationSigner's signature + IDelegationManager.SignatureWithExpiry memory approverSignatureAndExpiry = _getApproverSignature(delegationSignerPrivateKey, staker, operator, expiry); + + // delegate from the `staker` to the operator + cheats.startPrank(staker); + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit StakerDelegated(staker, operator); + delegationManager.delegateTo(operator, approverSignatureAndExpiry); + cheats.stopPrank(); + + require(delegationManager.isDelegated(staker), "staker not delegated correctly"); + require(delegationManager.delegatedTo(staker) == operator, "staker delegated to the wrong address"); + require(!delegationManager.isOperator(staker), "staker incorrectly registered as operator"); + + if (staker == operator || staker == delegationManager.delegationApprover(operator)) { + require(delegationManager.delegationApproverNonce(delegationManager.delegationApprover(operator)) == currentApproverNonce, + "delegationApprover nonce incremented inappropriately"); + } else { + require(delegationManager.delegationApproverNonce(delegationManager.delegationApprover(operator)) == currentApproverNonce + 1, + "delegationApprover nonce did not increment"); + } } - function testDelegationWhenPausedNewDelegationIsSet(address operator, address staker) public fuzzedAddress(operator) fuzzedAddress(staker) { - cheats.startPrank(pauser); - delegationManager.pause(1); + /** + * @notice Like `testDelegateToOperatorWhoRequiresECDSASignature` but using an incorrect signature on purpose and checking that reversion occurs + */ + function testDelegateToOperatorWhoRequiresECDSASignature_RevertsWithBadSignature(address staker, uint256 expiry) public + filterFuzzedAddressInputs(staker) + { + // filter to only valid `expiry` values + cheats.assume(expiry >= block.timestamp); + + address delegationApprover = cheats.addr(delegationSignerPrivateKey); + + // register *this contract* as an operator + address operator = address(this); + // filter inputs, since this will fail when the staker is already registered as an operator + cheats.assume(staker != operator); + + IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + earningsReceiver: operator, + delegationApprover: delegationApprover, + stakerOptOutWindowBlocks: 0 + }); + testRegisterAsOperator(operator, operatorDetails, emptyStringForMetadataURI); + + // calculate the signature + IDelegationManager.SignatureWithExpiry memory approverSignatureAndExpiry; + approverSignatureAndExpiry.expiry = expiry; + { + bytes32 digestHash = delegationManager.calculateCurrentDelegationApprovalDigestHash(staker, operator, expiry); + (uint8 v, bytes32 r, bytes32 s) = cheats.sign(delegationSignerPrivateKey, digestHash); + // mess up the signature by flipping v's parity + v = (v == 27 ? 28 : 27); + approverSignatureAndExpiry.signature = abi.encodePacked(r, s, v); + } + + // try to delegate from the `staker` to the operator, and check reversion + cheats.startPrank(staker); + cheats.expectRevert(bytes("EIP1271SignatureUtils.checkSignature_EIP1271: signature not from signer")); + delegationManager.delegateTo(operator, approverSignatureAndExpiry); cheats.stopPrank(); + } + + /** + * @notice Like `testDelegateToOperatorWhoRequiresECDSASignature` but using an invalid expiry on purpose and checking that reversion occurs + */ + function testDelegateToOperatorWhoRequiresECDSASignature_RevertsWithExpiredDelegationApproverSignature(address staker, uint256 expiry) public + filterFuzzedAddressInputs(staker) + { + // roll to a very late timestamp + cheats.roll(type(uint256).max / 2); + // filter to only *invalid* `expiry` values + cheats.assume(expiry < block.timestamp); + + address delegationApprover = cheats.addr(delegationSignerPrivateKey); + + // register *this contract* as an operator + address operator = address(this); + // filter inputs, since this will fail when the staker is already registered as an operator + cheats.assume(staker != operator); + + IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + earningsReceiver: operator, + delegationApprover: delegationApprover, + stakerOptOutWindowBlocks: 0 + }); + testRegisterAsOperator(operator, operatorDetails, emptyStringForMetadataURI); + + // calculate the delegationSigner's signature + IDelegationManager.SignatureWithExpiry memory approverSignatureAndExpiry = _getApproverSignature(delegationSignerPrivateKey, staker, operator, expiry); + // delegate from the `staker` to the operator cheats.startPrank(staker); - cheats.expectRevert(bytes("Pausable: index is paused")); - delegationManager.delegateTo(operator); + cheats.expectRevert(bytes("DelegationManager._delegate: approver signature expired")); + delegationManager.delegateTo(operator, approverSignatureAndExpiry); cheats.stopPrank(); } - function testRevertingDelegationReceivedHook(address operator, address staker) public fuzzedAddress(operator) fuzzedAddress(staker) { - cheats.assume(operator != staker); + /** + * @notice `staker` delegates to an operator who requires signature verification through an EIP1271-compliant contract (i.e. the operator’s `delegationApprover` address is + * set to a nonzero and code-containing address) via the `staker` calling `DelegationManager.delegateTo` + * The function uses OZ's ERC1271WalletMock contract, and thus should pass *only when a valid ECDSA signature from the `owner` of the ERC1271WalletMock contract, + * OR if called by the operator or their delegationApprover themselves + * Properly emits a `StakerDelegated` event + * Staker is correctly delegated after the call (i.e. correct storage update) + * Reverts if the staker is already delegated (to the operator or to anyone else) + * Reverts if the ‘operator’ is not actually registered as an operator + */ + function testDelegateToOperatorWhoRequiresEIP1271Signature(address staker, uint256 expiry) public + filterFuzzedAddressInputs(staker) + { + // filter to only valid `expiry` values + cheats.assume(expiry >= block.timestamp); - delegationTermsMock.setShouldRevert(true); - cheats.startPrank(operator); - delegationManager.registerAsOperator(delegationTermsMock); + address delegationSigner = cheats.addr(delegationSignerPrivateKey); + + // register *this contract* as an operator + address operator = address(this); + // filter inputs, since this will fail when the staker is already registered as an operator + cheats.assume(staker != operator); + + /** + * deploy a ERC1271WalletMock contract with the `delegationSigner` address as the owner, + * so that we can create valid signatures from the `delegationSigner` for the contract to check when called + */ + ERC1271WalletMock wallet = new ERC1271WalletMock(delegationSigner); + + IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + earningsReceiver: operator, + delegationApprover: address(wallet), + stakerOptOutWindowBlocks: 0 + }); + testRegisterAsOperator(operator, operatorDetails, emptyStringForMetadataURI); + + // fetch the delegationApprover's current nonce + uint256 currentApproverNonce = delegationManager.delegationApproverNonce(delegationManager.delegationApprover(operator)); + // calculate the delegationSigner's signature + IDelegationManager.SignatureWithExpiry memory approverSignatureAndExpiry = _getApproverSignature(delegationSignerPrivateKey, staker, operator, expiry); + + // delegate from the `staker` to the operator + cheats.startPrank(staker); + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit StakerDelegated(staker, operator); + delegationManager.delegateTo(operator, approverSignatureAndExpiry); cheats.stopPrank(); + require(delegationManager.isDelegated(staker), "staker not delegated correctly"); + require(delegationManager.delegatedTo(staker) == operator, "staker delegated to the wrong address"); + require(!delegationManager.isOperator(staker), "staker incorrectly registered as operator"); + + // check that the nonce incremented appropriately + if (staker == operator || staker == delegationManager.delegationApprover(operator)) { + require(delegationManager.delegationApproverNonce(delegationManager.delegationApprover(operator)) == currentApproverNonce, + "delegationApprover nonce incremented inappropriately"); + } else { + require(delegationManager.delegationApproverNonce(delegationManager.delegationApprover(operator)) == currentApproverNonce + 1, + "delegationApprover nonce did not increment"); + } + } + + /** + * @notice Like `testDelegateToOperatorWhoRequiresEIP1271Signature` but using a contract that + * returns a value other than the EIP1271 "magic bytes" and checking that reversion occurs appropriately + */ + function testDelegateToOperatorWhoRequiresEIP1271Signature_RevertsOnBadReturnValue(address staker, uint256 expiry) public + filterFuzzedAddressInputs(staker) + { + // filter to only valid `expiry` values + cheats.assume(expiry >= block.timestamp); + + // register *this contract* as an operator + address operator = address(this); + // filter inputs, since this will fail when the staker is already registered as an operator + cheats.assume(staker != operator); + + // deploy a ERC1271MaliciousMock contract that will return an incorrect value when called + ERC1271MaliciousMock wallet = new ERC1271MaliciousMock(); + + IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + earningsReceiver: operator, + delegationApprover: address(wallet), + stakerOptOutWindowBlocks: 0 + }); + testRegisterAsOperator(operator, operatorDetails, emptyStringForMetadataURI); + + // create the signature struct + IDelegationManager.SignatureWithExpiry memory approverSignatureAndExpiry; + approverSignatureAndExpiry.expiry = expiry; + + // try to delegate from the `staker` to the operator, and check reversion cheats.startPrank(staker); - // cheats.expectEmit(true, true, true, true, address(delegationManager)); - cheats.expectEmit(true, true, true, true); - emit OnDelegationReceivedCallFailure(delegationTermsMock, 0x08c379a000000000000000000000000000000000000000000000000000000000); - delegationManager.delegateTo(operator); + // because the ERC1271MaliciousMock contract returns the wrong amount of data, we get a low-level "EvmError: Revert" message here rather than the error message bubbling up + // cheats.expectRevert(bytes("EIP1271SignatureUtils.checkSignature_EIP1271: ERC1271 signature verification failed")); + cheats.expectRevert(); + delegationManager.delegateTo(operator, approverSignatureAndExpiry); cheats.stopPrank(); } - function testRevertingDelegationWithdrawnHook( - address operator, - address staker - ) public fuzzedAddress(operator) fuzzedAddress(staker) { - cheats.assume(operator != staker); - delegationTermsMock.setShouldRevert(true); + /** + * @notice `staker` becomes delegated to an operator who does not require any signature verification (i.e. the operator’s `delegationApprover` address is set to the zero address) + * via the `caller` calling `DelegationManager.delegateToBySignature` + * The function should pass with any `operatorSignature` input (since it should be unused) + * The function should pass only with a valid `stakerSignatureAndExpiry` input + * Properly emits a `StakerDelegated` event + * Staker is correctly delegated after the call (i.e. correct storage update) + * Reverts if the staker is already delegated (to the operator or to anyone else) + * Reverts if the ‘operator’ is not actually registered as an operator + */ + function testDelegateBySignatureToOperatorWhoAcceptsAllStakers(address caller, uint256 expiry) public + filterFuzzedAddressInputs(caller) + { + // filter to only valid `expiry` values + cheats.assume(expiry >= block.timestamp); - cheats.startPrank(operator); - delegationManager.registerAsOperator(delegationTermsMock); + address staker = cheats.addr(stakerPrivateKey); + + // register *this contract* as an operator + address operator = address(this); + // filter inputs, since this will fail when the staker is already registered as an operator + cheats.assume(staker != operator); + + IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + earningsReceiver: operator, + delegationApprover: address(0), + stakerOptOutWindowBlocks: 0 + }); + testRegisterAsOperator(operator, operatorDetails, emptyStringForMetadataURI); + + // fetch the delegationApprover's current nonce + uint256 currentApproverNonce = delegationManager.delegationApproverNonce(delegationManager.delegationApprover(operator)); + // fetch the staker's current nonce + uint256 currentStakerNonce = delegationManager.stakerNonce(staker); + // calculate the staker signature + IDelegationManager.SignatureWithExpiry memory stakerSignatureAndExpiry = _getStakerSignature(stakerPrivateKey, operator, expiry); + + // delegate from the `staker` to the operator, via having the `caller` call `DelegationManager.delegateToBySignature` + cheats.startPrank(caller); + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit StakerDelegated(staker, operator); + // use an empty approver signature input since none is needed / the input is unchecked + IDelegationManager.SignatureWithExpiry memory approverSignatureAndExpiry; + delegationManager.delegateToBySignature(staker, operator, stakerSignatureAndExpiry, approverSignatureAndExpiry); cheats.stopPrank(); - cheats.startPrank(staker); - delegationManager.delegateTo(operator); + // check all the delegation status changes + require(delegationManager.isDelegated(staker), "staker not delegated correctly"); + require(delegationManager.delegatedTo(staker) == operator, "staker delegated to the wrong address"); + require(!delegationManager.isOperator(staker), "staker incorrectly registered as operator"); + + // check that the staker nonce incremented appropriately + require(delegationManager.stakerNonce(staker) == currentStakerNonce + 1, + "staker nonce did not increment"); + // check that the delegationApprover nonce did not increment + require(delegationManager.delegationApproverNonce(delegationManager.delegationApprover(operator)) == currentApproverNonce, + "delegationApprover nonce incremented inappropriately"); + } + + /** + * @notice `staker` becomes delegated to an operator who requires signature verification through an EOA (i.e. the operator’s `delegationApprover` address is set to a nonzero EOA) + * via the `caller` calling `DelegationManager.delegateToBySignature` + * The function should pass *only with a valid ECDSA signature from the `delegationApprover`, OR if called by the operator or their delegationApprover themselves + * AND with a valid `stakerSignatureAndExpiry` input + * Properly emits a `StakerDelegated` event + * Staker is correctly delegated after the call (i.e. correct storage update) + * Reverts if the staker is already delegated (to the operator or to anyone else) + * Reverts if the ‘operator’ is not actually registered as an operator + */ + function testDelegateBySignatureToOperatorWhoRequiresECDSASignature(address caller, uint256 expiry) public + filterFuzzedAddressInputs(caller) + { + // filter to only valid `expiry` values + cheats.assume(expiry >= block.timestamp); + + address staker = cheats.addr(stakerPrivateKey); + address delegationApprover = cheats.addr(delegationSignerPrivateKey); + + // register *this contract* as an operator + address operator = address(this); + // filter inputs, since this will fail when the staker is already registered as an operator + cheats.assume(staker != operator); + + IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + earningsReceiver: operator, + delegationApprover: delegationApprover, + stakerOptOutWindowBlocks: 0 + }); + testRegisterAsOperator(operator, operatorDetails, emptyStringForMetadataURI); + + // fetch the delegationApprover's current nonce + uint256 currentApproverNonce = delegationManager.delegationApproverNonce(delegationManager.delegationApprover(operator)); + // calculate the delegationSigner's signature + IDelegationManager.SignatureWithExpiry memory approverSignatureAndExpiry = _getApproverSignature(delegationSignerPrivateKey, staker, operator, expiry); + + // fetch the staker's current nonce + uint256 currentStakerNonce = delegationManager.stakerNonce(staker); + // calculate the staker signature + IDelegationManager.SignatureWithExpiry memory stakerSignatureAndExpiry = _getStakerSignature(stakerPrivateKey, operator, expiry); + + // delegate from the `staker` to the operator, via having the `caller` call `DelegationManager.delegateToBySignature` + cheats.startPrank(caller); + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit StakerDelegated(staker, operator); + delegationManager.delegateToBySignature(staker, operator, stakerSignatureAndExpiry, approverSignatureAndExpiry); cheats.stopPrank(); - (IStrategy[] memory updatedStrategies, uint256[] memory updatedShares) = - strategyManager.getDeposits(staker); + require(delegationManager.isDelegated(staker), "staker not delegated correctly"); + require(delegationManager.delegatedTo(staker) == operator, "staker delegated to the wrong address"); + require(!delegationManager.isOperator(staker), "staker incorrectly registered as operator"); - cheats.startPrank(address(strategyManagerMock)); - // cheats.expectEmit(true, true, true, true, address(delegationManager)); - cheats.expectEmit(true, true, true, true); - emit OnDelegationWithdrawnCallFailure(delegationTermsMock, 0x08c379a000000000000000000000000000000000000000000000000000000000); - delegationManager.decreaseDelegatedShares(staker, updatedStrategies, updatedShares); + // check that the delegationApprover nonce incremented appropriately + if (caller == operator || caller == delegationManager.delegationApprover(operator)) { + require(delegationManager.delegationApproverNonce(delegationManager.delegationApprover(operator)) == currentApproverNonce, + "delegationApprover nonce incremented inappropriately"); + } else { + require(delegationManager.delegationApproverNonce(delegationManager.delegationApprover(operator)) == currentApproverNonce + 1, + "delegationApprover nonce did not increment"); + } + + // check that the staker nonce incremented appropriately + require(delegationManager.stakerNonce(staker) == currentStakerNonce + 1, + "staker nonce did not increment"); + } + + /** + * @notice `staker` becomes delegated to an operatorwho requires signature verification through an EIP1271-compliant contract (i.e. the operator’s `delegationApprover` address is + * set to a nonzero and code-containing address) via the `caller` calling `DelegationManager.delegateToBySignature` + * The function uses OZ's ERC1271WalletMock contract, and thus should pass *only when a valid ECDSA signature from the `owner` of the ERC1271WalletMock contract, + * OR if called by the operator or their delegationApprover themselves + * AND with a valid `stakerSignatureAndExpiry` input + * Properly emits a `StakerDelegated` event + * Staker is correctly delegated after the call (i.e. correct storage update) + * Reverts if the staker is already delegated (to the operator or to anyone else) + * Reverts if the ‘operator’ is not actually registered as an operator + */ + function testDelegateBySignatureToOperatorWhoRequiresEIP1271Signature(address caller, uint256 expiry) public + filterFuzzedAddressInputs(caller) + { + // filter to only valid `expiry` values + cheats.assume(expiry >= block.timestamp); + + address staker = cheats.addr(stakerPrivateKey); + address delegationSigner = cheats.addr(delegationSignerPrivateKey); + + // register *this contract* as an operator + address operator = address(this); + // filter inputs, since this will fail when the staker is already registered as an operator + cheats.assume(staker != operator); + + /** + * deploy a ERC1271WalletMock contract with the `delegationSigner` address as the owner, + * so that we can create valid signatures from the `delegationSigner` for the contract to check when called + */ + ERC1271WalletMock wallet = new ERC1271WalletMock(delegationSigner); + + IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + earningsReceiver: operator, + delegationApprover: address(wallet), + stakerOptOutWindowBlocks: 0 + }); + testRegisterAsOperator(operator, operatorDetails, emptyStringForMetadataURI); + + // fetch the delegationApprover's current nonce + uint256 currentApproverNonce = delegationManager.delegationApproverNonce(delegationManager.delegationApprover(operator)); + // calculate the delegationSigner's signature + IDelegationManager.SignatureWithExpiry memory approverSignatureAndExpiry = _getApproverSignature(delegationSignerPrivateKey, staker, operator, expiry); + + // fetch the staker's current nonce + uint256 currentStakerNonce = delegationManager.stakerNonce(staker); + // calculate the staker signature + IDelegationManager.SignatureWithExpiry memory stakerSignatureAndExpiry = _getStakerSignature(stakerPrivateKey, operator, expiry); + + // delegate from the `staker` to the operator, via having the `caller` call `DelegationManager.delegateToBySignature` + cheats.startPrank(caller); + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit StakerDelegated(staker, operator); + delegationManager.delegateToBySignature(staker, operator, stakerSignatureAndExpiry, approverSignatureAndExpiry); cheats.stopPrank(); + + require(delegationManager.isDelegated(staker), "staker not delegated correctly"); + require(delegationManager.delegatedTo(staker) == operator, "staker delegated to the wrong address"); + require(!delegationManager.isOperator(staker), "staker incorrectly registered as operator"); + + // check that the delegationApprover nonce incremented appropriately + if (caller == operator || caller == delegationManager.delegationApprover(operator)) { + require(delegationManager.delegationApproverNonce(delegationManager.delegationApprover(operator)) == currentApproverNonce, + "delegationApprover nonce incremented inappropriately"); + } else { + require(delegationManager.delegationApproverNonce(delegationManager.delegationApprover(operator)) == currentApproverNonce + 1, + "delegationApprover nonce did not increment"); + } + + // check that the staker nonce incremented appropriately + require(delegationManager.stakerNonce(staker) == currentStakerNonce + 1, + "staker nonce did not increment"); } - function testDelegationReceivedHookWithTooMuchReturnData(address operator, address staker) public fuzzedAddress(operator) fuzzedAddress(staker) { - cheats.assume(operator != staker); - delegationTermsMock.setShouldReturnData(true); + // @notice Checks that `DelegationManager.delegateToBySignature` reverts if the staker's signature has expired + function testDelegateBySignatureRevertsWhenStakerSignatureExpired(address staker, address operator, uint256 expiry, bytes memory signature) public{ + cheats.assume(expiry < block.timestamp); + cheats.expectRevert(bytes("DelegationManager.delegateToBySignature: staker signature expired")); + IDelegationManager.SignatureWithExpiry memory signatureWithExpiry = IDelegationManager.SignatureWithExpiry({ + signature: signature, + expiry: expiry + }); + delegation.delegateToBySignature(staker, operator, signatureWithExpiry, signatureWithExpiry); + } - cheats.startPrank(operator); - delegationManager.registerAsOperator(delegationTermsMock); + // @notice Checks that `DelegationManager.delegateToBySignature` reverts if the delegationApprover's signature has expired and their signature is checked + function testDelegateBySignatureRevertsWhenDelegationApproverSignatureExpired(address caller, uint256 stakerExpiry, uint256 delegationApproverExpiry) public + filterFuzzedAddressInputs(caller) + { + // filter to only valid `stakerExpiry` values + cheats.assume(stakerExpiry >= block.timestamp); + // roll to a very late timestamp + cheats.roll(type(uint256).max / 2); + // filter to only *invalid* `delegationApproverExpiry` values + cheats.assume(delegationApproverExpiry < block.timestamp); + + address staker = cheats.addr(stakerPrivateKey); + address delegationApprover = cheats.addr(delegationSignerPrivateKey); + + // register *this contract* as an operator + address operator = address(this); + // filter inputs, since this will fail when the staker is already registered as an operator + cheats.assume(staker != operator); + + IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + earningsReceiver: operator, + delegationApprover: delegationApprover, + stakerOptOutWindowBlocks: 0 + }); + testRegisterAsOperator(operator, operatorDetails, emptyStringForMetadataURI); + + // calculate the delegationSigner's signature + IDelegationManager.SignatureWithExpiry memory approverSignatureAndExpiry = _getApproverSignature(delegationSignerPrivateKey, staker, operator, delegationApproverExpiry); + + // calculate the staker signature + IDelegationManager.SignatureWithExpiry memory stakerSignatureAndExpiry = _getStakerSignature(stakerPrivateKey, operator, stakerExpiry); + + // try delegate from the `staker` to the operator, via having the `caller` call `DelegationManager.delegateToBySignature`, and check for reversion + cheats.startPrank(caller); + cheats.expectRevert(bytes("DelegationManager._delegate: approver signature expired")); + delegationManager.delegateToBySignature(staker, operator, stakerSignatureAndExpiry, approverSignatureAndExpiry); cheats.stopPrank(); + } + /** + * @notice Like `testDelegateToOperatorWhoRequiresECDSASignature` but using an invalid expiry on purpose and checking that reversion occurs + */ + function testDelegateToOperatorWhoRequiresECDSASignature_RevertsWithExpiredSignature(address staker, uint256 expiry) public + filterFuzzedAddressInputs(staker) + { + // roll to a very late timestamp + cheats.roll(type(uint256).max / 2); + // filter to only *invalid* `expiry` values + cheats.assume(expiry < block.timestamp); + + address delegationApprover = cheats.addr(delegationSignerPrivateKey); + + // register *this contract* as an operator + address operator = address(this); + // filter inputs, since this will fail when the staker is already registered as an operator + cheats.assume(staker != operator); + + IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + earningsReceiver: operator, + delegationApprover: delegationApprover, + stakerOptOutWindowBlocks: 0 + }); + testRegisterAsOperator(operator, operatorDetails, emptyStringForMetadataURI); + + // calculate the delegationSigner's signature + IDelegationManager.SignatureWithExpiry memory approverSignatureAndExpiry = _getApproverSignature(delegationSignerPrivateKey, staker, operator, expiry); + + // delegate from the `staker` to the operator cheats.startPrank(staker); - delegationManager.delegateTo(operator); + cheats.expectRevert(bytes("DelegationManager._delegate: approver signature expired")); + delegationManager.delegateTo(operator, approverSignatureAndExpiry); cheats.stopPrank(); } - function testDelegationWithdrawnHookWithTooMuchReturnData( - address operator, - address staker - ) public fuzzedAddress(operator) fuzzedAddress(staker) { - cheats.assume(operator != staker); + /** + * Staker is undelegated from an operator, via a call to `undelegate`, properly originating from the StrategyManager address. + * Reverts if called by any address that is not the StrategyManager + * Reverts if the staker is themselves an operator (i.e. they are delegated to themselves) + * Does nothing if the staker is already undelegated + * Properly undelegates the staker, i.e. the staker becomes “delegated to” the zero address, and `isDelegated(staker)` returns ‘false’ + * Emits a `StakerUndelegated` event + */ + function testUndelegateFromOperator(address staker) public { + // register *this contract* as an operator and delegate from the `staker` to them (already filters out case when staker is the operator since it will revert) + IDelegationManager.SignatureWithExpiry memory approverSignatureAndExpiry; + testDelegateToOperatorWhoAcceptsAllStakers(staker, approverSignatureAndExpiry); - delegationTermsMock.setShouldReturnData(true); + cheats.startPrank(address(strategyManagerMock)); + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit StakerUndelegated(staker, delegationManager.delegatedTo(staker)); + delegationManager.undelegate(staker); + cheats.stopPrank(); + require(!delegationManager.isDelegated(staker), "staker not undelegated!"); + require(delegationManager.delegatedTo(staker) == address(0), "undelegated staker should be delegated to zero address"); + } + // @notice Verifies that an operator cannot undelegate from themself (this should always be forbidden) + function testOperatorCannotUndelegateFromThemself(address operator) public fuzzedAddress(operator) { cheats.startPrank(operator); - delegationManager.registerAsOperator(delegationTermsMock); + IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + earningsReceiver: operator, + delegationApprover: address(0), + stakerOptOutWindowBlocks: 0 + }); + delegationManager.registerAsOperator(operatorDetails, emptyStringForMetadataURI); cheats.stopPrank(); + cheats.expectRevert(bytes("DelegationManager.undelegate: operators cannot undelegate from themselves")); + + cheats.startPrank(address(strategyManagerMock)); + delegationManager.undelegate(operator); + cheats.stopPrank(); + } - cheats.startPrank(staker); - delegationManager.delegateTo(operator); + // @notice Verifies that `DelegationManager.undelegate` reverts if not called by the StrategyManager + function testCannotCallUndelegateFromNonStrategyManagerAddress(address caller) public fuzzedAddress(caller) { + cheats.assume(caller != address(strategyManagerMock)); + cheats.expectRevert(bytes("onlyStrategyManager")); + cheats.startPrank(caller); + delegationManager.undelegate(address(this)); + } + + /** + * @notice Verifies that `DelegationManager.increaseDelegatedShares` properly increases the delegated `shares` that the operator + * who the `staker` is delegated to has in the strategy + * @dev Checks that there is no change if the staker is not delegated + */ + function testIncreaseDelegatedShares(address staker, uint256 shares, bool delegateFromStakerToOperator) public { + IStrategy strategy = strategyMock; + + // register *this contract* as an operator + address operator = address(this); + IDelegationManager.SignatureWithExpiry memory approverSignatureAndExpiry; + // filter inputs, since delegating to the operator will fail when the staker is already registered as an operator + cheats.assume(staker != operator); + + IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + earningsReceiver: operator, + delegationApprover: address(0), + stakerOptOutWindowBlocks: 0 + }); + testRegisterAsOperator(operator, operatorDetails, emptyStringForMetadataURI); + + // delegate from the `staker` to the operator *if `delegateFromStakerToOperator` is 'true'* + if (delegateFromStakerToOperator) { + cheats.startPrank(staker); + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit StakerDelegated(staker, operator); + delegationManager.delegateTo(operator, approverSignatureAndExpiry); + cheats.stopPrank(); + } + + uint256 delegatedSharesBefore = delegationManager.operatorShares(delegationManager.delegatedTo(staker), strategy); + + cheats.startPrank(address(strategyManagerMock)); + delegationManager.increaseDelegatedShares(staker, strategy, shares); cheats.stopPrank(); - (IStrategy[] memory updatedStrategies, uint256[] memory updatedShares) = - strategyManager.getDeposits(staker); + uint256 delegatedSharesAfter = delegationManager.operatorShares(delegationManager.delegatedTo(staker), strategy); + + if (delegationManager.isDelegated(staker)) { + require(delegatedSharesAfter == delegatedSharesBefore + shares, "delegated shares did not increment correctly"); + } else { + require(delegatedSharesAfter == delegatedSharesBefore, "delegated shares incremented incorrectly"); + require(delegatedSharesBefore == 0, "nonzero shares delegated to zero address!"); + } + } + + /** + * @notice Verifies that `DelegationManager.decreaseDelegatedShares` properly decreases the delegated `shares` that the operator + * who the `staker` is delegated to has in the strategies + * @dev Checks that there is no change if the staker is not delegated + */ + function testDecreaseDelegatedShares(address staker, IStrategy[] memory strategies, uint256 shares, bool delegateFromStakerToOperator) public { + // sanity-filtering on fuzzed input length + cheats.assume(strategies.length <= 64); + // register *this contract* as an operator + address operator = address(this); + IDelegationManager.SignatureWithExpiry memory approverSignatureAndExpiry; + // filter inputs, since delegating to the operator will fail when the staker is already registered as an operator + cheats.assume(staker != operator); + + IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + earningsReceiver: operator, + delegationApprover: address(0), + stakerOptOutWindowBlocks: 0 + }); + testRegisterAsOperator(operator, operatorDetails, emptyStringForMetadataURI); + + // delegate from the `staker` to the operator *if `delegateFromStakerToOperator` is 'true'* + if (delegateFromStakerToOperator) { + cheats.startPrank(staker); + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit StakerDelegated(staker, operator); + delegationManager.delegateTo(operator, approverSignatureAndExpiry); + cheats.stopPrank(); + } + uint256[] memory delegatedSharesBefore = new uint256[](strategies.length); + uint256[] memory sharesInputArray = new uint256[](strategies.length); + + // for each strategy in `strategies`, increase delegated shares by `shares` cheats.startPrank(address(strategyManagerMock)); - delegationManager.decreaseDelegatedShares(staker, updatedStrategies, updatedShares); + for (uint256 i = 0; i < strategies.length; ++i) { + delegationManager.increaseDelegatedShares(staker, strategies[i], shares); + delegatedSharesBefore[i] = delegationManager.operatorShares(delegationManager.delegatedTo(staker), strategies[i]); + // also construct an array which we'll use in another loop + sharesInputArray[i] = shares; + } cheats.stopPrank(); + + // for each strategy in `strategies`, decrease delegated shares by `shares` + cheats.startPrank(address(strategyManagerMock)); + delegationManager.decreaseDelegatedShares(delegationManager.delegatedTo(staker), strategies, sharesInputArray); + cheats.stopPrank(); + + // check shares after call to `decreaseDelegatedShares` + for (uint256 i = 0; i < strategies.length; ++i) { + uint256 delegatedSharesAfter = delegationManager.operatorShares(delegationManager.delegatedTo(staker), strategies[i]); + + if (delegationManager.isDelegated(staker)) { + require(delegatedSharesAfter == delegatedSharesBefore[i] - sharesInputArray[i], "delegated shares did not decrement correctly"); + } else { + require(delegatedSharesAfter == delegatedSharesBefore[i], "delegated shares decremented incorrectly"); + require(delegatedSharesBefore[i] == 0, "nonzero shares delegated to zero address!"); + } + } } - function testDelegationReceivedHookWithNoReturnData(address operator, address staker) public fuzzedAddress(operator) fuzzedAddress(staker) { - cheats.assume(operator != staker); + // @notice Verifies that `DelegationManager.increaseDelegatedShares` reverts if not called by the StrategyManager + function testCannotCallIncreaseDelegatedSharesFromNonStrategyManagerAddress(address operator, uint256 shares) public fuzzedAddress(operator) { + cheats.assume(operator != address(strategyManagerMock)); + cheats.expectRevert(bytes("onlyStrategyManager")); + cheats.startPrank(operator); + delegationManager.increaseDelegatedShares(operator, strategyMock, shares); + } + // @notice Verifies that `DelegationManager.decreaseDelegatedShares` reverts if not called by the StrategyManager + function testCannotCallDecreaseDelegatedSharesFromNonStrategyManagerAddress( + address operator, + IStrategy[] memory strategies, + uint256[] memory shareAmounts + ) public fuzzedAddress(operator) { + cheats.assume(operator != address(strategyManagerMock)); + cheats.expectRevert(bytes("onlyStrategyManager")); cheats.startPrank(operator); - delegationManager.registerAsOperator(delegationTermsMock); + delegationManager.decreaseDelegatedShares(operator, strategies, shareAmounts); + } + + // @notice Verifies that it is not possible for a staker to delegate to an operator when the operator is frozen in EigenLayer + function testCannotDelegateWhenOperatorIsFrozen(address operator, address staker) public fuzzedAddress(operator) fuzzedAddress(staker) { + cheats.assume(operator != staker); + + cheats.startPrank(operator); + IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + earningsReceiver: operator, + delegationApprover: address(0), + stakerOptOutWindowBlocks: 0 + }); + delegationManager.registerAsOperator(operatorDetails, emptyStringForMetadataURI); cheats.stopPrank(); + slasherMock.setOperatorFrozenStatus(operator, true); + cheats.expectRevert(bytes("DelegationManager._delegate: cannot delegate to a frozen operator")); cheats.startPrank(staker); - delegationManager.delegateTo(operator); + IDelegationManager.SignatureWithExpiry memory signatureWithExpiry; + delegationManager.delegateTo(operator, signatureWithExpiry); cheats.stopPrank(); } - function testDelegationWithdrawnHookWithNoReturnData( - address operator, - address staker - ) public fuzzedAddress(operator) fuzzedAddress(staker) { - cheats.assume(operator != staker); + // @notice Verifies that it is not possible for a staker to delegate to an operator when they are already delegated to an operator + function testCannotDelegateWhenStakerHasExistingDelegation(address staker, address operator, address operator2) public + fuzzedAddress(staker) + fuzzedAddress(operator) + fuzzedAddress(operator2) + { + cheats.assume(operator != operator2); + cheats.assume(staker != operator); + cheats.assume(staker != operator2); cheats.startPrank(operator); - delegationManager.registerAsOperator(delegationTermsMock); + IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + earningsReceiver: operator, + delegationApprover: address(0), + stakerOptOutWindowBlocks: 0 + }); + delegationManager.registerAsOperator(operatorDetails, emptyStringForMetadataURI); + cheats.stopPrank(); + + cheats.startPrank(operator2); + delegationManager.registerAsOperator(operatorDetails, emptyStringForMetadataURI); cheats.stopPrank(); cheats.startPrank(staker); - delegationManager.delegateTo(operator); + IDelegationManager.SignatureWithExpiry memory signatureWithExpiry; + delegationManager.delegateTo(operator, signatureWithExpiry); cheats.stopPrank(); - (IStrategy[] memory updatedStrategies, uint256[] memory updatedShares) = - strategyManager.getDeposits(staker); + cheats.startPrank(staker); + cheats.expectRevert(bytes("DelegationManager._delegate: staker is already actively delegated")); + delegationManager.delegateTo(operator2, signatureWithExpiry); + cheats.stopPrank(); + } - cheats.startPrank(address(strategyManagerMock)); - delegationManager.decreaseDelegatedShares(staker, updatedStrategies, updatedShares); + // @notice Verifies that it is not possible to delegate to an unregistered operator + function testCannotDelegateToUnregisteredOperator(address operator) public { + cheats.expectRevert(bytes("DelegationManager._delegate: operator is not registered in EigenLayer")); + IDelegationManager.SignatureWithExpiry memory signatureWithExpiry; + delegationManager.delegateTo(operator, signatureWithExpiry); + } + + // @notice Verifies that delegating is not possible when the "new delegations paused" switch is flipped + function testCannotDelegateWhenPausedNewDelegationIsSet(address operator, address staker) public fuzzedAddress(operator) fuzzedAddress(staker) { + cheats.startPrank(pauser); + delegationManager.pause(1); + cheats.stopPrank(); + + cheats.startPrank(staker); + cheats.expectRevert(bytes("Pausable: index is paused")); + IDelegationManager.SignatureWithExpiry memory signatureWithExpiry; + delegationManager.delegateTo(operator, signatureWithExpiry); cheats.stopPrank(); } + // special event purely used in the StrategyManagerMock contract, inside of `testForceUndelegation` to verify that the correct call is made + event ForceTotalWithdrawalCalled(address staker); + + /** + * @notice Verifies that the `forceUndelegation` function properly calls `strategyManager.forceTotalWithdrawal` + * @param callFromOperatorOrApprover -- calls from the operator if 'false' and the 'approver' if true + */ + function testForceUndelegation(address staker, bool callFromOperatorOrApprover) public + fuzzedAddress(staker) + { + address delegationApprover = cheats.addr(delegationSignerPrivateKey); + address operator = address(this); + + // filtering since you can't delegate to yourself after registering as an operator + cheats.assume(staker != operator); + + // register this contract as an operator and delegate from the staker to it + uint256 expiry = type(uint256).max; + testDelegateToOperatorWhoRequiresECDSASignature(staker, expiry); + + address caller; + if (callFromOperatorOrApprover) { + caller = delegationApprover; + } else { + caller = operator; + } + + // call the `forceUndelegation` function and check that the correct calldata is forwarded by looking for an event emitted by the StrategyManagerMock contract + cheats.startPrank(caller); + cheats.expectEmit(true, true, true, true, address(strategyManagerMock)); + emit ForceTotalWithdrawalCalled(staker); + bytes32 returnValue = delegationManager.forceUndelegation(staker); + // check that the return value is empty, as specified in the mock contract + require(returnValue == bytes32(uint256(0)), "mock contract returned wrong return value"); + cheats.stopPrank(); + } + + /** + * @notice Verifies that the `forceUndelegation` function has proper access controls (can only be called by the operator who the `staker` has delegated + * to or the operator's `delegationApprover`) + */ + function testCannotCallForceUndelegationFromImproperAddress(address staker, address caller) public + fuzzedAddress(staker) + fuzzedAddress(caller) + { + address delegationApprover = cheats.addr(delegationSignerPrivateKey); + address operator = address(this); + + // filtering since you can't delegate to yourself after registering as an operator + cheats.assume(staker != operator); + + // filter out addresses that are actually allowed to call the function + cheats.assume(caller != operator); + cheats.assume(caller != delegationApprover); + + // register this contract as an operator and delegate from the staker to it + uint256 expiry = type(uint256).max; + testDelegateToOperatorWhoRequiresECDSASignature(staker, expiry); + + // try to call the `forceUndelegation` function and check for reversion + cheats.startPrank(caller); + cheats.expectRevert(bytes("DelegationManager.forceUndelegation: caller must be operator or their delegationApprover")); + delegationManager.forceUndelegation(staker); + cheats.stopPrank(); + } + + /** + * @notice verifies that `DelegationManager.forceUndelegation` reverts if trying to undelegate an operator from themselves + * @param callFromOperatorOrApprover -- calls from the operator if 'false' and the 'approver' if true + */ + function testOperatorCannotForceUndelegateThemself(address delegationApprover, bool callFromOperatorOrApprover) public { + // register *this contract* as an operator + address operator = address(this); + IDelegationManager.OperatorDetails memory _operatorDetails = IDelegationManager.OperatorDetails({ + earningsReceiver: operator, + delegationApprover: delegationApprover, + stakerOptOutWindowBlocks: 0 + }); + testRegisterAsOperator(operator, _operatorDetails, emptyStringForMetadataURI); + + address caller; + if (callFromOperatorOrApprover) { + caller = delegationApprover; + } else { + caller = operator; + } + + // try to call the `forceUndelegation` function and check for reversion + cheats.startPrank(caller); + cheats.expectRevert(bytes("DelegationManager.forceUndelegation: operators cannot be force-undelegated")); + delegationManager.forceUndelegation(operator); + cheats.stopPrank(); + } + + /** + * @notice internal function for calculating a signature from the delegationSigner corresponding to `_delegationSignerPrivateKey`, approving + * the `staker` to delegate to `operator`, and expiring at `expiry`. + */ + function _getApproverSignature(uint256 _delegationSignerPrivateKey, address staker, address operator, uint256 expiry) + internal view returns (IDelegationManager.SignatureWithExpiry memory approverSignatureAndExpiry) + { + approverSignatureAndExpiry.expiry = expiry; + { + bytes32 digestHash = delegationManager.calculateCurrentDelegationApprovalDigestHash(staker, operator, expiry); + (uint8 v, bytes32 r, bytes32 s) = cheats.sign(_delegationSignerPrivateKey, digestHash); + approverSignatureAndExpiry.signature = abi.encodePacked(r, s, v); + } + return approverSignatureAndExpiry; + } + + /** + * @notice internal function for calculating a signature from the staker corresponding to `_stakerPrivateKey`, delegating them to + * the `operator`, and expiring at `expiry`. + */ + function _getStakerSignature(uint256 _stakerPrivateKey, address operator, uint256 expiry) + internal view returns (IDelegationManager.SignatureWithExpiry memory stakerSignatureAndExpiry) + { + address staker = cheats.addr(stakerPrivateKey); + stakerSignatureAndExpiry.expiry = expiry; + { + bytes32 digestHash = delegationManager.calculateCurrentStakerDelegationDigestHash(staker, operator, expiry); + (uint8 v, bytes32 r, bytes32 s) = cheats.sign(_stakerPrivateKey, digestHash); + stakerSignatureAndExpiry.signature = abi.encodePacked(r, s, v); + } + return stakerSignatureAndExpiry; + } } \ No newline at end of file diff --git a/src/test/unit/StakeRegistryUnit.t.sol b/src/test/unit/StakeRegistryUnit.t.sol index e27c5c500..4f85f7873 100644 --- a/src/test/unit/StakeRegistryUnit.t.sol +++ b/src/test/unit/StakeRegistryUnit.t.sol @@ -51,6 +51,7 @@ contract StakeRegistryUnitTests is Test { bytes32 defaultOperatorId = keccak256("defaultOperatorId"); uint8 defaultQuorumNumber = 0; uint8 numQuorums = 192; + uint8 maxQuorumsToRegisterFor = 4; uint256 gasUsed; @@ -98,14 +99,14 @@ contract StakeRegistryUnitTests is Test { ); // setup the dummy minimum stake for quorum - uint96[] memory minimumStakeForQuorum = new uint96[](numQuorums); + uint96[] memory minimumStakeForQuorum = new uint96[](maxQuorumsToRegisterFor); for (uint256 i = 0; i < minimumStakeForQuorum.length; i++) { minimumStakeForQuorum[i] = uint96(i+1); } // setup the dummy quorum strategies IVoteWeigher.StrategyAndWeightingMultiplier[][] memory quorumStrategiesConsideredAndMultipliers = - new IVoteWeigher.StrategyAndWeightingMultiplier[][](numQuorums); + new IVoteWeigher.StrategyAndWeightingMultiplier[][](maxQuorumsToRegisterFor); for (uint256 i = 0; i < quorumStrategiesConsideredAndMultipliers.length; i++) { quorumStrategiesConsideredAndMultipliers[i] = new IVoteWeigher.StrategyAndWeightingMultiplier[](1); quorumStrategiesConsideredAndMultipliers[i][0] = IVoteWeigher.StrategyAndWeightingMultiplier( @@ -159,7 +160,7 @@ contract StakeRegistryUnitTests is Test { } function testRegisterOperator_MoreQuorumsThanQuorumCount_Reverts() public { - bytes memory quorumNumbers = new bytes(numQuorums+1); + bytes memory quorumNumbers = new bytes(maxQuorumsToRegisterFor+1); for (uint i = 0; i < quorumNumbers.length; i++) { quorumNumbers[i] = bytes1(uint8(i)); } @@ -178,7 +179,7 @@ contract StakeRegistryUnitTests is Test { // set the weights of the operator // stakeRegistry.setOperatorWeight() - bytes memory quorumNumbers = new bytes(stakesForQuorum.length > 128 ? 128 : stakesForQuorum.length); + bytes memory quorumNumbers = new bytes(stakesForQuorum.length > maxQuorumsToRegisterFor ? maxQuorumsToRegisterFor : stakesForQuorum.length); for (uint i = 0; i < quorumNumbers.length; i++) { quorumNumbers[i] = bytes1(uint8(i)); } @@ -195,11 +196,12 @@ contract StakeRegistryUnitTests is Test { uint256 quorumBitmap, uint80[] memory stakesForQuorum ) public { - + // limit to maxQuorumsToRegisterFor quorums and register for quorum 0 + quorumBitmap = quorumBitmap & (1 << maxQuorumsToRegisterFor - 1) | 1; uint96[] memory paddedStakesForQuorum = _registerOperatorValid(defaultOperator, defaultOperatorId, quorumBitmap, stakesForQuorum); uint8 quorumNumberIndex = 0; - for (uint8 i = 0; i < 192; i++) { + for (uint8 i = 0; i < maxQuorumsToRegisterFor; i++) { if (quorumBitmap >> i & 1 == 1) { // check that the operator has 1 stake update in the quorum numbers they registered for assertEq(stakeRegistry.getStakeHistoryLengthForQuorumNumber(defaultOperatorId, i), 1); @@ -260,9 +262,9 @@ contract StakeRegistryUnitTests is Test { } // for each bit in each quorumBitmap, increment the number of operators in that quorum - uint32[] memory numOperatorsInQuorum = new uint32[](192); + uint32[] memory numOperatorsInQuorum = new uint32[](maxQuorumsToRegisterFor); for (uint256 i = 0; i < quorumBitmaps.length; i++) { - for (uint256 j = 0; j < 192; j++) { + for (uint256 j = 0; j < maxQuorumsToRegisterFor; j++) { if (quorumBitmaps[i] >> j & 1 == 1) { numOperatorsInQuorum[j]++; } @@ -274,7 +276,7 @@ contract StakeRegistryUnitTests is Test { uint32[] memory operatorQuorumIndices = new uint32[](quorumBitmaps.length); // for each quorum - for (uint8 i = 0; i < 192; i++) { + for (uint8 i = 0; i < maxQuorumsToRegisterFor; i++) { uint32 operatorCount = 0; // reset the cumulative block number cumulativeBlockNumber = initialBlockNumber; @@ -315,19 +317,21 @@ contract StakeRegistryUnitTests is Test { } } - function testDeregisterFirstOperator_Valid( + function testDeregisterOperator_Valid( uint256 pseudoRandomNumber, uint256 quorumBitmap, uint256 deregistrationQuorumsFlag, - uint80[] memory stakesForQuorum, - uint8 numOperatorsRegisterBefore + uint80[] memory stakesForQuorum ) public { // modulo so no overflow pseudoRandomNumber = pseudoRandomNumber % type(uint128).max; + // limit to maxQuorumsToRegisterFor quorums and register for quorum 0 + quorumBitmap = quorumBitmap & (1 << maxQuorumsToRegisterFor - 1) | 1; // register a bunch of operators cheats.roll(100); uint32 cumulativeBlockNumber = 100; + uint8 numOperatorsRegisterBefore = 5; uint256 numOperators = 1 + 2*numOperatorsRegisterBefore; uint256[] memory quorumBitmaps = new uint256[](numOperators); @@ -365,12 +369,12 @@ contract StakeRegistryUnitTests is Test { // deregister the operator from a subset of the quorums uint256 deregistrationQuroumBitmap = quorumBitmap & deregistrationQuorumsFlag; - _deregisterOperatorValid(operatorIdToDeregister, quorumBitmap); + _deregisterOperatorValid(operatorIdToDeregister, deregistrationQuroumBitmap); // for each bit in each quorumBitmap, increment the number of operators in that quorum - uint32[] memory numOperatorsInQuorum = new uint32[](192); + uint32[] memory numOperatorsInQuorum = new uint32[](maxQuorumsToRegisterFor); for (uint256 i = 0; i < quorumBitmaps.length; i++) { - for (uint256 j = 0; j < 192; j++) { + for (uint256 j = 0; j < maxQuorumsToRegisterFor; j++) { if (quorumBitmaps[i] >> j & 1 == 1) { numOperatorsInQuorum[j]++; } @@ -378,36 +382,37 @@ contract StakeRegistryUnitTests is Test { } uint8 quorumNumberIndex = 0; - for (uint8 i = 0; i < 192; i++) { + for (uint8 i = 0; i < maxQuorumsToRegisterFor; i++) { if (deregistrationQuroumBitmap >> i & 1 == 1) { // check that the operator has 2 stake updates in the quorum numbers they registered for - assertEq(stakeRegistry.getStakeHistoryLengthForQuorumNumber(operatorIdToDeregister, i), 2); + assertEq(stakeRegistry.getStakeHistoryLengthForQuorumNumber(operatorIdToDeregister, i), 2, "testDeregisterFirstOperator_Valid_0"); // make sure that the last stake update is as expected IStakeRegistry.OperatorStakeUpdate memory lastStakeUpdate = stakeRegistry.getStakeUpdateForQuorumFromOperatorIdAndIndex(i, operatorIdToDeregister, 1); - assertEq(lastStakeUpdate.stake, 0); - assertEq(lastStakeUpdate.updateBlockNumber, cumulativeBlockNumber); - assertEq(lastStakeUpdate.nextUpdateBlockNumber, 0); + assertEq(lastStakeUpdate.stake, 0, "testDeregisterFirstOperator_Valid_1"); + assertEq(lastStakeUpdate.updateBlockNumber, cumulativeBlockNumber, "testDeregisterFirstOperator_Valid_2"); + assertEq(lastStakeUpdate.nextUpdateBlockNumber, 0, "testDeregisterFirstOperator_Valid_3"); // make the analogous check for total stake history - assertEq(stakeRegistry.getLengthOfTotalStakeHistoryForQuorum(i), numOperatorsInQuorum[i] + 1); + assertEq(stakeRegistry.getLengthOfTotalStakeHistoryForQuorum(i), numOperatorsInQuorum[i] + 1, "testDeregisterFirstOperator_Valid_4"); // make sure that the last stake update is as expected IStakeRegistry.OperatorStakeUpdate memory lastTotalStakeUpdate = stakeRegistry.getTotalStakeUpdateForQuorumFromIndex(i, numOperatorsInQuorum[i]); assertEq(lastTotalStakeUpdate.stake, stakeRegistry.getTotalStakeUpdateForQuorumFromIndex(i, numOperatorsInQuorum[i] - 1).stake // the previous total stake - - paddedStakesForQuorum[quorumNumberIndex] // minus the stake that was deregistered + - paddedStakesForQuorum[quorumNumberIndex], // minus the stake that was deregistered + "testDeregisterFirstOperator_Valid_5" ); - assertEq(lastTotalStakeUpdate.updateBlockNumber, cumulativeBlockNumber); - assertEq(lastTotalStakeUpdate.nextUpdateBlockNumber, 0); + assertEq(lastTotalStakeUpdate.updateBlockNumber, cumulativeBlockNumber, "testDeregisterFirstOperator_Valid_6"); + assertEq(lastTotalStakeUpdate.nextUpdateBlockNumber, 0, "testDeregisterFirstOperator_Valid_7"); quorumNumberIndex++; } else if (quorumBitmap >> i & 1 == 1) { - assertEq(stakeRegistry.getStakeHistoryLengthForQuorumNumber(operatorIdToDeregister, i), 1); - assertEq(stakeRegistry.getLengthOfTotalStakeHistoryForQuorum(i), numOperatorsInQuorum[i]); + assertEq(stakeRegistry.getStakeHistoryLengthForQuorumNumber(operatorIdToDeregister, i), 1, "testDeregisterFirstOperator_Valid_8"); + assertEq(stakeRegistry.getLengthOfTotalStakeHistoryForQuorum(i), numOperatorsInQuorum[i], "testDeregisterFirstOperator_Valid_9"); quorumNumberIndex++; } else { // check that the operator has 0 stake updates in the quorum numbers they did not register for - assertEq(stakeRegistry.getStakeHistoryLengthForQuorumNumber(operatorIdToDeregister, i), 0); + assertEq(stakeRegistry.getStakeHistoryLengthForQuorumNumber(operatorIdToDeregister, i), 0, "testDeregisterFirstOperator_Valid_10"); } } } @@ -498,8 +503,8 @@ contract StakeRegistryUnitTests is Test { bytes32 operatorId, uint256 psuedoRandomNumber ) internal returns(uint256, uint96[] memory){ - // generate uint256 quorumBitmap from psuedoRandomNumber - uint256 quorumBitmap = uint256(keccak256(abi.encodePacked(psuedoRandomNumber, "quorumBitmap"))); + // generate uint256 quorumBitmap from psuedoRandomNumber and limit to maxQuorumsToRegisterFor quorums and register for quorum 0 + uint256 quorumBitmap = uint256(keccak256(abi.encodePacked(psuedoRandomNumber, "quorumBitmap"))) & (1 << maxQuorumsToRegisterFor - 1) | 1; // generate uint80[] stakesForQuorum from psuedoRandomNumber uint80[] memory stakesForQuorum = new uint80[](BitmapUtils.countNumOnes(quorumBitmap)); for(uint i = 0; i < stakesForQuorum.length; i++) { @@ -517,7 +522,6 @@ contract StakeRegistryUnitTests is Test { uint80[] memory stakesForQuorum ) internal returns(uint96[] memory){ cheats.assume(quorumBitmap != 0); - quorumBitmap = quorumBitmap & type(uint192).max; bytes memory quorumNumbers = BitmapUtils.bitmapToBytesArray(quorumBitmap); @@ -552,8 +556,6 @@ contract StakeRegistryUnitTests is Test { bytes32 operatorId, uint256 quorumBitmap ) internal { - quorumBitmap = quorumBitmap & type(uint192).max; - bytes memory quorumNumbers = BitmapUtils.bitmapToBytesArray(quorumBitmap); // deregister operator diff --git a/src/test/unit/StrategyManagerUnit.t.sol b/src/test/unit/StrategyManagerUnit.t.sol index 00931cb51..694542dc4 100644 --- a/src/test/unit/StrategyManagerUnit.t.sol +++ b/src/test/unit/StrategyManagerUnit.t.sol @@ -399,7 +399,7 @@ contract StrategyManagerUnitTests is Test, Utils { // not expecting a revert, so input an empty string bytes memory signature = _depositIntoStrategyWithSignature(staker, amount, expiry, ""); - cheats.expectRevert(bytes("StrategyManager.depositIntoStrategyWithSignature: signature not from staker")); + cheats.expectRevert(bytes("EIP1271SignatureUtils.checkSignature_EIP1271: signature not from signer")); strategyManager.depositIntoStrategyWithSignature(dummyStrat, dummyToken, amount, staker, expiry, signature); } @@ -448,7 +448,7 @@ contract StrategyManagerUnitTests is Test, Utils { { bytes32 structHash = keccak256(abi.encode(strategyManager.DEPOSIT_TYPEHASH(), strategy, token, amount, nonceBefore, expiry)); - bytes32 digestHash = keccak256(abi.encodePacked("\x19\x01", strategyManager.DOMAIN_SEPARATOR(), structHash)); + bytes32 digestHash = keccak256(abi.encodePacked("\x19\x01", strategyManager.domainSeparator(), structHash)); (uint8 v, bytes32 r, bytes32 s) = cheats.sign(privateKey, digestHash); // mess up the signature by flipping v's parity @@ -457,7 +457,7 @@ contract StrategyManagerUnitTests is Test, Utils { signature = abi.encodePacked(r, s, v); } - cheats.expectRevert(bytes("StrategyManager.depositIntoStrategyWithSignature: ERC1271 signature verification failed")); + cheats.expectRevert(bytes("EIP1271SignatureUtils.checkSignature_EIP1271: ERC1271 signature verification failed")); strategyManager.depositIntoStrategyWithSignature(strategy, token, amount, staker, expiry, signature); } @@ -513,7 +513,7 @@ contract StrategyManagerUnitTests is Test, Utils { { bytes32 structHash = keccak256(abi.encode(strategyManager.DEPOSIT_TYPEHASH(), strategy, token, amount, nonceBefore, expiry)); - bytes32 digestHash = keccak256(abi.encodePacked("\x19\x01", strategyManager.DOMAIN_SEPARATOR(), structHash)); + bytes32 digestHash = keccak256(abi.encodePacked("\x19\x01", strategyManager.domainSeparator(), structHash)); (uint8 v, bytes32 r, bytes32 s) = cheats.sign(privateKey, digestHash); @@ -560,7 +560,7 @@ contract StrategyManagerUnitTests is Test, Utils { { bytes32 structHash = keccak256(abi.encode(strategyManager.DEPOSIT_TYPEHASH(), strategy, token, amount, nonceBefore, expiry)); - bytes32 digestHash = keccak256(abi.encodePacked("\x19\x01", strategyManager.DOMAIN_SEPARATOR(), structHash)); + bytes32 digestHash = keccak256(abi.encodePacked("\x19\x01", strategyManager.domainSeparator(), structHash)); (uint8 v, bytes32 r, bytes32 s) = cheats.sign(privateKey, digestHash); @@ -602,7 +602,7 @@ contract StrategyManagerUnitTests is Test, Utils { { bytes32 structHash = keccak256(abi.encode(strategyManager.DEPOSIT_TYPEHASH(), strategy, token, amount, nonceBefore, expiry)); - bytes32 digestHash = keccak256(abi.encodePacked("\x19\x01", strategyManager.DOMAIN_SEPARATOR(), structHash)); + bytes32 digestHash = keccak256(abi.encodePacked("\x19\x01", strategyManager.domainSeparator(), structHash)); (uint8 v, bytes32 r, bytes32 s) = cheats.sign(privateKey, digestHash); @@ -633,7 +633,7 @@ contract StrategyManagerUnitTests is Test, Utils { { bytes32 structHash = keccak256(abi.encode(strategyManager.DEPOSIT_TYPEHASH(), strategy, token, amount, nonceBefore, expiry)); - bytes32 digestHash = keccak256(abi.encodePacked("\x19\x01", strategyManager.DOMAIN_SEPARATOR(), structHash)); + bytes32 digestHash = keccak256(abi.encodePacked("\x19\x01", strategyManager.domainSeparator(), structHash)); (uint8 v, bytes32 r, bytes32 s) = cheats.sign(privateKey, digestHash); @@ -642,7 +642,7 @@ contract StrategyManagerUnitTests is Test, Utils { uint256 sharesBefore = strategyManager.stakerStrategyShares(staker, strategy); - cheats.expectRevert(bytes("StrategyManager.depositIntoStrategyWithSignature: signature not from staker")); + cheats.expectRevert(bytes("EIP1271SignatureUtils.checkSignature_EIP1271: signature not from signer")); // call with `notStaker` as input instead of `staker` address address notStaker = address(3333); strategyManager.depositIntoStrategyWithSignature(strategy, token, amount, notStaker, expiry, signature); @@ -1015,7 +1015,8 @@ contract StrategyManagerUnitTests is Test, Utils { // TODO: set up delegation for the following three tests and check afterwords function testQueueWithdrawal_WithdrawEverything_DontUndelegate(uint256 amount) external { // delegate to self - delegationMock.delegateTo(address(this)); + IDelegationManager.SignatureWithExpiry memory signatureWithExpiry; + delegationMock.delegateTo(address(this), signatureWithExpiry); require(delegationMock.isDelegated(address(this)), "delegation mock setup failed"); bool undelegateIfPossible = false; // deposit and withdraw the same amount, don't undelegate @@ -2616,7 +2617,7 @@ testQueueWithdrawal_ToSelf_NotBeaconChainETHTwoStrategies(depositAmount, withdra { bytes32 structHash = keccak256(abi.encode(strategyManager.DEPOSIT_TYPEHASH(), dummyStrat, dummyToken, amount, nonceBefore, expiry)); - bytes32 digestHash = keccak256(abi.encodePacked("\x19\x01", strategyManager.DOMAIN_SEPARATOR(), structHash)); + bytes32 digestHash = keccak256(abi.encodePacked("\x19\x01", strategyManager.domainSeparator(), structHash)); (uint8 v, bytes32 r, bytes32 s) = cheats.sign(privateKey, digestHash); diff --git a/src/test/utils/MockAVSDeployer.sol b/src/test/utils/MockAVSDeployer.sol index bce927aec..cff580bed 100644 --- a/src/test/utils/MockAVSDeployer.sol +++ b/src/test/utils/MockAVSDeployer.sol @@ -314,7 +314,7 @@ contract MockAVSDeployer is Test { OperatorMetadata[] memory operatorMetadatas = new OperatorMetadata[](maxOperatorsToRegister); for (uint i = 0; i < operatorMetadatas.length; i++) { // limit to 16 quorums so we don't run out of gas, make them all register for quorum 0 as well - operatorMetadatas[i].quorumBitmap = uint256(keccak256(abi.encodePacked("quorumBitmap", pseudoRandomNumber, i))) & (maxQuorumsToRegisterFor << 1 - 1) | 1; + operatorMetadatas[i].quorumBitmap = uint256(keccak256(abi.encodePacked("quorumBitmap", pseudoRandomNumber, i))) & (1 << maxQuorumsToRegisterFor - 1) | 1; operatorMetadatas[i].operator = _incrementAddress(defaultOperator, i); operatorMetadatas[i].pubkey = BN254.hashToG1(keccak256(abi.encodePacked("pubkey", pseudoRandomNumber, i))); operatorMetadatas[i].operatorId = operatorMetadatas[i].pubkey.hashG1Point();