Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/test/integration/IntegrationBase.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
}
Expand Down
39 changes: 23 additions & 16 deletions src/test/integration/IntegrationChecks.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -294,22 +283,38 @@ 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");
assert_ValidWithdrawalHashes(withdrawals, withdrawalRoots,
"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
Expand All @@ -327,6 +332,8 @@ contract IntegrationCheckUtils is IntegrationBase {
}
}



function check_Decreased_SlashableStake(
User operator,
uint[] memory withdrawableShares,
Expand Down
136 changes: 126 additions & 10 deletions src/test/integration/tests/FullySlashed_EigenPod.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,38 @@ 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;
IStrategy[] strategies;
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();
Expand All @@ -37,22 +43,132 @@ 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
staker.delegateTo(operator);
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);
}
}
}
81 changes: 60 additions & 21 deletions src/test/integration/tests/Slashed_Eigenpod_BC.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);
}
}
}
Loading