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
7 changes: 7 additions & 0 deletions src/test/harnesses/EigenPodManagerWrapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,11 @@ contract EigenPodManagerWrapper is EigenPodManager {
function setPodOwnerShares(address owner, int256 shares) external {
podOwnerDepositShares[owner] = shares;
}

function setBeaconChainSlashingFactor(address podOwner, uint64 slashingFactor) external {
_beaconChainSlashingFactor[podOwner] = BeaconChainSlashingFactor({
slashingFactor: slashingFactor,
isSet: true
});
}
}
49 changes: 29 additions & 20 deletions src/test/integration/IntegrationBase.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -930,24 +930,11 @@ abstract contract IntegrationBase is IntegrationDeployer, TypeImporter {
IStrategy[] memory strategies,
string memory err
) internal {
uint[] memory curSlashableStake = _getMinSlashableStake(operator, operatorSet, strategies);
uint[] memory prevSlashableStake = _getPrevMinSlashableStake(operator, operatorSet, strategies);
uint[] memory curSlashableStake = _getMinSlashableStake(address(operator), operatorSet, strategies);
uint[] memory prevSlashableStake = _getPrevMinSlashableStake(address(operator), operatorSet, strategies);

for (uint i = 0; i < strategies.length; i++) {
assertTrue(prevSlashableStake[i] > curSlashableStake[i], err);
}
}

function assert_Snap_StakeBecomeUnslashable(
address operator,
OperatorSet memory operatorSet,
IStrategy[] memory strategies,
string memory err
) internal {
uint[] memory curSlashableStake = _getMinSlashableStake(operator, operatorSet, strategies);
uint[] memory prevSlashableStake = _getPrevMinSlashableStake(operator, operatorSet, strategies);

for (uint i = 0; i < strategies.length; i++) {
assertTrue(prevSlashableStake[i] > curSlashableStake[i], err);
}
}
Expand Down Expand Up @@ -1870,9 +1857,10 @@ abstract contract IntegrationBase is IntegrationDeployer, TypeImporter {
// Use timewarp to get previous staker shares
uint[] memory prevShares = _getPrevStakerWithdrawableShares(staker, strategies);

// For each strategy, check diff between (prev-removed) and curr is at most 1 gwei
// Assert that the decrease in withdrawable shares is at least as much as the removed shares
// Checking for expected rounding down behavior
for (uint i = 0; i < strategies.length; i++) {
assertApproxEqAbs(prevShares[i] - removedShares[i], curShares[i], 1e9, err);
assertGe(prevShares[i] - curShares[i], removedShares[i], err);
}
}

