diff --git a/src/test/integration/IntegrationBase.t.sol b/src/test/integration/IntegrationBase.t.sol index 6d530d6595..7b2f3e3108 100644 --- a/src/test/integration/IntegrationBase.t.sol +++ b/src/test/integration/IntegrationBase.t.sol @@ -2775,7 +2775,8 @@ abstract contract IntegrationBase is IntegrationDeployer, TypeImporter { IStrategy strat = strategies[i]; if (strat == BEACONCHAIN_ETH_STRAT) { - expectedTokens[i] = shares[i]; + // We round down expected tokens to the nearest gwei + expectedTokens[i] = (shares[i] / GWEI_TO_WEI) * GWEI_TO_WEI; } else { expectedTokens[i] = strat.sharesToUnderlying(shares[i]); } diff --git a/src/test/integration/IntegrationChecks.t.sol b/src/test/integration/IntegrationChecks.t.sol index 82b27639c9..113d98ac73 100644 --- a/src/test/integration/IntegrationChecks.t.sol +++ b/src/test/integration/IntegrationChecks.t.sol @@ -16,17 +16,6 @@ contract IntegrationCheckUtils is IntegrationBase { EIGENPOD CHECKS *******************************************************************************/ - function check_VerifyWC_State( - User_M2 staker, - uint40[] memory validators, - uint64 beaconBalanceGwei - ) internal { - uint beaconBalanceWei = beaconBalanceGwei * GWEI_TO_WEI; - assert_Snap_Added_Staker_DepositShares(staker, BEACONCHAIN_ETH_STRAT, beaconBalanceWei, "staker should have added deposit shares to beacon chain strat"); - assert_Snap_Added_ActiveValidatorCount(staker, validators.length, "staker should have increased active validator count"); - assert_Snap_Added_ActiveValidators(staker, validators, "validators should each be active"); - } - function check_VerifyWC_State( User staker, uint40[] memory validators, @@ -294,6 +283,26 @@ contract IntegrationCheckUtils 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. + _check_QueuedWithdrawal_State_NotDelegated(staker, strategies, depositShares, withdrawableShares, withdrawals, withdrawalRoots); + + if (delegationManager.isDelegated(address(staker))) { + assert_Snap_Removed_OperatorShares(operator, strategies, withdrawableShares, + "check_QueuedWithdrawal_State: failed to remove operator shares"); + assert_Snap_Increased_SlashableSharesInQueue(operator, withdrawals, + "check_QueuedWithdrawal_State: failed to increase slashable shares in queue"); + check_Decreased_SlashableStake(operator, withdrawableShares, strategies); + } + } + + /// @dev Basic queued withdrawal checks if the staker is not delegated, should be called by the above function only + function _check_QueuedWithdrawal_State_NotDelegated( + User staker, + IStrategy[] memory strategies, + uint[] memory depositShares, + uint[] memory withdrawableShares, + Withdrawal[] memory withdrawals, + bytes32[] memory withdrawalRoots + ) private { assertEq(withdrawalRoots.length, 1, "check_QueuedWithdrawal_State: should only have 1 withdrawal root after queueing"); assert_AllWithdrawalsPending(withdrawalRoots, "check_QueuedWithdrawal_State: staker withdrawals should now be pending"); @@ -301,15 +310,11 @@ contract IntegrationCheckUtils is IntegrationBase { "check_QueuedWithdrawal_State: calculated withdrawals should match returned roots"); assert_Snap_Added_QueuedWithdrawals(staker, withdrawals, "check_QueuedWithdrawal_State: staker should have increased nonce by withdrawals.length"); - assert_Snap_Removed_OperatorShares(operator, strategies, withdrawableShares, - "check_QueuedWithdrawal_State: failed to remove operator shares"); assert_Snap_Removed_Staker_DepositShares(staker, strategies, depositShares, "check_QueuedWithdrawal_State: failed to remove staker shares"); assert_Snap_Removed_Staker_WithdrawableShares(staker, strategies, withdrawableShares, "check_QueuedWithdrawal_State: failed to remove staker withdrawable shares"); - assert_Snap_Increased_SlashableSharesInQueue(operator, withdrawals, - "check_QueuedWithdrawal_State: failed to increase slashable shares in queue"); - check_Decreased_SlashableStake(operator, withdrawableShares, strategies); + // Check that the dsf is either reset to wad or unchanged for (uint i = 0; i < strategies.length; i++) { // For a full withdrawal, the dsf should be reset to wad & the staker strategy list should not contain the strategy @@ -327,6 +332,8 @@ contract IntegrationCheckUtils is IntegrationBase { } } + + function check_Decreased_SlashableStake( User operator, uint[] memory withdrawableShares, diff --git a/src/test/integration/tests/FullySlashed_EigenPod.t.sol b/src/test/integration/tests/FullySlashed_EigenPod.t.sol index 3f0cb5ce19..0e5c340c10 100644 --- a/src/test/integration/tests/FullySlashed_EigenPod.t.sol +++ b/src/test/integration/tests/FullySlashed_EigenPod.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.27; import "src/test/integration/IntegrationChecks.t.sol"; -contract Integration_FullySlashedEigenpod is IntegrationCheckUtils { +contract Integration_FullySlashedEigenpod_Base is IntegrationCheckUtils { using ArrayLib for *; User staker; @@ -11,24 +11,30 @@ contract Integration_FullySlashedEigenpod is IntegrationCheckUtils { uint[] initTokenBalances; uint[] initDepositShares; uint64 slashedGwei; + uint40[] validators; - function _init() internal override { + function _init() internal virtual override { _configAssetTypes(HOLDS_ETH); (staker, strategies, initTokenBalances) = _newRandomStaker(); - cheats.assume(initTokenBalances[0] >= 64 ether); // Deposit staker - (uint40[] memory validators,) = staker.startValidators(); - beaconChain.advanceEpoch_NoRewards(); - staker.verifyWithdrawalCredentials(validators); uint[] memory shares = _calculateExpectedShares(strategies, initTokenBalances); - initDepositShares = shares; + staker.depositIntoEigenlayer(strategies, initTokenBalances); check_Deposit_State(staker, strategies, shares); + initDepositShares = shares; + validators = staker.getActiveValidators(); // Slash all validators fully slashedGwei = beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Full); beaconChain.advanceEpoch_NoRewards(); // Withdraw slashed validators to pod + } +} + +contract Integration_FullySlashedEigenpod_Checkpointed is Integration_FullySlashedEigenpod_Base { + + function _init() internal override { + super._init(); // Start & complete a checkpoint staker.startCheckpoint(); @@ -37,7 +43,7 @@ contract Integration_FullySlashedEigenpod is IntegrationCheckUtils { check_CompleteCheckpoint_FullySlashed_State(staker, validators, slashedGwei); } - function test_fullSlash_Delegate(uint24 _rand) public rand(_rand) { + function testFuzz_fullSlash_Delegate(uint24 _rand) public rand(_rand) { (User operator,,) = _newRandomOperator(); // Delegate to an operator - should succeed given that delegation only checks the operator's slashing factor @@ -45,14 +51,124 @@ contract Integration_FullySlashedEigenpod is IntegrationCheckUtils { check_Delegation_State(staker, operator, strategies, initDepositShares); } - function test_fullSlash_Revert_Redeposit(uint24 _rand) public rand(_rand) { + function testFuzz_fullSlash_Revert_Redeposit(uint24 _rand) public rand(_rand) { // Start a new validator & verify withdrawal credentials cheats.deal(address(staker), 32 ether); - (uint40[] memory newValidators, uint64 addedBeaconBalanceGwei) = staker.startValidators(); + (uint40[] memory newValidators,) = staker.startValidators(); beaconChain.advanceEpoch_NoRewards(); // We should revert on verifyWithdrawalCredentials since the staker's slashing factor is 0 cheats.expectRevert(IDelegationManagerErrors.FullySlashed.selector); staker.verifyWithdrawalCredentials(newValidators); } + + function testFuzz_fullSlash_registerStakerAsOperator_Revert_Redeposit(uint24 _rand) public rand(_rand) { + // Register staker as operator + staker.registerAsOperator(); + + // Start a new validator & verify withdrawal credentials + cheats.deal(address(staker), 32 ether); + (uint40[] memory newValidators,) = staker.startValidators(); + beaconChain.advanceEpoch_NoRewards(); + + // We should revert on verifyWithdrawalCredentials since the staker's slashing factor is 0 + cheats.expectRevert(IDelegationManagerErrors.FullySlashed.selector); + staker.verifyWithdrawalCredentials(newValidators); + } + + function testFuzz_fullSlash_registerStakerAsOperator_delegate_undelegate_completeAsShares(uint24 _rand) public rand(_rand) { + // Register staker as operator + staker.registerAsOperator(); + User operator = User(payable(address(staker))); + + // Initialize new staker + (User staker2, IStrategy[] memory strategies2, uint[] memory initTokenBalances2) = _newRandomStaker(); + uint[] memory shares = _calculateExpectedShares(strategies2, initTokenBalances2); + staker2.depositIntoEigenlayer(strategies2, initTokenBalances2); + check_Deposit_State(staker2, strategies2, shares); + + // Delegate to an operator who has now become a staker, this should succeed as slashed operator's BCSF should not affect the staker + staker2.delegateTo(operator); + check_Delegation_State(staker2, operator, strategies2, shares); + + // Register as operator and undelegate - the equivalent of redelegating to yourself + Withdrawal[] memory withdrawals = staker2.undelegate(); + bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals); + check_Undelegate_State(staker2, operator, withdrawals, withdrawalRoots, strategies2, shares); + + // Complete withdrawals as shares + _rollBlocksForCompleteWithdrawals(withdrawals); + for (uint i = 0; i < withdrawals.length; i++) { + staker2.completeWithdrawalAsShares(withdrawals[i]); + check_Withdrawal_AsShares_Undelegated_State(staker2, operator, withdrawals[i], strategies2, shares); + } + } +} + +contract Integration_FullySlashedEigenpod_NotCheckpointed is Integration_FullySlashedEigenpod_Base { + + /// @dev Adding funds prior to checkpointing allows the pod to not be "bricked" + function testFuzz_proveValidator_checkpoint_queue_completeAsTokens(uint24 _rand) public rand(_rand) { + // Deal ETH to staker + uint amount = 32 ether; + cheats.deal(address(staker), amount); + uint[] memory initTokenBalances2 = new uint[](1); + initTokenBalances2[0] = amount; + + // Deposit staker + uint[] memory shares = _calculateExpectedShares(strategies, initTokenBalances2); + staker.depositIntoEigenlayer(strategies, initTokenBalances2); + check_Deposit_State(staker, strategies, shares); + + // Checkpoint slashed EigenPod + staker.startCheckpoint(); + check_StartCheckpoint_WithPodBalance_State(staker, 0); + staker.completeCheckpoint(); + check_CompleteCheckpoint_WithSlashing_HandleRoundDown_State(staker, validators, slashedGwei); + + // Queue Full Withdrawal + uint[] memory depositShares = _getStakerDepositShares(staker, strategies); + uint[] memory withdrawableShares = _getWithdrawableShares(staker, strategies); + Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, depositShares); + bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals); + check_QueuedWithdrawal_State(staker, User(payable(address(0))), strategies, depositShares, withdrawableShares, withdrawals, withdrawalRoots); + + // Complete withdrawal as tokens + _rollBlocksForCompleteWithdrawals(withdrawals); + for (uint i = 0; i < withdrawals.length; i++) { + IERC20[] memory tokens = _getUnderlyingTokens(withdrawals[i].strategies); + uint[] memory expectedTokens = _calculateExpectedTokens(withdrawals[i].strategies, withdrawableShares); + staker.completeWithdrawalAsTokens(withdrawals[i]); + check_Withdrawal_AsTokens_State(staker, User(payable(address(0))), withdrawals[i], withdrawals[i].strategies, withdrawableShares, tokens, expectedTokens); + } + } + + function testFuzz_depositMinimumAmount_checkpoint(uint24 _rand) public rand(_rand) { + // Deal ETH to staker, minimum amount to be checkpointed + uint64 podBalanceGwei = 1; + uint amountToDeal = 1 * GWEI_TO_WEI; + bool isBricked; + + // Randomly deal 1 less than minimum amount to be checkpointed such that the pod is bricked + if (_randBool()) { + amountToDeal -= 1; + podBalanceGwei -= 1; + isBricked = true; + } + + // Send ETH to pod + cheats.prank(address(staker)); + address(staker.pod()).call{value: amountToDeal}(""); + + // Checkpoint slashed EigenPod + staker.startCheckpoint(); + check_StartCheckpoint_WithPodBalance_State(staker, podBalanceGwei); + staker.completeCheckpoint(); + if (isBricked) { + // BCSF is asserted to be zero here + check_CompleteCheckpoint_FullySlashed_State(staker, validators, slashedGwei); + } else { + check_CompleteCheckpoint_WithSlashing_HandleRoundDown_State(staker, validators, slashedGwei); + } + } } \ No newline at end of file diff --git a/src/test/integration/tests/Slashed_Eigenpod_BC.t.sol b/src/test/integration/tests/Slashed_Eigenpod_BC.t.sol index c5a8d04198..c75af4e8bb 100644 --- a/src/test/integration/tests/Slashed_Eigenpod_BC.t.sol +++ b/src/test/integration/tests/Slashed_Eigenpod_BC.t.sol @@ -16,28 +16,29 @@ contract Integration_SlashedEigenpod_BC is IntegrationCheckUtils { IStrategy[] strategies; uint[] initTokenBalances; uint64 slashedGwei; + IERC20[] tokens; + uint40[] slashedValidators; function _init() internal override { _configAssetTypes(HOLDS_ETH); (staker, strategies, initTokenBalances) = _newRandomStaker(); (operator,,) = _newRandomOperator(); (avs,) = _newRandomAVS(); - + tokens = _getUnderlyingTokens(strategies); // Should only return ETH cheats.assume(initTokenBalances[0] >= 64 ether); - //Slash on Beacon chain - (uint40[] memory validators,) = staker.startValidators(); - beaconChain.advanceEpoch_NoRewards(); - staker.verifyWithdrawalCredentials(validators); - + // Deposit staker uint[] memory shares = _calculateExpectedShares(strategies, initTokenBalances); + staker.depositIntoEigenlayer(strategies, initTokenBalances); check_Deposit_State(staker, strategies, shares); + uint40[] memory validators = staker.getActiveValidators(); - uint40[] memory slashedValidators = _choose(validators); + //Slash on Beacon chain + slashedValidators = _choose(validators); slashedGwei = beaconChain.slashValidators(slashedValidators, BeaconChainMock.SlashType.Minor); - console.log(slashedGwei); beaconChain.advanceEpoch_NoWithdrawNoRewards(); + // Checkpoint post slash staker.startCheckpoint(); staker.completeCheckpoint(); check_CompleteCheckpoint_WithSlashing_HandleRoundDown_State(staker, slashedValidators, slashedGwei); @@ -84,17 +85,12 @@ contract Integration_SlashedEigenpod_BC is IntegrationCheckUtils { } function testFuzz_delegateSlashedStaker_dsfNonWad(uint24 _random) public rand(_random) { - //Additional deposit on beacon chain so dsf is nonwad uint amount = 32 ether * _randUint({min: 1, max: 5}); cheats.deal(address(staker), amount); (uint40[] memory validators,) = staker.startValidators(); beaconChain.advanceEpoch_NoWithdrawNoRewards(); staker.verifyWithdrawalCredentials(validators); - - staker.startCheckpoint(); - staker.completeCheckpoint(); - uint256[] memory initDelegatableShares = _getWithdrawableShares(staker, strategies); uint256[] memory initDepositShares = _getStakerDepositShares(staker, strategies); @@ -211,10 +207,6 @@ contract Integration_SlashedEigenpod_BC is IntegrationCheckUtils { (uint40[] memory validators,) = staker.startValidators(); beaconChain.advanceEpoch_NoWithdrawNoRewards(); staker.verifyWithdrawalCredentials(validators); - - staker.startCheckpoint(); - staker.completeCheckpoint(); - uint256[] memory initDepositShares = _getStakerDepositShares(staker, strategies); @@ -260,9 +252,6 @@ contract Integration_SlashedEigenpod_BC is IntegrationCheckUtils { (uint40[] memory validators,) = staker.startValidators(); beaconChain.advanceEpoch_NoWithdrawNoRewards(); staker.verifyWithdrawalCredentials(validators); - - staker.startCheckpoint(); - staker.completeCheckpoint(); uint256[] memory initDepositShares = _getStakerDepositShares(staker, strategies); @@ -303,5 +292,55 @@ contract Integration_SlashedEigenpod_BC is IntegrationCheckUtils { assertEq(depositSharesAfter[0], delegatedShares[0], "Deposit shares should reset to reflect slash(es)"); assertApproxEqAbs(withdrawableSharesAfter[0], depositSharesAfter[0], 100, "Withdrawable shares should equal deposit shares after withdrawal"); } - + + function testFuzz_redeposit_queue_completeAsTokens(uint24 _random) public rand(_random){ + // Prove an additional validator + uint amount = 32 ether * _randUint({min: 1, max: 5}); + cheats.deal(address(staker), amount); + (uint40[] memory validators, uint64 addedBeaconBalanceGwei) = staker.startValidators(); + beaconChain.advanceEpoch_NoWithdrawNoRewards(); + staker.verifyWithdrawalCredentials(validators); + check_VerifyWC_State(staker, validators, addedBeaconBalanceGwei); + + // Queue withdrawal for all tokens + uint[] memory depositShares = _getStakerDepositShares(staker, strategies); + uint[] memory withdrawableShares = _getStakerWithdrawableShares(staker, strategies); + Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, depositShares); + bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals); + check_QueuedWithdrawal_State(staker, User(payable(address(0))), strategies, depositShares, withdrawableShares, withdrawals, withdrawalRoots); + + // Complete withdrawal as tokens + // Fast forward to when we can complete the withdrawal + _rollBlocksForCompleteWithdrawals(withdrawals); + for (uint256 i = 0; i < withdrawals.length; ++i) { + uint[] memory expectedTokens = _calculateExpectedTokens(withdrawals[i].strategies, withdrawableShares); + staker.completeWithdrawalAsTokens(withdrawals[i]); + check_Withdrawal_AsTokens_State(staker, operator, withdrawals[i], withdrawals[i].strategies, withdrawableShares, tokens, expectedTokens); + } + } + + function testFuzz_redeposit_queue_completeAsShares(uint24 _random) public rand(_random){ + // Prove an additional validator + uint amount = 32 ether * _randUint({min: 1, max: 5}); + cheats.deal(address(staker), amount); + (uint40[] memory validators, uint64 addedBeaconBalanceGwei) = staker.startValidators(); + beaconChain.advanceEpoch_NoWithdrawNoRewards(); + staker.verifyWithdrawalCredentials(validators); + check_VerifyWC_State(staker, validators, addedBeaconBalanceGwei); + + // Queue withdrawal for all + uint[] memory depositShares = _getStakerDepositShares(staker, strategies); + uint[] memory withdrawableShares = _getStakerWithdrawableShares(staker, strategies); + Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, depositShares); + bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals); + check_QueuedWithdrawal_State(staker, User(payable(address(0))), strategies, depositShares, withdrawableShares, withdrawals, withdrawalRoots); + + // Complete withdrawal as shares + // Fast forward to when we can complete the withdrawal + _rollBlocksForCompleteWithdrawals(withdrawals); + for (uint256 i = 0; i < withdrawals.length; ++i) { + staker.completeWithdrawalAsShares(withdrawals[i]); + check_Withdrawal_AsShares_Undelegated_State(staker, operator, withdrawals[i], withdrawals[i].strategies, withdrawableShares); + } + } } \ No newline at end of file