diff --git a/src/test/integration/IntegrationBase.t.sol b/src/test/integration/IntegrationBase.t.sol index 1909df2bd0..22fcabec00 100644 --- a/src/test/integration/IntegrationBase.t.sol +++ b/src/test/integration/IntegrationBase.t.sol @@ -537,6 +537,14 @@ abstract contract IntegrationBase is IntegrationDeployer, TypeImporter { assertEq(depositScalingFactors[i], WAD, err); } } + + function assert_BCSF_Zero( + User staker, + string memory err + ) internal { + uint64 curBCSF = _getBeaconChainSlashingFactor(staker); + assertEq(curBCSF, 0, err); + } /******************************************************************************* SNAPSHOT ASSERTIONS @@ -2260,7 +2268,7 @@ abstract contract IntegrationBase is IntegrationDeployer, TypeImporter { uint40[] memory validators = staker.getActiveValidators(); emit log_named_uint("slashing validators", validators.length); - deltaGwei = -int64(beaconChain.slashValidators(validators)); + deltaGwei = -int64(beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Minor)); beaconChain.advanceEpoch_NoRewards(); emit log_named_int("slashed amount", deltaGwei); diff --git a/src/test/integration/IntegrationChecks.t.sol b/src/test/integration/IntegrationChecks.t.sol index 5602e47eaa..871a28a267 100644 --- a/src/test/integration/IntegrationChecks.t.sol +++ b/src/test/integration/IntegrationChecks.t.sol @@ -111,6 +111,22 @@ contract IntegrationCheckUtils is IntegrationBase { assert_SlashableStake_Decrease_BCSlash(staker); } + function check_CompleteCheckpoint_FullySlashed_State( + User staker, + uint40[] memory slashedValidators, + uint64 slashedAmountGwei + ) internal { + check_CompleteCheckpoint_State(staker); + + assert_Snap_Unchanged_Staker_DepositShares(staker, "staker shares should not have decreased"); + assert_Snap_Removed_Staker_WithdrawableShares(staker, BEACONCHAIN_ETH_STRAT, slashedAmountGwei * GWEI_TO_WEI, "should have decreased withdrawable shares by slashed amount"); + assert_Snap_Removed_ActiveValidatorCount(staker, slashedValidators.length, "should have decreased active validator count"); + assert_Snap_Removed_ActiveValidators(staker, slashedValidators, "exited validators should each be WITHDRAWN"); + assert_BCSF_Zero(staker, "BCSF should be 0"); + assert_Snap_Unchanged_DSF(staker, BEACONCHAIN_ETH_STRAT.toArray(), "DSF should be unchanged"); + assert_SlashableStake_Decrease_BCSlash(staker); + } + function check_CompleteCheckpoint_WithSlashing_HandleRoundDown_State( User staker, uint40[] memory slashedValidators, diff --git a/src/test/integration/mocks/BeaconChainMock.t.sol b/src/test/integration/mocks/BeaconChainMock.t.sol index 4f93f54739..cc055ecd4b 100644 --- a/src/test/integration/mocks/BeaconChainMock.t.sol +++ b/src/test/integration/mocks/BeaconChainMock.t.sol @@ -53,12 +53,19 @@ contract BeaconChainMock is Logger { uint64 exitEpoch; } + /// @dev The type of slash to apply to a validator + enum SlashType { + Minor, // `MINOR_SLASH_AMOUNT_GWEI` + Half, // Half of the validator's balance + Full // The validator's entire balance + } + /// @dev All withdrawals are processed with index == 0 uint constant ZERO_NODES_LENGTH = 100; // Rewards given to each validator during epoch processing uint64 public constant CONSENSUS_REWARD_AMOUNT_GWEI = 1; - uint64 public constant SLASH_AMOUNT_GWEI = 10; + uint64 public constant MINOR_SLASH_AMOUNT_GWEI = 10; /// PROOF CONSTANTS (PROOF LENGTHS, FIELD SIZES): @@ -212,7 +219,7 @@ contract BeaconChainMock is Logger { return exitedBalanceGwei; } - function slashValidators(uint40[] memory _validators) public returns (uint64 slashedBalanceGwei) { + function slashValidators(uint40[] memory _validators, SlashType _slashType) public returns (uint64 slashedBalanceGwei) { print.method("slashValidators"); for (uint i = 0; i < _validators.length; i++) { @@ -225,19 +232,32 @@ contract BeaconChainMock is Logger { v.isSlashed = true; v.exitEpoch = currentEpoch() + 1; } - + // Calculate slash amount + uint64 slashAmountGwei; uint64 curBalanceGwei = _currentBalanceGwei(validatorIndex); - if (SLASH_AMOUNT_GWEI > curBalanceGwei) { + + if (_slashType == SlashType.Minor) { + slashAmountGwei = MINOR_SLASH_AMOUNT_GWEI; + } else if (_slashType == SlashType.Half) { + slashAmountGwei = curBalanceGwei / 2; + } else if (_slashType == SlashType.Full) { + slashAmountGwei = curBalanceGwei; + } + + // Calculate slash amount + if (slashAmountGwei > curBalanceGwei) { slashedBalanceGwei += curBalanceGwei; curBalanceGwei = 0; } else { - slashedBalanceGwei += SLASH_AMOUNT_GWEI; - curBalanceGwei -= SLASH_AMOUNT_GWEI; + slashedBalanceGwei += slashAmountGwei; + curBalanceGwei -= slashAmountGwei; } // Decrease current balance (effective balance updated during epoch processing) _setCurrentBalance(validatorIndex, curBalanceGwei); + + console.log(" - Slashed validator %s by %s gwei", validatorIndex, slashAmountGwei); } return slashedBalanceGwei; diff --git a/src/test/integration/tests/FullySlashed_EigenPod.t.sol b/src/test/integration/tests/FullySlashed_EigenPod.t.sol new file mode 100644 index 0000000000..3f0cb5ce19 --- /dev/null +++ b/src/test/integration/tests/FullySlashed_EigenPod.t.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import "src/test/integration/IntegrationChecks.t.sol"; + +contract Integration_FullySlashedEigenpod is IntegrationCheckUtils { + using ArrayLib for *; + + User staker; + IStrategy[] strategies; + uint[] initTokenBalances; + uint[] initDepositShares; + uint64 slashedGwei; + + function _init() internal 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; + check_Deposit_State(staker, strategies, shares); + + // Slash all validators fully + slashedGwei = beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Full); + beaconChain.advanceEpoch_NoRewards(); // Withdraw slashed validators to pod + + // Start & complete a checkpoint + staker.startCheckpoint(); + check_StartCheckpoint_WithPodBalance_State(staker, 0); + staker.completeCheckpoint(); + check_CompleteCheckpoint_FullySlashed_State(staker, validators, slashedGwei); + } + + function test_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 + staker.delegateTo(operator); + check_Delegation_State(staker, operator, strategies, initDepositShares); + } + + function test_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(); + beaconChain.advanceEpoch_NoRewards(); + + // We should revert on verifyWithdrawalCredentials since the staker's slashing factor is 0 + cheats.expectRevert(IDelegationManagerErrors.FullySlashed.selector); + staker.verifyWithdrawalCredentials(newValidators); + } +} \ No newline at end of file diff --git a/src/test/integration/tests/Slashed_Eigenpod.t.sol b/src/test/integration/tests/Slashed_Eigenpod.t.sol index af338d948b..27de5db11a 100644 --- a/src/test/integration/tests/Slashed_Eigenpod.t.sol +++ b/src/test/integration/tests/Slashed_Eigenpod.t.sol @@ -34,7 +34,7 @@ contract Integration_SlashedEigenpod is IntegrationCheckUtils { check_Deposit_State(staker, strategies, shares); uint40[] memory slashedValidators = _choose(validators); - slashedGwei = beaconChain.slashValidators(slashedValidators); + slashedGwei = beaconChain.slashValidators(slashedValidators, BeaconChainMock.SlashType.Minor); console.log(slashedGwei); beaconChain.advanceEpoch_NoWithdrawNoRewards(); diff --git a/src/test/integration/tests/eigenpod/VerifyWC_StartCP_CompleteCP.t.sol b/src/test/integration/tests/eigenpod/VerifyWC_StartCP_CompleteCP.t.sol index e72a39a0cb..fe9c273817 100644 --- a/src/test/integration/tests/eigenpod/VerifyWC_StartCP_CompleteCP.t.sol +++ b/src/test/integration/tests/eigenpod/VerifyWC_StartCP_CompleteCP.t.sol @@ -293,7 +293,7 @@ contract Integration_VerifyWC_StartCP_CompleteCP is IntegrationCheckUtils { (User staker, ,) = _newRandomStaker(); (uint40[] memory validators, ) = staker.startValidators(); - beaconChain.slashValidators(validators); + beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Minor); // Advance epoch, withdrawing slashed validators to pod beaconChain.advanceEpoch_NoRewards(); @@ -316,7 +316,7 @@ contract Integration_VerifyWC_StartCP_CompleteCP is IntegrationCheckUtils { staker.verifyWithdrawalCredentials(validators); check_VerifyWC_State(staker, validators, beaconBalanceGwei); - uint64 slashedBalanceGwei = beaconChain.slashValidators(validators); + uint64 slashedBalanceGwei = beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Minor); beaconChain.advanceEpoch_NoRewards(); staker.startCheckpoint(); @@ -348,7 +348,7 @@ contract Integration_VerifyWC_StartCP_CompleteCP is IntegrationCheckUtils { staker.startCheckpoint(); check_StartCheckpoint_State(staker); - uint64 slashedBalanceGwei = beaconChain.slashValidators(validators); + uint64 slashedBalanceGwei = beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Minor); beaconChain.advanceEpoch_NoRewards(); staker.completeCheckpoint(); @@ -377,7 +377,7 @@ contract Integration_VerifyWC_StartCP_CompleteCP is IntegrationCheckUtils { check_VerifyWC_State(staker, validators, beaconBalanceGwei); // Slash validators - uint64 slashedBalanceGwei = beaconChain.slashValidators(validators); + uint64 slashedBalanceGwei = beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Minor); beaconChain.advanceEpoch_NoRewards(); // Start a checkpoint @@ -414,7 +414,7 @@ contract Integration_VerifyWC_StartCP_CompleteCP is IntegrationCheckUtils { staker.verifyWithdrawalCredentials(validators); check_VerifyWC_State(staker, validators, beaconBalanceGwei); - uint64 slashedBalanceGwei = beaconChain.slashValidators(validators); + uint64 slashedBalanceGwei = beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Minor); beaconChain.advanceEpoch_NoRewards(); staker.verifyStaleBalance(validators[0]); @@ -440,7 +440,7 @@ contract Integration_VerifyWC_StartCP_CompleteCP is IntegrationCheckUtils { check_VerifyWC_State(staker, validators, beaconBalanceGwei); // Slash validators but do not process exits to pod - uint64 slashedBalanceGwei = beaconChain.slashValidators(validators); + uint64 slashedBalanceGwei = beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Minor); beaconChain.advanceEpoch_NoWithdraw(); staker.verifyStaleBalance(validators[0]); @@ -450,7 +450,7 @@ contract Integration_VerifyWC_StartCP_CompleteCP is IntegrationCheckUtils { check_CompleteCheckpoint_WithCLSlashing_State(staker, slashedBalanceGwei); // Slash validators again but do not process exits to pod - uint64 secondSlashedBalanceGwei = beaconChain.slashValidators(validators); + uint64 secondSlashedBalanceGwei = beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Minor); beaconChain.advanceEpoch_NoWithdraw(); staker.verifyStaleBalance(validators[0]); @@ -482,7 +482,7 @@ contract Integration_VerifyWC_StartCP_CompleteCP is IntegrationCheckUtils { staker.startCheckpoint(); check_StartCheckpoint_State(staker); - uint64 slashedBalanceGwei = beaconChain.slashValidators(validators); + uint64 slashedBalanceGwei = beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Minor); beaconChain.advanceEpoch_NoRewards(); staker.completeCheckpoint(); diff --git a/src/test/unit/EigenPodUnit.t.sol b/src/test/unit/EigenPodUnit.t.sol index eb098cfa94..63f2a04041 100644 --- a/src/test/unit/EigenPodUnit.t.sol +++ b/src/test/unit/EigenPodUnit.t.sol @@ -1568,7 +1568,7 @@ contract EigenPodUnitTests_verifyStaleBalance is EigenPodUnitTests { staker.verifyWithdrawalCredentials(validators); // Slash validators and advance epoch - beaconChain.slashValidators(validators); + beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Minor); beaconChain.advanceEpoch(); StaleBalanceProofs memory proofs = beaconChain.getStaleBalanceProofs(validator); @@ -1597,7 +1597,7 @@ contract EigenPodUnitTests_verifyStaleBalance is EigenPodUnitTests { staker.verifyWithdrawalCredentials(validators); // Slash validators and advance epoch - beaconChain.slashValidators(validators); + beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Minor); beaconChain.advanceEpoch(); StaleBalanceProofs memory proofs = beaconChain.getStaleBalanceProofs(validator); @@ -1627,7 +1627,7 @@ contract EigenPodUnitTests_verifyStaleBalance is EigenPodUnitTests { staker.verifyWithdrawalCredentials(validators); // Slash validators and advance epoch - beaconChain.slashValidators(validators); + beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Minor); beaconChain.advanceEpoch(); StaleBalanceProofs memory proofs = beaconChain.getStaleBalanceProofs(validator); @@ -1673,7 +1673,7 @@ contract EigenPodUnitTests_verifyStaleBalance is EigenPodUnitTests { staker.verifyWithdrawalCredentials(validators); // Slash validators and advance epoch - beaconChain.slashValidators(validators); + beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Minor); beaconChain.advanceEpoch(); StaleBalanceProofs memory proofs = beaconChain.getStaleBalanceProofs(validator); @@ -1700,7 +1700,7 @@ contract EigenPodUnitTests_verifyStaleBalance is EigenPodUnitTests { staker.verifyWithdrawalCredentials(validators); // Slash validators and advance epoch - beaconChain.slashValidators(validators); + beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Minor); beaconChain.advanceEpoch(); StaleBalanceProofs memory proofs = beaconChain.getStaleBalanceProofs(validator);