From 64eafb384b9dc0fb34e83432665c8046cba6c286 Mon Sep 17 00:00:00 2001 From: Alex <18387287+wadealexc@users.noreply.github.com> Date: Thu, 30 Nov 2023 10:55:12 -0500 Subject: [PATCH] test: implement random withdrawal generator (#349) see PR for changes/notes --- src/test/integration/IntegrationBase.t.sol | 224 +++++++++-- .../integration/IntegrationDeployer.t.sol | 12 +- src/test/integration/User.t.sol | 8 +- .../Deposit_Delegate_Queue_Complete.t.sol | 363 ++++++++++++++++-- 4 files changed, 540 insertions(+), 67 deletions(-) diff --git a/src/test/integration/IntegrationBase.t.sol b/src/test/integration/IntegrationBase.t.sol index 397e90cf82..137cc5b01e 100644 --- a/src/test/integration/IntegrationBase.t.sol +++ b/src/test/integration/IntegrationBase.t.sol @@ -33,8 +33,8 @@ abstract contract IntegrationBase is IntegrationDeployer { operator.registerAsOperator(); operator.depositIntoEigenlayer(strategies, tokenBalances); - assert_Snap_AddedStakerShares(operator, strategies, tokenBalances, "_newRandomOperator: failed to add delegatable shares"); - assert_Snap_AddedOperatorShares(operator, strategies, tokenBalances, "_newRandomOperator: failed to award shares to operator"); + assert_Snap_Added_StakerShares(operator, strategies, tokenBalances, "_newRandomOperator: failed to add delegatable shares"); + assert_Snap_Added_OperatorShares(operator, strategies, tokenBalances, "_newRandomOperator: failed to award shares to operator"); assertTrue(delegationManager.isOperator(address(operator)), "_newRandomOperator: operator should be registered"); return (operator, strategies, tokenBalances); @@ -120,12 +120,14 @@ abstract contract IntegrationBase is IntegrationDeployer { } } + /// @dev Asserts that ALL of the `withdrawalRoots` is in `delegationManager.pendingWithdrawals` function assert_AllWithdrawalsPending(bytes32[] memory withdrawalRoots, string memory err) internal { for (uint i = 0; i < withdrawalRoots.length; i++) { assert_withdrawalPending(withdrawalRoots[i], err); } } +<<<<<<< HEAD function assert_withdrawalPending(bytes32 withdrawalRoot, string memory err) internal { assertTrue(delegationManager.pendingWithdrawals(withdrawalRoot), err); } @@ -140,13 +142,29 @@ abstract contract IntegrationBase is IntegrationDeployer { assertFalse(delegationManager.pendingWithdrawals(withdrawalRoot), err); } +======= + /// @dev Asserts that NONE of the `withdrawalRoots` is in `delegationManager.pendingWithdrawals` + function assert_NoWithdrawalsPending(bytes32[] memory withdrawalRoots, string memory err) internal { + for (uint i = 0; i < withdrawalRoots.length; i++) { + assertFalse(delegationManager.pendingWithdrawals(withdrawalRoots[i]), err); + } + } + + /// @dev Asserts that the hash of each withdrawal corresponds to the provided withdrawal root +>>>>>>> 272a7e2f (test: implement random withdrawal generator (#349)) function assert_ValidWithdrawalHashes( IDelegationManager.Withdrawal[] memory withdrawals, bytes32[] memory withdrawalRoots, string memory err ) internal { + bytes32[] memory expectedRoots = _getWithdrawalHashes(withdrawals); + for (uint i = 0; i < withdrawals.length; i++) { +<<<<<<< HEAD assert_ValidWithdrawalHash(withdrawals[i], withdrawalRoots[i], err); +======= + assertEq(withdrawalRoots[i], expectedRoots[i], err); +>>>>>>> 272a7e2f (test: implement random withdrawal generator (#349)) } } @@ -158,14 +176,16 @@ abstract contract IntegrationBase is IntegrationDeployer { assertEq(withdrawalRoot, delegationManager.calculateWithdrawalRoot(withdrawal), err); } - /** - * Snapshot assertions combine Timemachine's snapshots with assertions - * that allow easy comparisons between prev/cur values - */ + /******************************************************************************* + SNAPSHOT ASSERTIONS + TIME TRAVELERS ONLY BEYOND THIS POINT + *******************************************************************************/ - /// @dev Check that the operator has `addedShares` additional shares for each - /// strategy since the last snapshot - function assert_Snap_AddedOperatorShares( + /// Snapshot assertions for delegationManager.operatorShares: + + /// @dev Check that the operator has `addedShares` additional operator shares + // for each strategy since the last snapshot + function assert_Snap_Added_OperatorShares( User operator, IStrategy[] memory strategies, uint[] memory addedShares, @@ -181,9 +201,9 @@ abstract contract IntegrationBase is IntegrationDeployer { } } - /// @dev Check that the operator has `removedShares` prior shares for each - /// strategy since the last snapshot - function assert_Snap_RemovedOperatorShares( + /// @dev Check that the operator has `removedShares` fewer operator shares + /// for each strategy since the last snapshot + function assert_Snap_Removed_OperatorShares( User operator, IStrategy[] memory strategies, uint[] memory removedShares, @@ -199,9 +219,29 @@ abstract contract IntegrationBase is IntegrationDeployer { } } - /// @dev Check that the staker has `addedShares` additional shares for each - /// strategy since the last snapshot - function assert_Snap_AddedStakerShares( + /// @dev Check that the operator's shares in ALL strategies have not changed + /// since the last snapshot + function assert_Snap_Unchanged_OperatorShares( + User operator, + string memory err + ) internal { + IStrategy[] memory strategies = allStrats; + + uint[] memory curShares = _getOperatorShares(operator, strategies); + // Use timewarp to get previous operator shares + uint[] memory prevShares = _getPrevOperatorShares(operator, strategies); + + // For each strategy, check (prev == cur) + for (uint i = 0; i < strategies.length; i++) { + assertEq(prevShares[i], curShares[i], err); + } + } + + /// Snapshot assertions for strategyMgr.stakerStrategyShares and eigenPodMgr.podOwnerShares: + + /// @dev Check that the staker has `addedShares` additional delegatable shares + /// for each strategy since the last snapshot + function assert_Snap_Added_StakerShares( User staker, IStrategy[] memory strategies, uint[] memory addedShares, @@ -217,9 +257,9 @@ abstract contract IntegrationBase is IntegrationDeployer { } } - /// @dev Check that the staker has `removedShares` prior shares for each - /// strategy since the last snapshot - function assert_Snap_RemovedStakerShares( + /// @dev Check that the staker has `removedShares` fewer delegatable shares + /// for each strategy since the last snapshot + function assert_Snap_Removed_StakerShares( User staker, IStrategy[] memory strategies, uint[] memory removedShares, @@ -235,18 +275,25 @@ abstract contract IntegrationBase is IntegrationDeployer { } } - function assert_Snap_IncreasedQueuedWithdrawals( - User staker, - IDelegationManager.Withdrawal[] memory withdrawals, + /// @dev Check that the staker's delegatable shares in ALL strategies have not changed + /// since the last snapshot + function assert_Snap_Unchanged_StakerShares( + User staker, string memory err ) internal { - uint curQueuedWithdrawals = _getCumulativeWithdrawals(staker); - // Use timewarp to get previous cumulative withdrawals - uint prevQueuedWithdrawals = _getPrevCumulativeWithdrawals(staker); + IStrategy[] memory strategies = allStrats; - assertEq(prevQueuedWithdrawals + withdrawals.length, curQueuedWithdrawals, err); + uint[] memory curShares = _getStakerShares(staker, strategies); + // Use timewarp to get previous staker shares + uint[] memory prevShares = _getPrevStakerShares(staker, strategies); + + // For each strategy, check (prev == cur) + for (uint i = 0; i < strategies.length; i++) { + assertEq(prevShares[i], curShares[i], err); + } } +<<<<<<< HEAD function assert_Snap_IncrementQueuedWithdrawals( User staker, string memory err @@ -259,6 +306,13 @@ abstract contract IntegrationBase is IntegrationDeployer { } function assert_Snap_IncreasedTokenBalances( +======= + /// Snapshot assertions for underlying token balances: + + /// @dev Check that the staker has `addedTokens` additional underlying tokens + // since the last snapshot + function assert_Snap_Added_TokenBalances( +>>>>>>> 272a7e2f (test: implement random withdrawal generator (#349)) User staker, IERC20[] memory tokens, uint[] memory addedTokens, @@ -276,6 +330,7 @@ abstract contract IntegrationBase is IntegrationDeployer { } } +<<<<<<< HEAD function assert_Snap_DecreasedStrategyShares( IStrategy[] memory strategies, uint256[] memory removedShares, @@ -318,6 +373,96 @@ abstract contract IntegrationBase is IntegrationDeployer { /** * Helpful getters: */ +======= + /// @dev Check that the staker has `removedTokens` fewer underlying tokens + // since the last snapshot + function assert_Snap_Removed_TokenBalances( + User staker, + IStrategy[] memory strategies, + uint[] memory removedTokens, + string memory err + ) internal { + IERC20[] memory tokens = _getUnderlyingTokens(strategies); + + uint[] memory curTokenBalances = _getTokenBalances(staker, tokens); + // Use timewarp to get previous token balances + uint[] memory prevTokenBalances = _getPrevTokenBalances(staker, tokens); + + for (uint i = 0; i < tokens.length; i++) { + uint prevBalance = prevTokenBalances[i]; + uint curBalance = curTokenBalances[i]; + + assertEq(prevBalance - removedTokens[i], curBalance, err); + } + } + + /// @dev Check that the staker's underlying token balance for ALL tokens have + /// not changed since the last snapshot + function assert_Snap_Unchanged_TokenBalances( + User staker, + string memory err + ) internal { + IERC20[] memory tokens = allTokens; + + uint[] memory curTokenBalances = _getTokenBalances(staker, tokens); + // Use timewarp to get previous token balances + uint[] memory prevTokenBalances = _getPrevTokenBalances(staker, tokens); + + for (uint i = 0; i < tokens.length; i++) { + assertEq(prevTokenBalances[i], curTokenBalances[i], err); + } + } + + /// Other snapshot assertions: + + function assert_Snap_Added_QueuedWithdrawals( + User staker, + IDelegationManager.Withdrawal[] memory withdrawals, + string memory err + ) internal { + uint curQueuedWithdrawals = _getCumulativeWithdrawals(staker); + // Use timewarp to get previous cumulative withdrawals + uint prevQueuedWithdrawals = _getPrevCumulativeWithdrawals(staker); + + assertEq(prevQueuedWithdrawals + withdrawals.length, curQueuedWithdrawals, err); + } + + /******************************************************************************* + UTILITY METHODS + *******************************************************************************/ + + function _randWithdrawal( + IStrategy[] memory strategies, + uint[] memory shares + ) internal returns (IStrategy[] memory, uint[] memory) { + uint stratsToWithdraw = _randUint({ min: 1, max: strategies.length }); + + IStrategy[] memory withdrawStrats = new IStrategy[](stratsToWithdraw); + uint[] memory withdrawShares = new uint[](stratsToWithdraw); + + for (uint i = 0; i < stratsToWithdraw; i++) { + uint sharesToWithdraw; + + if (strategies[i] == BEACONCHAIN_ETH_STRAT) { + // For native eth, withdraw a random amount of gwei (at least 1) + uint portion = _randUint({ min: 1, max: shares[i] / GWEI_TO_WEI }); + portion *= GWEI_TO_WEI; + + sharesToWithdraw = shares[i] - portion; + } else { + // For LSTs, withdraw a random amount of shares (at least 1) + uint portion = _randUint({ min: 1, max: shares[i] }); + + sharesToWithdraw = shares[i] - portion; + } + + withdrawStrats[i] = strategies[i]; + withdrawShares[i] = sharesToWithdraw; + } + + return (withdrawStrats, withdrawShares); + } +>>>>>>> 272a7e2f (test: implement random withdrawal generator (#349)) /// @dev For some strategies/underlying token balances, calculate the expected shares received /// from depositing all tokens @@ -356,6 +501,35 @@ abstract contract IntegrationBase is IntegrationDeployer { return expectedTokens; } + function _getWithdrawalHashes( + IDelegationManager.Withdrawal[] memory withdrawals + ) internal view returns (bytes32[] memory) { + bytes32[] memory withdrawalRoots = new bytes32[](withdrawals.length); + + for (uint i = 0; i < withdrawals.length; i++) { + withdrawalRoots[i] = delegationManager.calculateWithdrawalRoot(withdrawals[i]); + } + + return withdrawalRoots; + } + + /// @dev Converts a list of strategies to underlying tokens + function _getUnderlyingTokens(IStrategy[] memory strategies) internal view returns (IERC20[] memory) { + IERC20[] memory tokens = new IERC20[](strategies.length); + + for (uint i = 0; i < tokens.length; i++) { + IStrategy strat = strategies[i]; + + if (strat == BEACONCHAIN_ETH_STRAT) { + tokens[i] = NATIVE_ETH; + } else { + tokens[i] = strat.underlyingToken(); + } + } + + return tokens; + } + modifier timewarp() { uint curState = timeMachine.warpToLast(); _; diff --git a/src/test/integration/IntegrationDeployer.t.sol b/src/test/integration/IntegrationDeployer.t.sol index c867c6f27b..ddf8dafba2 100644 --- a/src/test/integration/IntegrationDeployer.t.sol +++ b/src/test/integration/IntegrationDeployer.t.sol @@ -50,7 +50,8 @@ abstract contract IntegrationDeployer is Test, IUserDeployer { // which of these lists to select user assets from. IStrategy[] lstStrats; IStrategy[] ethStrats; // only has one strat tbh - IStrategy[] mixedStrats; // just a combination of the above 2 lists + IStrategy[] allStrats; // just a combination of the above 2 lists + IERC20[] allTokens; // `allStrats`, but contains all of the underlying tokens instead // Mock Contracts to deploy ETHPOSDepositMock ethPOSDeposit; @@ -80,6 +81,7 @@ abstract contract IntegrationDeployer is Test, IUserDeployer { uint constant MIN_BALANCE = 1e6; uint constant MAX_BALANCE = 5e6; + uint constant GWEI_TO_WEI = 1e9; // Flags uint constant FLAG = 1; @@ -256,7 +258,8 @@ abstract contract IntegrationDeployer is Test, IUserDeployer { _newStrategyAndToken("Strategy3Token", "str3", 10e50, address(this)); // initialSupply, owner ethStrats.push(BEACONCHAIN_ETH_STRAT); - mixedStrats.push(BEACONCHAIN_ETH_STRAT); + allStrats.push(BEACONCHAIN_ETH_STRAT); + allTokens.push(NATIVE_ETH); // Create time machine and set block timestamp forward so we can create EigenPod proofs in the past timeMachine = new TimeMachine(); @@ -286,9 +289,10 @@ abstract contract IntegrationDeployer is Test, IUserDeployer { cheats.prank(strategyManager.strategyWhitelister()); strategyManager.addStrategiesToDepositWhitelist(strategies); - // Add to lstStrats and mixedStrats + // Add to lstStrats and allStrats lstStrats.push(strategy); - mixedStrats.push(strategy); + allStrats.push(strategy); + allTokens.push(underlyingToken); } function _configRand( diff --git a/src/test/integration/User.t.sol b/src/test/integration/User.t.sol index 296e516a2c..1391f70db9 100644 --- a/src/test/integration/User.t.sol +++ b/src/test/integration/User.t.sol @@ -137,13 +137,11 @@ contract User is Test { function queueWithdrawals( IStrategy[] memory strategies, uint[] memory shares - ) public createSnapshot virtual returns (IDelegationManager.Withdrawal[] memory, bytes32[] memory) { + ) public createSnapshot virtual returns (IDelegationManager.Withdrawal[] memory) { address operator = delegationManager.delegatedTo(address(this)); address withdrawer = address(this); uint nonce = delegationManager.cumulativeWithdrawalsQueued(address(this)); - - bytes32[] memory withdrawalRoots; // Create queueWithdrawals params IDelegationManager.QueuedWithdrawalParams[] memory params = new IDelegationManager.QueuedWithdrawalParams[](1); @@ -165,12 +163,12 @@ contract User is Test { shares: shares }); - withdrawalRoots = delegationManager.queueWithdrawals(params); + bytes32[] memory withdrawalRoots = delegationManager.queueWithdrawals(params); // Basic sanity check - we do all other checks outside this file assertEq(withdrawals.length, withdrawalRoots.length, "User.queueWithdrawals: length mismatch"); - return (withdrawals, withdrawalRoots); + return (withdrawals); } function completeQueuedWithdrawal( diff --git a/src/test/integration/tests/Deposit_Delegate_Queue_Complete.t.sol b/src/test/integration/tests/Deposit_Delegate_Queue_Complete.t.sol index 81a205a278..c4df08e863 100644 --- a/src/test/integration/tests/Deposit_Delegate_Queue_Complete.t.sol +++ b/src/test/integration/tests/Deposit_Delegate_Queue_Complete.t.sol @@ -6,11 +6,15 @@ import "src/test/integration/User.t.sol"; contract Integration_Deposit_Delegate_Queue_Complete is IntegrationBase { - /// Randomly generates a user with different held assets. Then: - /// 1. deposit into strategy - /// 2. delegate to an operator - /// 3. queue a withdrawal for all shares (withdrawer set to staker) - /// 4. complete their queued withdrawal as tokens + /******************************************************************************* + FULL WITHDRAWALS + *******************************************************************************/ + + /// Generates a random staker and operator. The staker: + /// 1. deposits all assets into strategies + /// 2. delegates to an operator + /// 3. queues a withdrawal for a ALL shares + /// 4. completes the queued withdrawal as tokens function testFuzz_deposit_delegate_queue_completeAsTokens(uint24 _random) public { // When new Users are created, they will choose a random configuration from these params: _configRand({ @@ -45,7 +49,7 @@ contract Integration_Deposit_Delegate_Queue_Complete is IntegrationBase { staker.depositIntoEigenlayer(strategies, tokenBalances); assert_HasNoUnderlyingTokenBalance(staker, strategies, "staker should have transferred all underlying tokens"); - assert_Snap_AddedStakerShares(staker, strategies, shares, "staker should expected shares in each strategy after depositing"); + assert_Snap_Added_StakerShares(staker, strategies, shares, "staker should expected shares in each strategy after depositing"); } { @@ -57,13 +61,229 @@ contract Integration_Deposit_Delegate_Queue_Complete is IntegrationBase { assertTrue(delegationManager.isDelegated(address(staker)), "staker should be delegated"); assertEq(address(operator), delegationManager.delegatedTo(address(staker)), "staker should be delegated to operator"); - assert_HasExpectedShares(staker, strategies, shares, "staker should still have expected shares after delegating"); - assert_Snap_AddedOperatorShares(operator, strategies, shares, "operator should have received shares"); + assert_Snap_Unchanged_StakerShares(staker, "staker shares should be unchanged after delegating"); + assert_Snap_Added_OperatorShares(operator, strategies, shares, "operator should have received shares"); } IDelegationManager.Withdrawal[] memory withdrawals; bytes32[] memory withdrawalRoots; + { + /// 3. Queue withdrawal(s): + // The staker will queue one or more withdrawals for all strategies and shares + // + // ... check that each withdrawal was successfully enqueued, that the returned withdrawals + // match now-pending withdrawal roots, and that the staker and operator have + // reduced shares. + withdrawals = staker.queueWithdrawals(strategies, shares); + withdrawalRoots = _getWithdrawalHashes(withdrawals); + + assert_ValidWithdrawalHashes(withdrawals, withdrawalRoots, "calculated withdrawals should match returned roots"); + assert_AllWithdrawalsPending(withdrawalRoots, "staker's withdrawals should now be pending"); + assert_Snap_Added_QueuedWithdrawals(staker, withdrawals, "staker should have increased nonce by withdrawals.length"); + assert_Snap_Removed_OperatorShares(operator, strategies, shares, "failed to remove operator shares"); + assert_Snap_Removed_StakerShares(staker, strategies, shares, "failed to remove staker shares"); + } + + // Fast forward to when we can complete the withdrawal + cheats.roll(block.number + delegationManager.withdrawalDelayBlocks()); + + { + /// 4. Complete withdrawal(s): + // The staker will complete each withdrawal as tokens + // + // ... check that the staker received their tokens + for (uint i = 0; i < withdrawals.length; i++) { + IDelegationManager.Withdrawal memory withdrawal = withdrawals[i]; + + uint[] memory expectedTokens = _calculateExpectedTokens(withdrawal.strategies, withdrawal.shares); + IERC20[] memory tokens = staker.completeQueuedWithdrawal(withdrawal, true); + + assert_Snap_Added_TokenBalances(staker, tokens, expectedTokens, "staker should have received expected tokens"); + assert_Snap_Unchanged_TokenBalances(operator, "operator token balances should not have changed"); + assert_Snap_Unchanged_StakerShares(staker, "staker shares should not have changed"); + assert_Snap_Unchanged_OperatorShares(operator, "operator shares should not have changed"); + } + } + + // Check final state: + assertEq(address(operator), delegationManager.delegatedTo(address(staker)), "staker should still be delegated to operator"); + assert_HasNoDelegatableShares(staker, "staker should have withdrawn all shares"); + assert_HasUnderlyingTokenBalances(staker, strategies, tokenBalances, "staker should once again have original token balances"); + assert_NoWithdrawalsPending(withdrawalRoots, "all withdrawals should be removed from pending"); + } + + /// Generates a random staker and operator. The staker: + /// 1. deposits all assets into strategies + /// 2. delegates to an operator + /// 3. queues a withdrawal for a ALL shares + /// 4. completes the queued withdrawal as shares + function testFuzz_deposit_delegate_queue_completeAsShares(uint24 _random) public { + // When new Users are created, they will choose a random configuration from these params: + _configRand({ + _randomSeed: _random, + _assetTypes: HOLDS_LST | HOLDS_ETH | HOLDS_ALL, + _userTypes: DEFAULT | ALT_METHODS + }); + + /// 0. Create an operator and a staker with: + // - some nonzero underlying token balances + // - corresponding to a random subset of valid strategies (StrategyManager and/or EigenPodManager) + // + // ... check that the staker has no delegatable shares and isn't currently delegated + ( + User staker, + IStrategy[] memory strategies, + uint[] memory tokenBalances + ) = _newRandomStaker(); + (User operator, ,) = _newRandomOperator(); + uint[] memory shares = _calculateExpectedShares(strategies, tokenBalances); + + assert_HasNoDelegatableShares(staker, "staker should not have delegatable shares before depositing"); + assertFalse(delegationManager.isDelegated(address(staker)), "staker should not be delegated"); + + { + /// 1. Deposit into strategies: + // For each of the assets held by the staker (either StrategyManager or EigenPodManager), + // the staker calls the relevant deposit function, depositing all held assets. + // + // ... check that all underlying tokens were transferred to the correct destination + // and that the staker now has the expected amount of delegated shares in each strategy + staker.depositIntoEigenlayer(strategies, tokenBalances); + + assert_HasNoUnderlyingTokenBalance(staker, strategies, "staker should have transferred all underlying tokens"); + assert_Snap_Added_StakerShares(staker, strategies, shares, "staker should expected shares in each strategy after depositing"); + } + + { + /// 2. Delegate to an operator: + // + // ... check that the staker is now delegated to the operator, and that the operator + // was awarded the staker's shares + staker.delegateTo(operator); + + assertTrue(delegationManager.isDelegated(address(staker)), "staker should be delegated"); + assertEq(address(operator), delegationManager.delegatedTo(address(staker)), "staker should be delegated to operator"); + assert_Snap_Unchanged_StakerShares(staker, "staker shares should be unchanged after delegating"); + assert_Snap_Added_OperatorShares(operator, strategies, shares, "operator should have received shares"); + } + + IDelegationManager.Withdrawal[] memory withdrawals; + bytes32[] memory withdrawalRoots; + + { + /// 3. Queue withdrawal(s): + // The staker will queue one or more withdrawals for all strategies and shares + // + // ... check that each withdrawal was successfully enqueued, that the returned withdrawals + // match now-pending withdrawal roots, and that the staker and operator have + // reduced shares. + withdrawals = staker.queueWithdrawals(strategies, shares); + withdrawalRoots = _getWithdrawalHashes(withdrawals); + + assert_ValidWithdrawalHashes(withdrawals, withdrawalRoots, "calculated withdrawals should match returned roots"); + assert_AllWithdrawalsPending(withdrawalRoots, "staker's withdrawals should now be pending"); + assert_Snap_Added_QueuedWithdrawals(staker, withdrawals, "staker should have increased nonce by withdrawals.length"); + assert_Snap_Removed_OperatorShares(operator, strategies, shares, "failed to remove operator shares"); + assert_Snap_Removed_StakerShares(staker, strategies, shares, "failed to remove staker shares"); + } + + // Fast forward to when we can complete the withdrawal + cheats.roll(block.number + delegationManager.withdrawalDelayBlocks()); + + { + /// 4. Complete withdrawal(s): + // The staker will complete each withdrawal as tokens + // + // ... check that the staker and operator received their shares and that neither + // have any change in token balances + for (uint i = 0; i < withdrawals.length; i++) { + IDelegationManager.Withdrawal memory withdrawal = withdrawals[i]; + + staker.completeQueuedWithdrawal(withdrawal, false); + + assert_Snap_Unchanged_TokenBalances(staker, "staker should not have any change in underlying token balances"); + assert_Snap_Unchanged_TokenBalances(operator, "operator should not have any change in underlying token balances"); + assert_Snap_Added_StakerShares(staker, withdrawal.strategies, withdrawal.shares, "staker should have received shares"); + assert_Snap_Added_OperatorShares(operator, withdrawal.strategies, withdrawal.shares, "operator should have received shares"); + } + } + + // Check final state: + assertEq(address(operator), delegationManager.delegatedTo(address(staker)), "staker should still be delegated to operator"); + assert_HasExpectedShares(staker, strategies, shares, "staker should have all original shares"); + assert_HasNoUnderlyingTokenBalance(staker, strategies, "staker not have any underlying tokens"); + assert_NoWithdrawalsPending(withdrawalRoots, "all withdrawals should be removed from pending"); + } + + /******************************************************************************* + RANDOM WITHDRAWALS + *******************************************************************************/ + + /// Generates a random staker and operator. The staker: + /// 1. deposits all assets into strategies + /// 2. delegates to an operator + /// 3. queues a withdrawal for a random subset of shares + /// 4. completes the queued withdrawal as tokens + function testFuzz_deposit_delegate_queueRand_completeAsTokens(uint24 _random) public { + // When new Users are created, they will choose a random configuration from these params: + _configRand({ + _randomSeed: _random, + _assetTypes: HOLDS_LST | HOLDS_ETH | HOLDS_ALL, + _userTypes: DEFAULT | ALT_METHODS + }); + + /// 0. Create an operator and a staker with: + // - some nonzero underlying token balances + // - corresponding to a random subset of valid strategies (StrategyManager and/or EigenPodManager) + // + // ... check that the staker has no delegatable shares and isn't currently delegated + ( + User staker, + IStrategy[] memory strategies, + uint[] memory tokenBalances + ) = _newRandomStaker(); + (User operator, ,) = _newRandomOperator(); + uint[] memory shares = _calculateExpectedShares(strategies, tokenBalances); + + assert_HasNoDelegatableShares(staker, "staker should not have delegatable shares before depositing"); + assertFalse(delegationManager.isDelegated(address(staker)), "staker should not be delegated"); + + { + /// 1. Deposit into strategies: + // For each of the assets held by the staker (either StrategyManager or EigenPodManager), + // the staker calls the relevant deposit function, depositing all held assets. + // + // ... check that all underlying tokens were transferred to the correct destination + // and that the staker now has the expected amount of delegated shares in each strategy + staker.depositIntoEigenlayer(strategies, tokenBalances); + + assert_HasNoUnderlyingTokenBalance(staker, strategies, "staker should have transferred all underlying tokens"); + assert_Snap_Added_StakerShares(staker, strategies, shares, "staker should expected shares in each strategy after depositing"); + } + + { + /// 2. Delegate to an operator: + // + // ... check that the staker is now delegated to the operator, and that the operator + // was awarded the staker's shares + staker.delegateTo(operator); + + assertTrue(delegationManager.isDelegated(address(staker)), "staker should be delegated"); + assertEq(address(operator), delegationManager.delegatedTo(address(staker)), "staker should be delegated to operator"); + assert_Snap_Unchanged_StakerShares(staker, "staker shares should be unchanged after delegating"); + assert_Snap_Added_OperatorShares(operator, strategies, shares, "operator should have received shares"); + } + + // Randomly select one or more assets to withdraw + ( + IStrategy[] memory withdrawStrats, + uint[] memory withdrawShares + ) = _randWithdrawal(strategies, shares); + + IDelegationManager.Withdrawal[] memory withdrawals; + bytes32[] memory withdrawalRoots; + { /// 3. Queue withdrawal(s): // The staker will queue one or more withdrawals for the selected strategies and shares @@ -71,13 +291,14 @@ contract Integration_Deposit_Delegate_Queue_Complete is IntegrationBase { // ... check that each withdrawal was successfully enqueued, that the returned roots // match the hashes of each withdrawal, and that the staker and operator have // reduced shares. - (withdrawals, withdrawalRoots) = staker.queueWithdrawals(strategies, shares); + withdrawals = staker.queueWithdrawals(withdrawStrats, withdrawShares); + withdrawalRoots = _getWithdrawalHashes(withdrawals); - assert_AllWithdrawalsPending(withdrawalRoots, "staker's withdrawals should now be pending"); assert_ValidWithdrawalHashes(withdrawals, withdrawalRoots, "calculated withdrawals should match returned roots"); - assert_Snap_IncreasedQueuedWithdrawals(staker, withdrawals, "staker should have increased nonce by withdrawals.length"); - assert_Snap_RemovedOperatorShares(operator, strategies, shares, "failed to remove operator shares"); - assert_Snap_RemovedStakerShares(staker, strategies, shares, "failed to remove staker shares"); + assert_AllWithdrawalsPending(withdrawalRoots, "staker's withdrawals should now be pending"); + assert_Snap_Added_QueuedWithdrawals(staker, withdrawals, "staker should have increased nonce by withdrawals.length"); + assert_Snap_Removed_OperatorShares(operator, withdrawStrats, withdrawShares, "failed to remove operator shares"); + assert_Snap_Removed_StakerShares(staker, withdrawStrats, withdrawShares, "failed to remove staker shares"); } // Fast forward to when we can complete the withdrawal @@ -87,22 +308,32 @@ contract Integration_Deposit_Delegate_Queue_Complete is IntegrationBase { /// 4. Complete withdrawal(s): // The staker will complete each withdrawal as tokens // - // ... check that the withdrawal is not pending, that the staker received the expected tokens, and that the total shares of each - // strategy withdrawn from decreased + // ... check that the staker received their tokens and that the staker/operator + // have unchanged share amounts for (uint i = 0; i < withdrawals.length; i++) { IDelegationManager.Withdrawal memory withdrawal = withdrawals[i]; uint[] memory expectedTokens = _calculateExpectedTokens(withdrawal.strategies, withdrawal.shares); IERC20[] memory tokens = staker.completeQueuedWithdrawal(withdrawal, true); - assert_withdrawalsNotPending(withdrawalRoots, "staker's withdrawals should no longer be pending"); - assert_Snap_IncreasedTokenBalances(staker, tokens, expectedTokens, "staker should have received expected tokens"); - assert_Snap_DecreasedStrategyShares(withdrawal.strategies, withdrawal.shares, "strategies should have total shares decremented"); + assert_Snap_Added_TokenBalances(staker, tokens, expectedTokens, "staker should have received expected tokens"); + assert_Snap_Unchanged_TokenBalances(operator, "operator token balances should not have changed"); + assert_Snap_Unchanged_StakerShares(staker, "staker shares should not have changed"); + assert_Snap_Unchanged_OperatorShares(operator, "operator shares should not have changed"); } } + + // Check final state: + assertEq(address(operator), delegationManager.delegatedTo(address(staker)), "staker should still be delegated to operator"); + assert_NoWithdrawalsPending(withdrawalRoots, "all withdrawals should be removed from pending"); } - function testFuzz_deposit_delegate_queue_completeAsShares(uint24 _random) public { + /// Generates a random staker and operator. The staker: + /// 1. deposits all assets into strategies + /// 2. delegates to an operator + /// 3. queues a withdrawal for a random subset of shares + /// 4. completes the queued withdrawal as shares + function testFuzz_deposit_delegate_queueRand_completeAsShares(uint24 _random) public { // When new Users are created, they will choose a random configuration from these params: _configRand({ _randomSeed: _random, @@ -136,7 +367,7 @@ contract Integration_Deposit_Delegate_Queue_Complete is IntegrationBase { staker.depositIntoEigenlayer(strategies, tokenBalances); assert_HasNoUnderlyingTokenBalance(staker, strategies, "staker should have transferred all underlying tokens"); - assert_Snap_AddedStakerShares(staker, strategies, shares, "staker should expected shares in each strategy after depositing"); + assert_Snap_Added_StakerShares(staker, strategies, shares, "staker should expected shares in each strategy after depositing"); } { @@ -148,10 +379,16 @@ contract Integration_Deposit_Delegate_Queue_Complete is IntegrationBase { assertTrue(delegationManager.isDelegated(address(staker)), "staker should be delegated"); assertEq(address(operator), delegationManager.delegatedTo(address(staker)), "staker should be delegated to operator"); - assert_HasExpectedShares(staker, strategies, shares, "staker should still have expected shares after delegating"); - assert_Snap_AddedOperatorShares(operator, strategies, shares, "operator should have received shares"); + assert_Snap_Unchanged_StakerShares(staker, "staker shares should be unchanged after delegating"); + assert_Snap_Added_OperatorShares(operator, strategies, shares, "operator should have received shares"); } + // Randomly select one or more assets to withdraw + ( + IStrategy[] memory withdrawStrats, + uint[] memory withdrawShares + ) = _randWithdrawal(strategies, shares); + IDelegationManager.Withdrawal[] memory withdrawals; bytes32[] memory withdrawalRoots; @@ -162,13 +399,14 @@ contract Integration_Deposit_Delegate_Queue_Complete is IntegrationBase { // ... check that each withdrawal was successfully enqueued, that the returned roots // match the hashes of each withdrawal, and that the staker and operator have // reduced shares. - (withdrawals, withdrawalRoots) = staker.queueWithdrawals(strategies, shares); + withdrawals = staker.queueWithdrawals(withdrawStrats, withdrawShares); + withdrawalRoots = _getWithdrawalHashes(withdrawals); - assert_AllWithdrawalsPending(withdrawalRoots, "staker's withdrawals should now be pending"); assert_ValidWithdrawalHashes(withdrawals, withdrawalRoots, "calculated withdrawals should match returned roots"); - assert_Snap_IncreasedQueuedWithdrawals(staker, withdrawals, "staker should have increased nonce by withdrawals.length"); - assert_Snap_RemovedOperatorShares(operator, strategies, shares, "failed to remove operator shares"); - assert_Snap_RemovedStakerShares(staker, strategies, shares, "failed to remove staker shares"); + assert_AllWithdrawalsPending(withdrawalRoots, "staker's withdrawals should now be pending"); + assert_Snap_Added_QueuedWithdrawals(staker, withdrawals, "staker should have increased nonce by withdrawals.length"); + assert_Snap_Removed_OperatorShares(operator, withdrawStrats, withdrawShares, "failed to remove operator shares"); + assert_Snap_Removed_StakerShares(staker, withdrawStrats, withdrawShares, "failed to remove staker shares"); } // Fast forward to when we can complete the withdrawal @@ -178,18 +416,77 @@ contract Integration_Deposit_Delegate_Queue_Complete is IntegrationBase { /// 4. Complete withdrawal(s): // The staker will complete each withdrawal as tokens // - // ... check that the withdrawal is not pending, that the withdrawer received the expected shares, and that the total shares of each - // strategy withdrawn remains unchanged + // ... check that the staker received their tokens and that the staker/operator + // have unchanged share amounts for (uint i = 0; i < withdrawals.length; i++) { IDelegationManager.Withdrawal memory withdrawal = withdrawals[i]; - // uint[] memory expectedTokens = _calculateExpectedTokens(withdrawal.strategies, withdrawal.shares); staker.completeQueuedWithdrawal(withdrawal, false); - assert_withdrawalsNotPending(withdrawalRoots, "staker's withdrawals should no longer be pending"); - assert_Snap_AddedStakerShares(staker, withdrawal.strategies, withdrawal.shares, "staker should have received expected tokens"); - assert_Snap_UnchangedStrategyShares(withdrawal.strategies, "strategies should have total shares unchanged"); + assert_Snap_Unchanged_TokenBalances(staker, "staker should not have any change in underlying token balances"); + assert_Snap_Unchanged_TokenBalances(operator, "operator should not have any change in underlying token balances"); + assert_Snap_Added_StakerShares(staker, withdrawal.strategies, withdrawal.shares, "staker should have received shares"); + assert_Snap_Added_OperatorShares(operator, withdrawal.strategies, withdrawal.shares, "operator should have received shares"); } } + + // Check final state: + assertEq(address(operator), delegationManager.delegatedTo(address(staker)), "staker should still be delegated to operator"); + assert_HasExpectedShares(staker, strategies, shares, "staker should have all original shares"); + assert_HasNoUnderlyingTokenBalance(staker, strategies, "staker not have any underlying tokens"); + assert_NoWithdrawalsPending(withdrawalRoots, "all withdrawals should be removed from pending"); + } + + /******************************************************************************* + UNHAPPY PATH TESTS + *******************************************************************************/ + + /// Generates a random staker and operator. The staker: + /// 1. deposits all assets into strategies + /// --- registers as an operator + /// 2. delegates to an operator + /// + /// ... we check that the final step fails + function testFuzz_deposit_delegate_revert_alreadyDelegated(uint24 _random) public { + _configRand({ + _randomSeed: _random, + _assetTypes: NO_ASSETS | HOLDS_LST | HOLDS_ETH | HOLDS_ALL, + _userTypes: DEFAULT | ALT_METHODS + }); + + /// 0. Create a staker and operator + ( + User staker, + IStrategy[] memory strategies, + uint[] memory tokenBalances + ) = _newRandomStaker(); + (User operator, ,) = _newRandomOperator(); + uint[] memory shares = _calculateExpectedShares(strategies, tokenBalances); + + assert_HasNoDelegatableShares(staker, "staker should not have delegatable shares before depositing"); + assertFalse(delegationManager.isDelegated(address(staker)), "staker should not be delegated"); + + { + /// 1. Deposit into strategies: + // For each of the assets held by the staker (either StrategyManager or EigenPodManager), + // the staker calls the relevant deposit function, depositing all held assets. + // + // ... check that all underlying tokens were transferred to the correct destination + // and that the staker now has the expected amount of delegated shares in each strategy + staker.depositIntoEigenlayer(strategies, tokenBalances); + + assert_HasNoUnderlyingTokenBalance(staker, strategies, "staker should have transferred all underlying tokens"); + assert_Snap_Added_StakerShares(staker, strategies, shares, "staker should expected shares in each strategy after depositing"); + } + + { + /// 2. Register the staker as an operator, then attempt to delegate to an operator. + /// This should fail as the staker is already delegated to themselves. + staker.registerAsOperator(); + assertTrue(delegationManager.isDelegated(address(staker)), "staker should be delegated"); + + cheats.expectRevert("DelegationManager._delegate: staker is already actively delegated"); + staker.delegateTo(operator); + } } } \ No newline at end of file