Expand Down Expand Up @@ -2010,7 +1998,11 @@ abstract contract IntegrationBase is IntegrationDeployer, TypeImporter {
// If there was a slashing, but we complete a withdrawal for 0 shares, no need to normalize
if (curSlashingFactor == WAD || curDepositShares[i] == 0) {
assert_Snap_Unchanged_DSF(staker, stratArray, err);
assert_DSF_WAD(staker, stratArray, err);
assert_DSF_WAD(staker, stratArray, err);
}
// If the staker has a slashingFactor of 0, any withdrawal as shares won't change the DSF
else if (staker.getSlashingFactor(strategies[i]) == 0) {
assert_Snap_Unchanged_DSF(staker, stratArray, err);
}
// If there was a slashing and we complete a withdrawal for non-zero shares, normalize the DSF
else {
Expand All @@ -2030,7 +2022,8 @@ abstract contract IntegrationBase is IntegrationDeployer, TypeImporter {
uint[] memory delegatableShares,
string memory err
) internal {
uint64[] memory maxMags = _getMaxMagnitudes(operator, strategies);
address operator = delegationManager.delegatedTo(address(staker));
uint64[] memory maxMags = _getMaxMagnitudes(User(payable(operator)), strategies);

for (uint i = 0; i < strategies.length; i++) {
IStrategy[] memory stratArray = strategies[i].toArray();
Expand Down Expand Up @@ -2104,7 +2097,7 @@ abstract contract IntegrationBase is IntegrationDeployer, TypeImporter {
(OperatorSet[] memory operatorSets, Allocation[] memory allocations) = _getStrategyAllocations(operator, BEACONCHAIN_ETH_STRAT);
for (uint i = 0; i < operatorSets.length; i++) {
if (allocations[i].currentMagnitude > 0) {
assert_Snap_StakeBecomeUnslashable(operator, operatorSets[i], BEACONCHAIN_ETH_STRAT.toArray(), "operator should have minSlashableStake decreased");
assert_Snap_StakeBecomeUnslashable(User(payable(operator)), operatorSets[i], BEACONCHAIN_ETH_STRAT.toArray(), "operator should have minSlashableStake decreased");
}
}
}
Expand Down Expand Up @@ -2574,6 +2567,22 @@ abstract contract IntegrationBase is IntegrationDeployer, TypeImporter {
}
}

function _genSlashing_Custom(
User operator,
OperatorSet memory operatorSet,
uint wadsToSlash
) internal view returns (SlashingParams memory params) {
params.operator = address(operator);
params.operatorSetId = operatorSet.id;
params.description = "_genSlashing_Custom";
params.strategies = allocationManager.getStrategiesInOperatorSet(operatorSet).sort();
params.wadsToSlash = new uint[](params.strategies.length);

for (uint i = 0; i < params.wadsToSlash.length; i++) {
params.wadsToSlash[i] = wadsToSlash;
}
}

function _randWadToSlash() internal returns (uint) {
return _randUint({ min: 0.01 ether, max: 1 ether });
}
Expand Down
76 changes: 74 additions & 2 deletions src/test/integration/IntegrationChecks.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,6 @@ contract IntegrationCheckUtils is IntegrationBase {
uint64 slashedAmountGwei
) internal {
check_CompleteCheckpoint_State(staker);

assert_Snap_Unchanged_Staker_DepositShares(staker, "staker shares should not have decreased");
assert_Snap_Removed_Staker_WithdrawableShares_AtLeast(staker, BEACONCHAIN_ETH_STRAT, slashedAmountGwei * GWEI_TO_WEI, "should have decreased withdrawable shares by at least slashed amount");
assert_Snap_Unchanged_ActiveValidatorCount(staker, "should not have changed active validator count");
Expand All @@ -160,6 +159,20 @@ contract IntegrationCheckUtils is IntegrationBase {
assert_SlashableStake_Decrease_BCSlash(staker);
}

/// @notice Used for edge cases where rounding behaviors of magnitudes close to 1 are tested.
/// Normal
function check_CompleteCheckPoint_WithSlashing_LowMagnitude_State(
User staker,
uint64 slashedAmountGwei
) internal {
check_CompleteCheckpoint_State(staker);
assert_Snap_Unchanged_Staker_DepositShares(staker, "staker shares should not have decreased");
assert_Snap_Removed_Staker_WithdrawableShares_AtLeast(staker, BEACONCHAIN_ETH_STRAT, slashedAmountGwei * GWEI_TO_WEI, "should have decreased withdrawable shares by at least slashed amount");
assert_Snap_Unchanged_ActiveValidatorCount(staker, "should not have changed active validator count");
assert_Snap_BCSF_Decreased(staker, "BCSF should decrease");
assert_Snap_Unchanged_DSF(staker, BEACONCHAIN_ETH_STRAT.toArray(), "DSF should be unchanged");
}

function check_CompleteCheckpoint_WithCLSlashing_State(
User staker,
uint64 slashedAmountGwei
Expand Down Expand Up @@ -234,7 +247,31 @@ contract IntegrationCheckUtils is IntegrationBase {
} else {
assert_Snap_Added_Staker_WithdrawableShares(staker, strategies, shares, "deposit should increase withdrawable shares");
}
assert_Snap_DSF_State_Deposit(staker, strategies, "staker's DSF not updated correctly"); }
assert_Snap_DSF_State_Deposit(staker, strategies, "staker's DSF not updated correctly");
}

/// @notice Used for edge cases where rounding behaviors impact the assertions after a deposit, specifically when
/// there already exists depositShares for a staker for a strategy.
function check_Deposit_State_SubsequentDeposit_WithRounding(User staker, IStrategy[] memory strategies, uint[] memory shares) internal {
/// 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
assert_HasNoUnderlyingTokenBalance(staker, strategies, "staker should have transferred all underlying tokens");
assert_DepositShares_GTE_WithdrawableShares(staker, strategies, "deposit shares should be greater than or equal to withdrawable shares");
assert_Snap_Added_Staker_DepositShares(staker, strategies, shares, "staker should expect shares in each strategy after depositing");
assert_StrategiesInStakerStrategyList(staker, strategies, "staker strategy list should contain all strategies");

if (delegationManager.isDelegated(address(staker))) {
User operator = User(payable(delegationManager.delegatedTo(address(staker))));
assert_Snap_Expected_Staker_WithdrawableShares_Deposit(staker, operator, strategies, shares, "staker should have received expected withdrawable shares");
} else {
assert_Snap_Added_Staker_WithdrawableShares(staker, strategies, shares, "deposit should increase withdrawable shares");
}
assert_Snap_WithinErrorBounds_DSF(staker, strategies, "staker's DSF not updated correctly");
}

function check_Delegation_State(
User staker,
Expand Down Expand Up @@ -881,6 +918,24 @@ contract IntegrationCheckUtils is IntegrationBase {
check_IncrAlloc_State_Slashable_Active(operator, params);
}

/// @dev Invariants for modifyAllocations. Use when:
/// - operator IS slashable for this operator set
/// - last call to modifyAllocations created an INCREASE in allocation
/// - operator has no delegated shares/stake so their slashable stake remains UNCHANGED
function check_IncrAlloc_State_Slashable_NoDelegatedStake(
User operator,
AllocateParams memory params
) internal {
check_Base_IncrAlloc_State(operator, params);
check_IsSlashable_State(operator, params.operatorSet, params.strategies);

/// Run checks on pending allocation, if the operator has a nonzero delay
check_IncrAlloc_State_Slashable_Pending(operator, params);

// Validate operator has no pending modification and has increased allocation
check_IncrAlloc_State_Slashable_Active_NoDelegatedStake(operator, params);
}

/// @dev Invariants for modifyAllocations. Used when:
/// - operator IS slashable for this operator set
/// - last call to modifyAllocations created an INCREASE in allocation
Expand Down Expand Up @@ -919,6 +974,23 @@ contract IntegrationCheckUtils is IntegrationBase {
assert_Snap_StakeBecameSlashable(operator, params.operatorSet, params.strategies, "slashable stake should have increased");
}

/// @dev Invariants for modifyAllocations. Used when:
/// - operator IS slashable for this operator set
/// - last call to modifyAllocations created an INCREASE in allocation
/// - effectBlock for the increase HAS been reached
function check_IncrAlloc_State_Slashable_Active_NoDelegatedStake(
User operator,
AllocateParams memory params
) private activateAllocation(operator) {
// Validate operator does not have a pending modification, and has expected slashable stake
check_ActiveModification_State(operator, params);

// SHOULD set current magnitude and increase slashable/allocated stake
assert_Snap_Set_CurrentMagnitude(operator, params, "should have updated the operator's magnitude");
assert_HasAllocatedStake(operator, params, "operator should have expected allocated stake for each strategy");
assert_HasSlashableStake(operator, params, "operator should have expected slashable stake for each strategy");
}

/*******************************************************************************
ALM - DECREASE ALLOCATION
*******************************************************************************/
Expand Down
14 changes: 14 additions & 0 deletions src/test/integration/IntegrationDeployer.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,20 @@ abstract contract IntegrationDeployer is ExistingDeploymentParser {
return tokenBalances;
}

/// Given an array of strategies and an array of amounts, deal the amounts to the user
function _dealAmounts(User user, IStrategy[] memory strategies, uint[] memory amounts) internal noTracing {
for (uint i = 0; i < amounts.length; i++) {
IStrategy strategy = strategies[i];

if (strategy == BEACONCHAIN_ETH_STRAT) {
cheats.deal(address(user), amounts[i]);
} else {
IERC20 underlyingToken = strategy.underlyingToken();
StdCheats.deal(address(underlyingToken), address(user), amounts[i]);
}
}
}

/// @dev Uses `random` to return a random uint, with a range given by `min` and `max` (inclusive)
/// @return `min` <= result <= `max`
function _randUint(uint min, uint max) internal returns (uint) {
Expand Down
35 changes: 35 additions & 0 deletions src/test/integration/mocks/BeaconChainMock.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,41 @@ contract BeaconChainMock is Logger {
return slashedBalanceGwei;
}

function slashValidators(uint40[] memory _validators, uint64 _slashAmountGwei) public {
print.method("slashValidatorsAmountGwei");

for (uint i = 0; i < _validators.length; i++) {
uint40 validatorIndex = _validators[i];
Validator storage v = validators[validatorIndex];
require(!v.isDummy, "BeaconChainMock: attempting to exit dummy validator. We need those for proofgen >:(");

// Mark slashed and initiate validator exit
if (!v.isSlashed) {
v.isSlashed = true;
v.exitEpoch = currentEpoch() + 1;
}

// Calculate slash amount
uint64 curBalanceGwei = _currentBalanceGwei(validatorIndex);

// Calculate slash amount
uint64 slashedAmountGwei;
if (_slashAmountGwei > curBalanceGwei) {
slashedAmountGwei = curBalanceGwei;
_slashAmountGwei -= curBalanceGwei;
curBalanceGwei = 0;
} else {
slashedAmountGwei = _slashAmountGwei;
curBalanceGwei -= _slashAmountGwei;
}

// Decrease current balance (effective balance updated during epoch processing)
_setCurrentBalance(validatorIndex, curBalanceGwei);

console.log(" - Slashed validator %s by %s gwei", validatorIndex, slashedAmountGwei);
}
}

/// @dev Move forward one epoch on the beacon chain, taking care of important epoch processing:
/// - Award ALL validators CONSENSUS_REWARD_AMOUNT
/// - Withdraw any balance over 32 ETH
Expand Down
2 changes: 1 addition & 1 deletion src/test/integration/tests/FullySlashed_EigenPod.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ contract Integration_FullySlashedEigenpod_NotCheckpointed is Integration_FullySl
// BCSF is asserted to be zero here
check_CompleteCheckpoint_FullySlashed_State(staker, validators, slashedGwei);
} else {
check_CompleteCheckpoint_WithSlashing_HandleRoundDown_State(staker, validators, slashedGwei);
check_CompleteCheckpoint_WithSlashing_HandleRoundDown_State(staker, validators, slashedGwei - podBalanceGwei);
}
}
}
83 changes: 83 additions & 0 deletions src/test/integration/tests/HighDSF_Multiple_Deposits.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;

import "src/test/integration/IntegrationChecks.t.sol";

/// @notice Testing the rounding behavior when the DSF is high and there are multiple deposits
contract Integration_HighDSF_Multiple_Deposits is IntegrationCheckUtils {
using ArrayLib for *;

AVS avs;
OperatorSet operatorSet;

User operator;
AllocateParams allocateParams;
SlashingParams slashParams;

User staker;
IStrategy[] strategies;
IERC20[] tokens; // underlying token for each strategy
uint[] initTokenBalances;
uint[] initDepositShares;

/**
* Shared setup:
* 1. create a new staker, operator, and avs
* 2. create an operator set and register an operator, allocate all magnitude to the operator set
* 3. slash operator to 1 magnitude remaining
* 4. delegate to operator
*/
function _init() internal override {
// 1. create a new staker, operator, and avs
_configAssetTypes(HOLDS_LST);
(staker, strategies, initTokenBalances) = _newRandomStaker();
(operator,,) = _newRandomOperator();
(avs,) = _newRandomAVS();

// 2. Create an operator set and register an operator, allocate all magnitude to the operator set
operatorSet = avs.createOperatorSet(strategies);
operator.registerForOperatorSet(operatorSet);
check_Registration_State_NoAllocation(operator, operatorSet, strategies);
allocateParams = _genAllocation_AllAvailable(operator, operatorSet, strategies);
operator.modifyAllocations(allocateParams);
check_IncrAlloc_State_Slashable_NoDelegatedStake(operator, allocateParams);
_rollBlocksForCompleteAllocation(operator, operatorSet, strategies);

// 3. slash operator to 1 magnitude remaining
SlashingParams memory slashParams = _genSlashing_Custom(operator, operatorSet, WAD - 1);
avs.slashOperator(slashParams);
check_Base_Slashing_State(operator, allocateParams, slashParams);

// 4. delegate to operator
staker.delegateTo(operator);

uint256 slashingFactor = staker.getSlashingFactor(strategies[0]);
assertEq(slashingFactor, 1, "slashing factor should be 1");
}

/// @notice Test setup with a staker with slashingFactor of 1 (maxMagnitude = 1)
/// with repeat deposits to increase the DSF. Limiting number of fuzzed runs to speed up tests since this
/// for loops several times.
/// forge-config: default.fuzz.runs = 10
function test_multiple_deposits(uint24 _r) public rand(_r) {
// deposit initial assets into strategies
staker.depositIntoEigenlayer(strategies, initTokenBalances);
initDepositShares = _calculateExpectedShares(strategies, initTokenBalances);
check_Deposit_State(staker, strategies, initDepositShares);

// Repeat the deposit 50 times
for (uint i = 0; i < 50; i++) {
_dealAmounts(staker, strategies, initTokenBalances);
staker.depositIntoEigenlayer(strategies, initTokenBalances);
initDepositShares = _calculateExpectedShares(strategies, initTokenBalances);
check_Deposit_State_SubsequentDeposit_WithRounding(staker, strategies, initDepositShares);
}

// Check that the DSF is still bounded without overflow
for (uint i = 0; i < strategies.length; i++) {
assertGe(delegationManager.depositScalingFactor(address(staker), strategies[i]), WAD, "DSF should be >= WAD");
// theoretical upper bound on DSF is 1e74
assertLt(delegationManager.depositScalingFactor(address(staker), strategies[i]), 1e74, "DSF should be < 1e74");
}
}
}
Loading
Loading