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
2 changes: 1 addition & 1 deletion pkg/bindings/AVSDirectory/binding.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pkg/bindings/AllocationManager/binding.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pkg/bindings/AllocationManagerView/binding.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pkg/bindings/CrossChainRegistry/binding.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pkg/bindings/DelegationManager/binding.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pkg/bindings/EigenPod/binding.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pkg/bindings/EigenPodManager/binding.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pkg/bindings/EigenStrategy/binding.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pkg/bindings/KeyRegistrar/binding.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pkg/bindings/RewardsCoordinator/binding.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pkg/bindings/SlashingLib/binding.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pkg/bindings/Snapshots/binding.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pkg/bindings/StrategyBase/binding.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pkg/bindings/StrategyBaseTVLLimits/binding.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pkg/bindings/StrategyFactory/binding.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pkg/bindings/StrategyManager/binding.go

Large diffs are not rendered by default.

45 changes: 34 additions & 11 deletions src/contracts/core/DelegationManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ contract DelegationManager is
newMaxMagnitude: newMaxMagnitude
});

uint256 scaledSharesSlashedFromQueue = _getSlashableSharesInQueue({
uint256 operatorSharesSlashedFromQueue = _getSlashableSharesInQueue({
operator: operator,
strategy: strategy,
prevMaxMagnitude: prevMaxMagnitude,
Expand All @@ -304,7 +304,7 @@ contract DelegationManager is

// Calculate the total deposit shares to slash (burn or redistribute) - slashed operator shares plus still-slashable
// shares sitting in the withdrawal queue.
totalDepositSharesToSlash = operatorSharesSlashed + scaledSharesSlashedFromQueue;
totalDepositSharesToSlash = operatorSharesSlashed + operatorSharesSlashedFromQueue;

// Remove shares from operator
_decreaseDelegation({
Expand Down Expand Up @@ -377,8 +377,8 @@ contract DelegationManager is

// forgefmt: disable-next-item
_increaseDelegation({
operator: operator,
staker: staker,
operator: operator,
staker: staker,
strategy: strategies[i],
prevDepositShares: uint256(0),
addedShares: withdrawableShares[i],
Expand Down Expand Up @@ -780,6 +780,12 @@ contract DelegationManager is
uint64 prevMaxMagnitude,
uint64 newMaxMagnitude
) internal view returns (uint256) {
// A maxMagnitude of 0 means the operator has been fully slashed (100%).
// There's nothing left to slash, so slashable shares in the queue is 0.
if (prevMaxMagnitude == 0) {
return 0;
}

// We want ALL shares added to the withdrawal queue in the window [block.number - MIN_WITHDRAWAL_DELAY_BLOCKS, block.number]
//
// To get this, we take the current shares in the withdrawal queue and subtract the number of shares
Expand All @@ -793,8 +799,25 @@ contract DelegationManager is
// less than or equal to MIN_WITHDRAWAL_DELAY_BLOCKS ago. These shares are still slashable.
uint256 scaledSharesAdded = curQueuedScaledShares - prevQueuedScaledShares;

return SlashingLib.scaleForBurning({
scaledShares: scaledSharesAdded,
// Convert scaled shares to slashed withdrawable shares.
//
// @dev Math derivation (where n is the pre-slash state):
// - depositShares (s_n): shares stored in StrategyManager/EigenPodManager
// - depositScalingFactor (k_n): staker's scaling factor, initialized to 1
// - prevMaxMagnitude (m_n): operator's previous magnitude, starts at WAD, decreases on slash
// - newMaxMagnitude (m_(n+1)): operator's new magnitude, <= prevMaxMagnitude
// - operatorShares: s_n * k_n * m_n (withdrawable shares)
// - scaledShares: s_n * k_n (stored when queueing withdrawal)
//
// We want: operatorShares slashed = s_n * k_n * m_n - s_n * k_n * m_(n+1)
//
// calcSlashedAmount computes: opShares - opShares * m_(n+1) / m_n
// = s_n * k_n * m_n - s_n * k_n * m_n * m_(n+1) / m_n
// = s_n * k_n * m_n - s_n * k_n * m_(n+1) ✓
//
// So we pass: operatorShares = scaledShares * m_n = s_n * k_n * m_n
return SlashingLib.calcSlashedAmount({
operatorShares: scaledSharesAdded.mulWad(prevMaxMagnitude),
prevMaxMagnitude: prevMaxMagnitude,
newMaxMagnitude: newMaxMagnitude
});
Expand Down Expand Up @@ -1053,11 +1076,11 @@ contract DelegationManager is
return _calculateSignableDigest(
keccak256(
abi.encode(
DELEGATION_APPROVAL_TYPEHASH,
approver,
staker,
operator,
approverSalt,
DELEGATION_APPROVAL_TYPEHASH,
approver,
staker,
operator,
approverSalt,
expiry
)
)
Expand Down
24 changes: 8 additions & 16 deletions src/contracts/libraries/SlashingLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ uint64 constant WAD = 1e18;
* There are 2 types of shares:
* 1. deposit shares
* - These can be converted to an amount of tokens given a strategy
* - by calling `sharesToUnderlying` on the strategy address (they're already tokens
* - by calling `sharesToUnderlying` on the strategy address (they're already tokens
* in the case of EigenPods)
* - These live in the storage of the EigenPodManager and individual StrategyManager strategies
* - These live in the storage of the EigenPodManager and individual StrategyManager strategies
* 2. withdrawable shares
* - For a staker, this is the amount of shares that they can withdraw
* - For an operator, the shares delegated to them are equal to the sum of their stakers'
Expand Down Expand Up @@ -77,20 +77,6 @@ library SlashingLib {
return scaledShares.mulWad(slashingFactor);
}

/**
* @notice Scales shares according to the difference in an operator's magnitude before and
* after being slashed. This is used to calculate the number of slashable shares in the
* withdrawal queue.
* NOTE: max magnitude is guaranteed to only ever decrease.
*/
function scaleForBurning(
uint256 scaledShares,
uint64 prevMaxMagnitude,
uint64 newMaxMagnitude
) internal pure returns (uint256) {
return scaledShares.mulWad(prevMaxMagnitude - newMaxMagnitude);
}

function update(
DepositScalingFactor storage dsf,
uint256 prevDepositShares,
Expand Down Expand Up @@ -179,6 +165,12 @@ library SlashingLib {
.divWad(slashingFactor);
}

/// @notice Calculates the amount of shares that should be slashed given the previous and new magnitudes.
/// @param operatorShares The amount of shares to slash.
/// @param prevMaxMagnitude The previous magnitude of the operator.
/// @param newMaxMagnitude The new magnitude of the operator.
/// @return The amount of shares that should be slashed.
/// @dev This function will revert with a divide by zero error if the previous magnitude is 0.
function calcSlashedAmount(
uint256 operatorShares,
uint256 prevMaxMagnitude,
Expand Down
27 changes: 27 additions & 0 deletions src/test/unit/DelegationUnit.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5676,6 +5676,33 @@ contract DelegationManagerUnitTests_slashingShares is DelegationManagerUnitTests
assertEq(slashableSharesInQueueAfter, 0, "slashable shares in queue should be 0 after burning");
}

/// @notice Verifies getSlashableSharesInQueue returns 0 when operator is fully slashed (maxMagnitude = 0).
/// A fully slashed operator has no remaining slashable shares.
function test_getSlashableSharesInQueue_ReturnsZero_WhenFullySlashed() public {
// Register operator and set up deposits
_registerOperatorWithBaseDetails(defaultOperator);
_setOperatorMagnitude(defaultOperator, strategyMock, WAD);

uint depositAmount = 100e18;
strategyManagerMock.addDeposit(defaultStaker, strategyMock, depositAmount);
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);

// Queue a withdrawal so there are shares in the queue
(QueuedWithdrawalParams[] memory queuedWithdrawalParams,,) =
_setUpQueueWithdrawalsSingleStrat({staker: defaultStaker, strategy: strategyMock, depositSharesToWithdraw: depositAmount});
cheats.prank(defaultStaker);
delegationManager.queueWithdrawals(queuedWithdrawalParams);

// Fully slash the operator (maxMagnitude -> 0)
_setOperatorMagnitude(defaultOperator, strategyMock, 0);
cheats.prank(address(allocationManagerMock));
delegationManager.slashOperatorShares(defaultOperator, defaultOperatorSet, defaultSlashId, strategyMock, WAD, uint64(0));

// After full slashing, there are no more slashable shares - should return 0
uint slashableShares = delegationManager.getSlashableSharesInQueue(defaultOperator, strategyMock);
assertEq(slashableShares, 0, "fully slashed operator should have 0 slashable shares in queue");
}

/// @notice Verifies that shares are NOT burnable for a withdrawal queued just before the MIN_WITHDRAWAL_DELAY_BLOCKS
function test_sharesNotBurnableWhenWithdrawalCompletable() public {
// Register operator
Expand Down