diff --git a/README.md b/README.md index 673fd0932..bfe0c0ecd 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ + # EigenLayer +
+🚧 The Slasher contract is under active development and its interface expected to change. We recommend writing slashing logic without integrating with the Slasher at this point in time. 🚧 +
EigenLayer (formerly 'EigenLayr') is a set of smart contracts deployed on Ethereum that enable restaking of assets to secure new services. At present, this repository contains *both* the contracts for EigenLayer *and* a set of general "middleware" contracts, designed to be reusable across different applications built on top of EigenLayer. diff --git a/certora/specs/core/DelegationManager.spec b/certora/specs/core/DelegationManager.spec index e0d545887..a24fc29b7 100644 --- a/certora/specs/core/DelegationManager.spec +++ b/certora/specs/core/DelegationManager.spec @@ -6,6 +6,9 @@ methods { function decreaseDelegatedShares(address,address,uint256) external; function increaseDelegatedShares(address,address,uint256) external; + // external calls from DelegationManager to ServiceManager + function _.updateStakes(address[]) external => NONDET; + // external calls to Slasher function _.isFrozen(address) external => DISPATCHER(true); function _.canWithdraw(address,uint32,uint256) external => DISPATCHER(true); diff --git a/certora/specs/core/StrategyManager.spec b/certora/specs/core/StrategyManager.spec index 9af8f8562..ec7f7b9cd 100644 --- a/certora/specs/core/StrategyManager.spec +++ b/certora/specs/core/StrategyManager.spec @@ -10,6 +10,9 @@ methods { function _.decreaseDelegatedShares(address,address,uint256) external => DISPATCHER(true); function _.increaseDelegatedShares(address,address,uint256) external => DISPATCHER(true); + // external calls from DelegationManager to ServiceManager + function _.updateStakes(address[]) external => NONDET; + // external calls to Slasher function _.isFrozen(address) external => DISPATCHER(true); function _.canWithdraw(address,uint32,uint256) external => DISPATCHER(true); diff --git a/certora/specs/pods/EigenPodManager.spec b/certora/specs/pods/EigenPodManager.spec index 4d5e1afbe..b7a2802fa 100644 --- a/certora/specs/pods/EigenPodManager.spec +++ b/certora/specs/pods/EigenPodManager.spec @@ -6,6 +6,9 @@ methods { function _.decreaseDelegatedShares(address,address,uint256) external; function _.increaseDelegatedShares(address,address,uint256) external; + // external calls from DelegationManager to ServiceManager + function _.updateStakes(address[]) external => NONDET; + // external calls to Slasher function _.isFrozen(address) external => DISPATCHER(true); function _.canWithdraw(address,uint32,uint256) external => DISPATCHER(true); diff --git a/docs/outdated/AVS-Guide.md b/docs/experimental/AVS-Guide.md similarity index 96% rename from docs/outdated/AVS-Guide.md rename to docs/experimental/AVS-Guide.md index e56d38fb2..7de4c7e34 100644 --- a/docs/outdated/AVS-Guide.md +++ b/docs/experimental/AVS-Guide.md @@ -1,16 +1,21 @@ [middleware-folder-link]: https://github.com/Layr-Labs/eigenlayer-contracts/tree/master/src/contracts/middleware [middleware-guide-link]: https://github.com/Layr-Labs/eigenlayer-contracts/blob/master/docs/AVS-Guide.md#quick-start-guide-to-build-avs-contracts # Purpose -This document aims to describe and summarize how actively validated services (AVSs) building on EigenLayer interact with the core EigenLayer protocol. Currently, this doc explains how AVS developers can use the APIs for: +This document aims to describe and summarize how actively validated services (AVSs) building on EigenLayer interact with the core EigenLayer protocol. Currently, this doc explains how AVS developers can use the current** APIs for: - enabling operators to opt-in to the AVS, - enabling operators to opt-out (withdraw stake) from the AVS, - enabling operators to continuously update their commitments to middlewares, and - enabling AVS to freeze operators for the purpose of slashing (the corresponding unfreeze actions are determined by the veto committee). +
+🚧 ** The Slasher contract is under active development and its interface expected to change. We recommend writing slashing logic without integrating with the Slasher at this point in time. 🚧 +
+ We are currently in the process of implementing the API for payment flow from AVSs to operators in EigenLayer. Details of this API will be added to this document in the near future. The following figure summarizes scope of this document: -![Doc Outline](./images/middleware_outline_doc.png) +![Doc Outline](../images/middleware_outline_doc.png) + # Introduction In designing EigenLayer, the EigenLabs team aspired to make minimal assumptions about the structure of AVSs built on top of it. If you are getting started looking at EigenLayer's codebase, the `Slasher.sol` contains most of the logic that actually mediates the interactions between EigenLayer and AVSs. Additionally, there is a general-purpose [/middleware/ folder][middleware-folder-link], which contains code that can be extended, used directly, or consulted as a reference in building an AVS on top of EigenLayer. Note that there will be a single, EigenLayer-owned, `Slasher.sol` contract, but all the `middleware` contracts are AVS-specific and need to be deployed separately by AVS teams. @@ -47,7 +52,7 @@ In order for any EigenLayer operator to be able to opt-in to an AVS, EigenLayer 2. Next, the operator needs to register with the AVS on chain via an AVS-specific registry contract (see [this][middleware-guide-link] section for examples). To integrate with EigenLayer, the AVS's Registry contract provides a registration endpoint that calls on the AVS's `ServiceManager.recordFirstStakeUpdate(..)` which in turn calls `Slasher.recordFirstStakeUpdate(..)`. On successful execution of this function call, the event `MiddlewareTimesAdded(..)` is emitted and the operator has to start serving the tasks from the AVS. The following figure illustrates the above flow: -![Operator opting-in](./images/operator_opting.png) +![Operator opting-in](../images/operator_opting.png) ### *Staker Delegation to an Operator: Which Opts-In to AVSs* @@ -70,19 +75,19 @@ Let us illustrate the usage of this facility with an example: A staker has deleg - The AVS provider now is aware of the change in stake, and the staker can eventually complete their withdrawal. Refer [here](https://github.com/Layr-Labs/eigenlayer-contracts/blob/master/docs/EigenLayer-withdrawal-flow.md) for more details The following figure illustrates the above flow: -![Stake update](./images/staker_withdrawing.png) +![Stake update](../images/staker_withdrawing.png) ### *Deregistering from AVS* In order for any EigenLayer operator to be able to de-register from an AVS, EigenLayer provides the interface `Slasher.recordLastStakeUpdateAndRevokeSlashingAbility(..)`. Essentially, in order for an operator to deregister from an AVS, the operator has to call `Slasher.recordLastStakeUpdateAndRevokeSlashingAbility(..)` via the AVS's ServiceManager contract. It is important to note that the latest block number until which the operator is required to serve tasks for the service must be known by the service and included in the ServiceManager's call to `Slasher.recordLastStakeUpdateAndRevokeSlashingAbility`. The following figure illustrates the above flow in which the operator calls the `deregister(..)` function in a sample Registry contract. -![Operator deregistering](./images/operator_deregister.png) +![Operator deregistering](../images/operator_deregister.png) ### *Slashing* As mentioned above, EigenLayer is built to support slashing as a result of an on-chain-checkable, objectively attributable action. In order for an AVS to be able to slash an operator in an objective manner, the AVS needs to deploy a DisputeResolution contract which anyone can call to raise a challenge against an EigenLayer operator for its adversarial action. On successful challenge, the DisputeResolution contract calls `ServiceManager.freezeOperator(..)`; the ServiceManager in turn calls `Slasher.freezeOperator(..)` to freeze the operator in EigenLayer. EigenLayer's Slasher contract emits a `OperatorFrozen(..)` event whenever an operator is (successfully) frozen The following figure illustrates the above flow: -![Slashing](./images/slashing.png) +![Slashing](../images/slashing.png) ## Quick Start Guide to Build AVS Contracts: diff --git a/src/contracts/core/DelegationManager.sol b/src/contracts/core/DelegationManager.sol index f53455754..17615d2e1 100644 --- a/src/contracts/core/DelegationManager.sol +++ b/src/contracts/core/DelegationManager.sol @@ -245,9 +245,6 @@ contract DelegationManager is Initializable, OwnableUpgradeable, Pausable, Deleg (IStrategy[] memory strategies, uint256[] memory shares) = getDelegatableShares(staker); - // push the operator's new stake to the StakeRegistry - _pushOperatorStakeUpdate(operator); - // emit an event if this action was not initiated by the staker themselves if (msg.sender != staker) { emit StakerForceUndelegated(staker, operator); @@ -607,16 +604,19 @@ contract DelegationManager is Initializable, OwnableUpgradeable, Pausable, Deleg podOwner: staker, shares: withdrawal.shares[i] }); - currentOperator = delegatedTo[staker]; + address podOwnerOperator = delegatedTo[staker]; // Similar to `isDelegated` logic - if (currentOperator != address(0)) { + if (podOwnerOperator != address(0)) { _increaseOperatorShares({ - operator: currentOperator, + operator: podOwnerOperator, // the 'staker' here is the address receiving new shares staker: staker, strategy: withdrawal.strategies[i], shares: increaseInDelegateableShares }); + + // push the operator's new stake to the StakeRegistry + _pushOperatorStakeUpdate(podOwnerOperator); } } else { strategyManager.addShares(msg.sender, withdrawal.strategies[i], withdrawal.shares[i]); @@ -687,9 +687,6 @@ contract DelegationManager is Initializable, OwnableUpgradeable, Pausable, Deleg strategy: strategies[i], shares: shares[i] }); - - // push the operator's new stake to the StakeRegistry - _pushOperatorStakeUpdate(operator); } // Remove active shares from EigenPodManager/StrategyManager @@ -709,6 +706,11 @@ contract DelegationManager is Initializable, OwnableUpgradeable, Pausable, Deleg unchecked { ++i; } } + // Push the operator's new stake to the StakeRegistry + if (operator != address(0)) { + _pushOperatorStakeUpdate(operator); + } + // Create queue entry and increment withdrawal nonce uint256 nonce = cumulativeWithdrawalsQueued[staker]; cumulativeWithdrawalsQueued[staker]++; diff --git a/src/test/mocks/DelegationManagerMock.sol b/src/test/mocks/DelegationManagerMock.sol index ac7c9b250..a50fd4d3d 100644 --- a/src/test/mocks/DelegationManagerMock.sol +++ b/src/test/mocks/DelegationManagerMock.sol @@ -3,6 +3,7 @@ pragma solidity =0.8.12; import "forge-std/Test.sol"; import "../../contracts/interfaces/IDelegationManager.sol"; +import "../../contracts/interfaces/IStrategyManager.sol"; contract DelegationManagerMock is IDelegationManager, Test { @@ -137,4 +138,33 @@ contract DelegationManagerMock is IDelegationManager, Test { ) external {} function migrateQueuedWithdrawals(IStrategyManager.DeprecatedStruct_QueuedWithdrawal[] memory withdrawalsToQueue) external {} + + // onlyDelegationManager functions in StrategyManager + function addShares( + IStrategyManager strategyManager, + address staker, + IStrategy strategy, + uint256 shares + ) external { + strategyManager.addShares(staker, strategy, shares); + } + + function removeShares( + IStrategyManager strategyManager, + address staker, + IStrategy strategy, + uint256 shares + ) external { + strategyManager.removeShares(staker, strategy, shares); + } + + function withdrawSharesAsTokens( + IStrategyManager strategyManager, + address recipient, + IStrategy strategy, + uint256 shares, + IERC20 token + ) external { + strategyManager.withdrawSharesAsTokens(recipient, strategy, shares, token); + } } \ No newline at end of file diff --git a/src/test/unit/StrategyManagerUnit.t.sol b/src/test/unit/StrategyManagerUnit.t.sol index b0fe36b00..4f95c6c40 100644 --- a/src/test/unit/StrategyManagerUnit.t.sol +++ b/src/test/unit/StrategyManagerUnit.t.sol @@ -140,7 +140,7 @@ contract StrategyManagerUnitTests is Test, Utils { initialOwner, initialOwner, pauserRegistry, - 0/*initialPausedStatus*/ + 0 /*initialPausedStatus*/ ) ) ) @@ -533,251 +533,6 @@ contract StrategyManagerUnitTests is Test, Utils { require(nonceAfter == nonceBefore, "nonceAfter != nonceBefore"); } - // Comment out withdraw tests to be moved - - // // queue and complete withdrawal. Ensure that strategy is no longer part - // function testQueueWithdrawalFullyWithdraw(uint256 amount) external { - // address staker = address(this); - // IDelegationManager.SignatureWithExpiry memory signatureWithExpiry; - // // deposit and withdraw the same amount - // testQueueWithdrawal_ToSelf(amount, amount); - // IStrategy[] memory strategyArray = new IStrategy[](1); - // IERC20[] memory tokensArray = new IERC20[](1); - // uint256[] memory shareAmounts = new uint256[](1); - // { - // strategyArray[0] = _tempStrategyStorage; - // shareAmounts[0] = amount; - // tokensArray[0] = dummyToken; - // } - - // uint256[] memory strategyIndexes = new uint256[](1); - // strategyIndexes[0] = 0; - - // IStrategyManager.QueuedWithdrawal memory queuedWithdrawal; - - // { - // uint256 nonce = strategyManager.numWithdrawalsQueued(staker); - - // IStrategyManager.WithdrawerAndNonce memory withdrawerAndNonce = IStrategyManager.WithdrawerAndNonce({ - // withdrawer: staker, - // nonce: (uint96(nonce) - 1) - // }); - // queuedWithdrawal = IStrategyManager.QueuedWithdrawal({ - // strategies: strategyArray, - // shares: shareAmounts, - // depositor: staker, - // withdrawerAndNonce: withdrawerAndNonce, - // withdrawalStartBlock: uint32(block.number), - // delegatedAddress: strategyManager.delegation().delegatedTo(staker) - // }); - // } - - // uint256 sharesBefore = strategyManager.stakerStrategyShares(staker, _tempStrategyStorage); - // uint256 balanceBefore = dummyToken.balanceOf(address(staker)); - - // cheats.expectEmit(true, true, true, true, address(strategyManager)); - // emit WithdrawalCompleted( - // queuedWithdrawal.depositor, - // queuedWithdrawal.withdrawerAndNonce.nonce, - // queuedWithdrawal.withdrawerAndNonce.withdrawer, - // strategyManager.calculateWithdrawalRoot(queuedWithdrawal) - // ); - // strategyManager.completeQueuedWithdrawal( - // queuedWithdrawal, - // tokensArray, - // /*middlewareTimesIndex*/ 0, - // /*receiveAsTokens*/ true - // ); - - // uint256 sharesAfter = strategyManager.stakerStrategyShares(staker, _tempStrategyStorage); - // uint256 balanceAfter = dummyToken.balanceOf(address(staker)); - - // require(sharesAfter == sharesBefore, "sharesAfter != sharesBefore"); - // require(balanceAfter == balanceBefore + amount, "balanceAfter != balanceBefore + withdrawalAmount"); - // require( - // !_isDepositedStrategy(staker, strategyArray[0]), - // "Strategy still part of staker's deposited strategies" - // ); - // require(sharesAfter == 0, "staker shares is not 0"); - // } - - // function testQueueWithdrawalRevertsWhenStakerFrozen(uint256 depositAmount, uint256 withdrawalAmount) public { - // cheats.assume(withdrawalAmount != 0 && withdrawalAmount <= depositAmount); - // address staker = address(this); - // IStrategy strategy = dummyStrat; - // IERC20 token = dummyToken; - - // testDepositIntoStrategySuccessfully(staker, depositAmount); - - // ( - // IStrategyManager.QueuedWithdrawal memory queuedWithdrawal /*IERC20[] memory tokensArray*/, - // , - // bytes32 withdrawalRoot - // ) = _setUpQueuedWithdrawalStructSingleStrat(staker, /*withdrawer*/ staker, token, strategy, withdrawalAmount); - - // uint256 sharesBefore = strategyManager.stakerStrategyShares(staker, strategy); - // uint256 nonceBefore = delegationManagerMock.cumulativeWithdrawalsQueued(staker); - - // require(!strategyManager.withdrawalRootPending(withdrawalRoot), "withdrawalRootPendingBefore is true!"); - - // // freeze the staker - // slasherMock.freezeOperator(staker); - - // uint256[] memory strategyIndexes = new uint256[](1); - // strategyIndexes[0] = 0; - // cheats.expectRevert( - // bytes("StrategyManager.onlyNotFrozen: staker has been frozen and may be subject to slashing") - // ); - // strategyManager.queueWithdrawal( - // strategyIndexes, - // queuedWithdrawal.strategies, - // queuedWithdrawal.shares, - // /*withdrawer*/ staker - // ); - - // uint256 sharesAfter = strategyManager.stakerStrategyShares(address(this), strategy); - // uint256 nonceAfter = delegationManagerMock.cumulativeWithdrawalsQueued(address(this)); - - // require(!strategyManager.withdrawalRootPending(withdrawalRoot), "withdrawalRootPendingAfter is true!"); - // require(sharesAfter == sharesBefore, "sharesAfter != sharesBefore"); - // require(nonceAfter == nonceBefore, "nonceAfter != nonceBefore"); - // } - - // function testCompleteQueuedWithdrawal_ReceiveAsTokensMarkedTrue( - // uint256 depositAmount, - // uint256 withdrawalAmount - // ) external { - // cheats.assume(withdrawalAmount != 0 && withdrawalAmount <= depositAmount); - // address staker = address(this); - // _tempStrategyStorage = dummyStrat; - - // testQueueWithdrawal_ToSelf(depositAmount, withdrawalAmount); - - // IStrategy[] memory strategyArray = new IStrategy[](1); - // IERC20[] memory tokensArray = new IERC20[](1); - // uint256[] memory shareAmounts = new uint256[](1); - // { - // strategyArray[0] = _tempStrategyStorage; - // shareAmounts[0] = withdrawalAmount; - // tokensArray[0] = dummyToken; - // } - - // uint256[] memory strategyIndexes = new uint256[](1); - // strategyIndexes[0] = 0; - - // IStrategyManager.QueuedWithdrawal memory queuedWithdrawal; - - // { - // uint256 nonce = delegationManagerMock.cumulativeWithdrawalsQueued(staker); - - // queuedWithdrawal = - // IDelegationManager.Withdrawal({ - // strategies: strategyArray, - // shares: shareAmounts, - // staker: staker, - // withdrawer: staker, - // nonce: (nonce - 1), - // startBlock: uint32(block.number), - // delegatedTo: strategyManager.delegation().delegatedTo(staker) - // } - // ); - // } - - // uint256 sharesBefore = strategyManager.stakerStrategyShares(staker, _tempStrategyStorage); - // uint256 balanceBefore = dummyToken.balanceOf(address(staker)); - - // cheats.expectEmit(true, true, true, true, address(strategyManager)); - // emit WithdrawalCompleted( - // queuedWithdrawal.depositor, - // queuedWithdrawal.withdrawerAndNonce.nonce, - // queuedWithdrawal.withdrawerAndNonce.withdrawer, - // strategyManager.calculateWithdrawalRoot(queuedWithdrawal) - // ); - // strategyManager.completeQueuedWithdrawal( - // queuedWithdrawal, - // tokensArray, - // /*middlewareTimesIndex*/ 0, - // /*receiveAsTokens*/ true - // ); - - // uint256 sharesAfter = strategyManager.stakerStrategyShares(staker, _tempStrategyStorage); - // uint256 balanceAfter = dummyToken.balanceOf(address(staker)); - - // require(sharesAfter == sharesBefore, "sharesAfter != sharesBefore"); - // require(balanceAfter == balanceBefore + withdrawalAmount, "balanceAfter != balanceBefore + withdrawalAmount"); - // if (depositAmount == withdrawalAmount) { - // // Since receiving tokens instead of shares, if withdrawal amount is entire deposit, then strategy will be removed - // // with sharesAfter being 0 - // require( - // !_isDepositedStrategy(staker, _tempStrategyStorage), - // "Strategy still part of staker's deposited strategies" - // ); - // require(sharesAfter == 0, "staker shares is not 0"); - // } - // } - - // function testCompleteQueuedWithdrawalWithNonzeroWithdrawalDelayBlocks( - // uint256 depositAmount, - // uint256 withdrawalAmount, - // uint16 valueToSet - // ) external { - // // filter fuzzed inputs to allowed *and nonzero* amounts - // cheats.assume(valueToSet <= strategyManager.MAX_WITHDRAWAL_DELAY_BLOCKS() && valueToSet != 0); - // cheats.assume(depositAmount != 0 && withdrawalAmount != 0); - // cheats.assume(depositAmount >= withdrawalAmount); - // address staker = address(this); - - // ( - // IStrategyManager.QueuedWithdrawal memory queuedWithdrawal, - // IERC20[] memory tokensArray /*bytes32 withdrawalRoot*/, - - // ) = testQueueWithdrawal_ToSelf(depositAmount, withdrawalAmount); - - // // cheats.expectRevert(bytes("StrategyManager.completeQueuedWithdrawal: withdrawalDelayBlocks period has not yet passed")); - // // strategyManager.completeQueuedWithdrawal(queuedWithdrawal, tokensArray, middlewareTimesIndex, receiveAsTokens); - // // } - - // // set the `withdrawalDelayBlocks` variable - // cheats.startPrank(strategyManager.owner()); - // uint256 previousValue = strategyManager.withdrawalDelayBlocks(); - // cheats.expectEmit(true, true, true, true, address(strategyManager)); - // emit WithdrawalDelayBlocksSet(previousValue, valueToSet); - // strategyManager.setWithdrawalDelayBlocks(valueToSet); - // cheats.stopPrank(); - // require( - // strategyManager.withdrawalDelayBlocks() == valueToSet, - // "strategyManager.withdrawalDelayBlocks() != valueToSet" - // ); - - // cheats.expectRevert( - // bytes("StrategyManager.completeQueuedWithdrawal: withdrawalDelayBlocks period has not yet passed") - // ); - // strategyManager.completeQueuedWithdrawal(queuedWithdrawal, tokensArray, middlewareTimesIndex, receiveAsTokens); - - // uint256 sharesBefore = strategyManager.stakerStrategyShares(address(this), dummyStrat); - // uint256 balanceBefore = dummyToken.balanceOf(address(staker)); - - // // roll block number forward to one block before the withdrawal should be completeable and attempt again - // uint256 originalBlockNumber = block.number; - // cheats.roll(originalBlockNumber + valueToSet - 1); - // cheats.expectRevert( - // bytes("StrategyManager.completeQueuedWithdrawal: withdrawalDelayBlocks period has not yet passed") - // ); - // strategyManager.completeQueuedWithdrawal(queuedWithdrawal, tokensArray, middlewareTimesIndex, receiveAsTokens); - - // // roll block number forward to the block at which the withdrawal should be completeable, and complete it - // cheats.roll(originalBlockNumber + valueToSet); - // strategyManager.completeQueuedWithdrawal(queuedWithdrawal, tokensArray, middlewareTimesIndex, receiveAsTokens); - - // uint256 sharesAfter = strategyManager.stakerStrategyShares(address(this), dummyStrat); - // uint256 balanceAfter = dummyToken.balanceOf(address(staker)); - - // require(sharesAfter == sharesBefore + withdrawalAmount, "sharesAfter != sharesBefore + withdrawalAmount"); - // require(balanceAfter == balanceBefore, "balanceAfter != balanceBefore"); - // } - - - function test_addSharesRevertsWhenSharesIsZero() external { // replace dummyStrat with Reenterer contract reenterer = new Reenterer(); @@ -1075,9 +830,234 @@ contract StrategyManagerUnitTests is Test, Utils { cheats.stopPrank(); } + function testAddSharesRevertsDelegationManagerModifier() external { + DelegationManagerMock invalidDelegationManager = new DelegationManagerMock(); + cheats.expectRevert(bytes("StrategyManager.onlyDelegationManager: not the DelegationManager")); + invalidDelegationManager.addShares(strategyManager, address(this), dummyStrat, 1); + } + + function testAddSharesRevertsStakerZeroAddress(uint256 amount) external { + cheats.expectRevert(bytes("StrategyManager._addShares: staker cannot be zero address")); + delegationManagerMock.addShares(strategyManager, address(0), dummyStrat, amount); + } + + function testAddSharesRevertsZeroShares(address staker) external { + cheats.assume(staker != address(0)); + cheats.expectRevert(bytes("StrategyManager._addShares: shares should not be zero!")); + delegationManagerMock.addShares(strategyManager, staker, dummyStrat, 0); + } + + function testAddSharesAppendsStakerStrategyList(address staker, uint256 amount) external { + cheats.assume(staker != address(0) && amount != 0); + uint256 stakerStrategyListLengthBefore = strategyManager.stakerStrategyListLength(staker); + uint256 sharesBefore = strategyManager.stakerStrategyShares(staker, dummyStrat); + require(sharesBefore == 0, "Staker has already deposited into this strategy"); + require(!_isDepositedStrategy(staker, dummyStrat), "strategy shouldn't be deposited"); + + delegationManagerMock.addShares(strategyManager, staker, dummyStrat, amount); + uint256 stakerStrategyListLengthAfter = strategyManager.stakerStrategyListLength(staker); + uint256 sharesAfter = strategyManager.stakerStrategyShares(staker, dummyStrat); + require( + stakerStrategyListLengthAfter == stakerStrategyListLengthBefore + 1, + "stakerStrategyListLengthAfter != stakerStrategyListLengthBefore + 1" + ); + require(sharesAfter == amount, "sharesAfter != amount"); + require(_isDepositedStrategy(staker, dummyStrat), "strategy should be deposited"); + } + + function testAddSharesExistingShares(address staker, uint256 sharesAmount) external { + cheats.assume(staker != address(0) && 0 < sharesAmount && sharesAmount <= dummyToken.totalSupply()); + uint256 initialAmount = 1e18; + IStrategy strategy = dummyStrat; + _depositIntoStrategySuccessfully(strategy, staker, initialAmount); + uint256 stakerStrategyListLengthBefore = strategyManager.stakerStrategyListLength(staker); + uint256 sharesBefore = strategyManager.stakerStrategyShares(staker, dummyStrat); + require(sharesBefore == initialAmount, "Staker has not deposited into strategy"); + require(_isDepositedStrategy(staker, strategy), "strategy should be deposited"); + + delegationManagerMock.addShares(strategyManager, staker, dummyStrat, sharesAmount); + uint256 stakerStrategyListLengthAfter = strategyManager.stakerStrategyListLength(staker); + uint256 sharesAfter = strategyManager.stakerStrategyShares(staker, dummyStrat); + require( + stakerStrategyListLengthAfter == stakerStrategyListLengthBefore, + "stakerStrategyListLengthAfter != stakerStrategyListLengthBefore" + ); + require(sharesAfter == sharesBefore + sharesAmount, "sharesAfter != sharesBefore + amount"); + require(_isDepositedStrategy(staker, strategy), "strategy should be deposited"); + } + + function testRemoveSharesRevertsDelegationManagerModifier() external { + DelegationManagerMock invalidDelegationManager = new DelegationManagerMock(); + cheats.expectRevert(bytes("StrategyManager.onlyDelegationManager: not the DelegationManager")); + invalidDelegationManager.removeShares(strategyManager, address(this), dummyStrat, 1); + } + + function testRemoveSharesRevertsShareAmountTooHigh( + address staker, + uint256 depositAmount, + uint256 removeSharesAmount + ) external { + cheats.assume(staker != address(0)); + cheats.assume(depositAmount > 0 && depositAmount < dummyToken.totalSupply()); + cheats.assume(removeSharesAmount > depositAmount); + IStrategy strategy = dummyStrat; + _depositIntoStrategySuccessfully(strategy, staker, depositAmount); + cheats.expectRevert(bytes("StrategyManager._removeShares: shareAmount too high")); + delegationManagerMock.removeShares(strategyManager, staker, strategy, removeSharesAmount); + } + + function testRemoveSharesRemovesStakerStrategyListSingleStrat(address staker, uint256 sharesAmount) external { + cheats.assume(staker != address(0)); + cheats.assume(sharesAmount > 0 && sharesAmount < dummyToken.totalSupply()); + IStrategy strategy = dummyStrat; + _depositIntoStrategySuccessfully(strategy, staker, sharesAmount); + + uint256 stakerStrategyListLengthBefore = strategyManager.stakerStrategyListLength(staker); + uint256 sharesBefore = strategyManager.stakerStrategyShares(staker, strategy); + require(sharesBefore == sharesAmount, "Staker has not deposited amount into strategy"); + + delegationManagerMock.removeShares(strategyManager, staker, strategy, sharesAmount); + uint256 stakerStrategyListLengthAfter = strategyManager.stakerStrategyListLength(staker); + uint256 sharesAfter = strategyManager.stakerStrategyShares(staker, strategy); + require( + stakerStrategyListLengthAfter == stakerStrategyListLengthBefore - 1, + "stakerStrategyListLengthAfter != stakerStrategyListLengthBefore - 1" + ); + require(sharesAfter == 0, "sharesAfter != 0"); + require(!_isDepositedStrategy(staker, strategy), "strategy should not be part of staker strategy list"); + } + + // Remove Strategy from staker strategy list with multiple strategies deposited + function testRemoveSharesRemovesStakerStrategyListMultipleStrat( + address staker, + uint256[3] memory amounts, + uint8 randStrategy + ) external { + cheats.assume(staker != address(0)); + IStrategy[] memory strategies = new IStrategy[](3); + strategies[0] = dummyStrat; + strategies[1] = dummyStrat2; + strategies[2] = dummyStrat3; + for (uint256 i = 0; i < 3; ++i) { + cheats.assume(amounts[i] > 0 && amounts[i] < dummyToken.totalSupply()); + _depositIntoStrategySuccessfully(strategies[i], staker, amounts[i]); + } + IStrategy removeStrategy = strategies[randStrategy % 3]; + uint256 removeAmount = amounts[randStrategy % 3]; + + uint256 stakerStrategyListLengthBefore = strategyManager.stakerStrategyListLength(staker); + uint256[] memory sharesBefore = new uint256[](3); + for (uint256 i = 0; i < 3; ++i) { + sharesBefore[i] = strategyManager.stakerStrategyShares(staker, strategies[i]); + require(sharesBefore[i] == amounts[i], "Staker has not deposited amount into strategy"); + require(_isDepositedStrategy(staker, strategies[i]), "strategy should be deposited"); + } + + delegationManagerMock.removeShares(strategyManager, staker, removeStrategy, removeAmount); + uint256 stakerStrategyListLengthAfter = strategyManager.stakerStrategyListLength(staker); + uint256 sharesAfter = strategyManager.stakerStrategyShares(staker, removeStrategy); + require( + stakerStrategyListLengthAfter == stakerStrategyListLengthBefore - 1, + "stakerStrategyListLengthAfter != stakerStrategyListLengthBefore - 1" + ); + require(sharesAfter == 0, "sharesAfter != 0"); + require(!_isDepositedStrategy(staker, removeStrategy), "strategy should not be part of staker strategy list"); + } + + // removeShares() from staker strategy list with multiple strategies deposited. Only callable by DelegationManager + function testRemoveShares(uint256[3] memory depositAmounts, uint256[3] memory sharesAmounts) external { + address staker = address(this); + IStrategy[] memory strategies = new IStrategy[](3); + strategies[0] = dummyStrat; + strategies[1] = dummyStrat2; + strategies[2] = dummyStrat3; + uint256[] memory sharesBefore = new uint256[](3); + for (uint256 i = 0; i < 3; ++i) { + cheats.assume(sharesAmounts[i] > 0 && sharesAmounts[i] <= depositAmounts[i]); + _depositIntoStrategySuccessfully(strategies[i], staker, depositAmounts[i]); + sharesBefore[i] = strategyManager.stakerStrategyShares(staker, strategies[i]); + require(sharesBefore[i] == depositAmounts[i], "Staker has not deposited amount into strategy"); + require(_isDepositedStrategy(staker, strategies[i]), "strategy should be deposited"); + } + uint256 stakerStrategyListLengthBefore = strategyManager.stakerStrategyListLength(staker); + + uint256 numPoppedStrategies = 0; + uint256[] memory sharesAfter = new uint256[](3); + for (uint256 i = 0; i < 3; ++i) { + delegationManagerMock.removeShares(strategyManager, staker, strategies[i], sharesAmounts[i]); + sharesAfter[i] = strategyManager.stakerStrategyShares(staker, strategies[i]); + if (sharesAmounts[i] == depositAmounts[i]) { + ++numPoppedStrategies; + require( + !_isDepositedStrategy(staker, strategies[i]), + "strategy should not be part of staker strategy list" + ); + require(sharesAfter[i] == 0, "sharesAfter != 0"); + } else { + require(_isDepositedStrategy(staker, strategies[i]), "strategy should be part of staker strategy list"); + require( + sharesAfter[i] == sharesBefore[i] - sharesAmounts[i], + "sharesAfter != sharesBefore - sharesAmounts" + ); + } + } + require( + stakerStrategyListLengthBefore - numPoppedStrategies == strategyManager.stakerStrategyListLength(staker), + "stakerStrategyListLengthBefore - numPoppedStrategies != strategyManager.stakerStrategyListLength(staker)" + ); + } + + function testWithdrawSharesAsTokensRevertsDelegationManagerModifier() external { + DelegationManagerMock invalidDelegationManager = new DelegationManagerMock(); + cheats.expectRevert(bytes("StrategyManager.onlyDelegationManager: not the DelegationManager")); + invalidDelegationManager.removeShares(strategyManager, address(this), dummyStrat, 1); + } + + function testWithdrawSharesAsTokensRevertsShareAmountTooHigh( + address staker, + uint256 depositAmount, + uint256 sharesAmount + ) external { + cheats.assume(staker != address(0)); + cheats.assume(depositAmount > 0 && depositAmount < dummyToken.totalSupply() && depositAmount < sharesAmount); + IStrategy strategy = dummyStrat; + IERC20 token = dummyToken; + _depositIntoStrategySuccessfully(strategy, staker, depositAmount); + cheats.expectRevert(bytes("StrategyBase.withdraw: amountShares must be less than or equal to totalShares")); + delegationManagerMock.withdrawSharesAsTokens(strategyManager, staker, strategy, sharesAmount, token); + } + + function testWithdrawSharesAsTokensSingleStrategyDeposited( + address staker, + uint256 depositAmount, + uint256 sharesAmount + ) external { + cheats.assume(staker != address(0)); + cheats.assume(sharesAmount > 0 && sharesAmount < dummyToken.totalSupply() && depositAmount >= sharesAmount); + IStrategy strategy = dummyStrat; + IERC20 token = dummyToken; + _depositIntoStrategySuccessfully(strategy, staker, depositAmount); + uint256 balanceBefore = token.balanceOf(staker); + delegationManagerMock.withdrawSharesAsTokens(strategyManager, staker, strategy, sharesAmount, token); + uint256 balanceAfter = token.balanceOf(staker); + require(balanceAfter == balanceBefore + sharesAmount, "balanceAfter != balanceBefore + sharesAmount"); + } + // INTERNAL / HELPER FUNCTIONS - function _setUpQueuedWithdrawalStructSingleStrat(address staker, address withdrawer, IERC20 token, IStrategy strategy, uint256 shareAmount) - internal view returns (IDelegationManager.Withdrawal memory queuedWithdrawal, IERC20[] memory tokensArray, bytes32 withdrawalRoot) + function _setUpQueuedWithdrawalStructSingleStrat( + address staker, + address withdrawer, + IERC20 token, + IStrategy strategy, + uint256 shareAmount + ) + internal + view + returns ( + IDelegationManager.Withdrawal memory queuedWithdrawal, + IERC20[] memory tokensArray, + bytes32 withdrawalRoot + ) { IStrategy[] memory strategyArray = new IStrategy[](1); tokensArray = new IERC20[](1); @@ -1085,17 +1065,15 @@ contract StrategyManagerUnitTests is Test, Utils { strategyArray[0] = strategy; tokensArray[0] = token; shareAmounts[0] = shareAmount; - queuedWithdrawal = - IDelegationManager.Withdrawal({ - strategies: strategyArray, - shares: shareAmounts, - staker: staker, - withdrawer: withdrawer, - nonce: delegationManagerMock.cumulativeWithdrawalsQueued(staker), - startBlock: uint32(block.number), - delegatedTo: strategyManager.delegation().delegatedTo(staker) - } - ); + queuedWithdrawal = IDelegationManager.Withdrawal({ + strategies: strategyArray, + shares: shareAmounts, + staker: staker, + withdrawer: withdrawer, + nonce: delegationManagerMock.cumulativeWithdrawalsQueued(staker), + startBlock: uint32(block.number), + delegatedTo: strategyManager.delegation().delegatedTo(staker) + }); // calculate the withdrawal root withdrawalRoot = delegationManagerMock.calculateWithdrawalRoot(queuedWithdrawal); return (queuedWithdrawal, tokensArray, withdrawalRoot); @@ -1148,20 +1126,16 @@ contract StrategyManagerUnitTests is Test, Utils { address withdrawer, IStrategy[] memory strategyArray, uint256[] memory shareAmounts - ) - internal view returns (IDelegationManager.Withdrawal memory queuedWithdrawal, bytes32 withdrawalRoot) - { - queuedWithdrawal = - IDelegationManager.Withdrawal({ - strategies: strategyArray, - shares: shareAmounts, - staker: staker, - withdrawer: withdrawer, - nonce: delegationManagerMock.cumulativeWithdrawalsQueued(staker), - startBlock: uint32(block.number), - delegatedTo: strategyManager.delegation().delegatedTo(staker) - } - ); + ) internal view returns (IDelegationManager.Withdrawal memory queuedWithdrawal, bytes32 withdrawalRoot) { + queuedWithdrawal = IDelegationManager.Withdrawal({ + strategies: strategyArray, + shares: shareAmounts, + staker: staker, + withdrawer: withdrawer, + nonce: delegationManagerMock.cumulativeWithdrawalsQueued(staker), + startBlock: uint32(block.number), + delegatedTo: strategyManager.delegation().delegatedTo(staker) + }); // calculate the withdrawal root withdrawalRoot = delegationManagerMock.calculateWithdrawalRoot(queuedWithdrawal); return (queuedWithdrawal, withdrawalRoot);