Skip to content

Commit

Permalink
feat: track staker withdrawals
Browse files Browse the repository at this point in the history
  • Loading branch information
0xClandestine committed Oct 30, 2024
1 parent 4b4298d commit 6b25887
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 22 deletions.
61 changes: 56 additions & 5 deletions src/contracts/core/DelegationManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ contract DelegationManager is
SignatureUtils
{
using SlashingLib for *;
using EnumerableSet for EnumerableSet.Bytes32Set;

// @notice Simple permission for functions that are only callable by the StrategyManager contract OR by the EigenPodManagerContract
modifier onlyStrategyManagerOrEigenPodManager() {
Expand Down Expand Up @@ -275,6 +276,8 @@ contract DelegationManager is
sharesToWithdraw: params[i].shares,
maxMagnitudes: maxMagnitudes
});

pendingWithdrawals[withdrawalRoots[i]] = true;
}

return withdrawalRoots;
Expand All @@ -295,11 +298,26 @@ contract DelegationManager is
IERC20[][] calldata tokens,
bool[] calldata receiveAsTokens
) external onlyWhenNotPaused(PAUSED_EXIT_WITHDRAWAL_QUEUE) nonReentrant {
for (uint256 i = 0; i < withdrawals.length; ++i) {
uint256 n = withdrawals.length;
for (uint256 i; i < n; ++i) {
_completeQueuedWithdrawal(withdrawals[i], tokens[i], receiveAsTokens[i]);
}
}

/// @inheritdoc IDelegationManager
function completeQueuedWithdrawals(
IERC20[][] calldata tokens,
bool[] calldata receiveAsTokens,
uint256 numToComplete
) external onlyWhenNotPaused(PAUSED_EXIT_WITHDRAWAL_QUEUE) nonReentrant {
EnumerableSet.Bytes32Set storage withdrawalRoots = _stakerQueuedWithdrawalRoots[msg.sender];
uint256 totalQueued = withdrawalRoots.length();
numToComplete = numToComplete > totalQueued ? totalQueued : numToComplete;
for (uint256 i; i < numToComplete; ++i) {
_completeQueuedWithdrawal(queuedWithdrawals[withdrawalRoots.at(i)], tokens[i], receiveAsTokens[i]);
}
}

/// @inheritdoc IDelegationManager
function increaseDelegatedShares(
address staker,
Expand Down Expand Up @@ -494,7 +512,7 @@ contract DelegationManager is
* and added back to the operator's delegatedShares.
*/
function _completeQueuedWithdrawal(
Withdrawal calldata withdrawal,
Withdrawal memory withdrawal,
IERC20[] calldata tokens,
bool receiveAsTokens
) internal {
Expand Down Expand Up @@ -541,8 +559,12 @@ contract DelegationManager is
}
}

// Remove `withdrawalRoot` from pending roots
_stakerQueuedWithdrawalRoots[withdrawal.staker].remove(withdrawalRoot);

delete queuedWithdrawals[withdrawalRoot];

delete pendingWithdrawals[withdrawalRoot];

emit SlashingWithdrawalCompleted(withdrawalRoot);
}

Expand Down Expand Up @@ -678,10 +700,12 @@ contract DelegationManager is

bytes32 withdrawalRoot = calculateWithdrawalRoot(withdrawal);

// Place withdrawal in queue
pendingWithdrawals[withdrawalRoot] = true;
_stakerQueuedWithdrawalRoots[staker].add(withdrawalRoot);

queuedWithdrawals[withdrawalRoot] = withdrawal;

emit SlashingWithdrawalQueued(withdrawalRoot, withdrawal, sharesToWithdraw);

return withdrawalRoot;
}

Expand Down Expand Up @@ -824,6 +848,33 @@ contract DelegationManager is

return (strategies, shares);
}

/// @inheritdoc IDelegationManager
function getQueuedWithdrawals(
address staker
) external view returns (Withdrawal[] memory withdrawals, uint256[][] memory shares) {
bytes32[] memory withdrawalRoots = _stakerQueuedWithdrawalRoots[staker].values();
uint256 totalQueued = withdrawalRoots.length;

withdrawals = new Withdrawal[](totalQueued);
shares = new uint256[][](totalQueued);

address operator = delegatedTo[staker];

for (uint256 i; i < totalQueued; ++i) {
withdrawals[i] = queuedWithdrawals[withdrawalRoots[i]];

uint64[] memory operatorMagnitudes =
allocationManager.getMaxMagnitudes(operator, withdrawals[i].strategies);

for (uint256 j; j < withdrawals[i].strategies.length; ++j) {
StakerScalingFactors memory ssf = stakerScalingFactor[staker][withdrawals[i].strategies[j]];

shares[i][j] =
withdrawals[i].scaledShares[j].scaleSharesForCompleteWithdrawal(ssf, operatorMagnitudes[i]);
}
}
}

/// @inheritdoc IDelegationManager
function calculateWithdrawalRoot(
Expand Down
16 changes: 14 additions & 2 deletions src/contracts/core/DelegationManagerStorage.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;

import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";

import "../libraries/SlashingLib.sol";
import "../interfaces/IDelegationManager.sol";
import "../interfaces/IAVSDirectory.sol";
Expand Down Expand Up @@ -86,7 +88,8 @@ abstract contract DelegationManagerStorage is IDelegationManager {
/// @dev Do not remove, deprecated storage.
uint256 private __deprecated_minWithdrawalDelayBlocks;

/// @notice Returns whether a given `withdrawalRoot` has a pending withdrawal.
/// @dev Returns whether a withdrawal is pending for a given `withdrawalRoot`.
/// @dev This variable will be deprecated in the future, values should only be read or deleted.
mapping(bytes32 withdrawalRoot => bool pending) public pendingWithdrawals;

/// @notice Returns the total number of withdrawals that have been queued for a given `staker`.
Expand All @@ -104,6 +107,15 @@ abstract contract DelegationManagerStorage is IDelegationManager {
/// @dev We do not need the `beaconChainScalingFactor` for non-beaconchain strategies, but it's nicer syntactically to keep it.
mapping(address staker => mapping(IStrategy strategy => StakerScalingFactors)) public stakerScalingFactor;

/// @notice Returns a list of queued withdrawals for a given `staker`.
/// @dev Entrys are removed when the withdrawal is completed.
/// @dev This variable only reflects withdrawals that were made after the slashing release.
mapping(address staker => EnumerableSet.Bytes32Set withdrawalRoots) internal _stakerQueuedWithdrawalRoots;

/// @notice Returns the details of a queued withdrawal for a given `staker` and `withdrawalRoot`.
/// @dev This variable only reflects withdrawals that were made after the slashing release.
mapping(bytes32 withdrawalRoot => Withdrawal withdrawal) public queuedWithdrawals;

// Construction

constructor(
Expand All @@ -125,5 +137,5 @@ abstract contract DelegationManagerStorage is IDelegationManager {
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[38] private __gap;
uint256[36] private __gap;
}
29 changes: 23 additions & 6 deletions src/contracts/interfaces/IDelegationManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -326,10 +326,22 @@ interface IDelegationManager is ISignatureUtils, IDelegationManagerErrors, IDele
) external returns (bytes32[] memory);

/**
* @notice Used to complete the specified `withdrawal`. The caller must match `withdrawal.withdrawer`
* Withdrawals remain slashable during the withdrawal delay period and the actual withdrawn shares are calculated
* based off the scaledShares.
* @param withdrawal The Withdrawal to complete.
* @notice Used to complete the all queued withdrawals.
* Used to complete the specified `withdrawals`. The function caller must match `withdrawals[...].withdrawer`
* @param tokens Array of tokens for each Withdrawal. See `completeQueuedWithdrawal` for the usage of a single array.
* @param receiveAsTokens Whether or not to complete each withdrawal as tokens. See `completeQueuedWithdrawal` for the usage of a single boolean.
* @param numToComplete The number of withdrawals to complete. This must be less than or equal to the number of queued withdrawals.
* @dev See `completeQueuedWithdrawal` for relevant dev tags
*/
function completeQueuedWithdrawals(
IERC20[][] calldata tokens,
bool[] calldata receiveAsTokens,
uint256 numToComplete
) external;

/**
* @notice Used to complete the lastest queued withdrawal.
* @param withdrawal The withdrawal to complete.
* @param tokens Array in which the i-th entry specifies the `token` input to the 'withdraw' function of the i-th Strategy in the `withdrawal.strategies` array.
* @param receiveAsTokens If true, the shares calculated to be withdrawn will be withdrawn from the specified strategies themselves
* and sent to the caller, through calls to `withdrawal.strategies[i].withdraw`. If false, then the shares in the specified strategies
Expand All @@ -345,9 +357,9 @@ interface IDelegationManager is ISignatureUtils, IDelegationManagerErrors, IDele
) external;

/**
* @notice Array-ified version of `completeQueuedWithdrawal`.
* @notice Used to complete the all queued withdrawals.
* Used to complete the specified `withdrawals`. The function caller must match `withdrawals[...].withdrawer`
* @param withdrawals The Withdrawals to complete.
* @param withdrawals Array of Withdrawals to complete. See `completeQueuedWithdrawal` for the usage of a single Withdrawal.
* @param tokens Array of tokens for each Withdrawal. See `completeQueuedWithdrawal` for the usage of a single array.
* @param receiveAsTokens Whether or not to complete each withdrawal as tokens. See `completeQueuedWithdrawal` for the usage of a single boolean.
* @dev See `completeQueuedWithdrawal` for relevant dev tags
Expand Down Expand Up @@ -541,6 +553,11 @@ interface IDelegationManager is ISignatureUtils, IDelegationManagerErrors, IDele
*/
function MIN_WITHDRAWAL_DELAY_BLOCKS() external view returns (uint32);

/// @notice Returns a list of pending queued withdrawals for a `staker`, and the `shares` to be withdrawn.
function getQueuedWithdrawals(
address staker
) external view returns (Withdrawal[] memory withdrawals, uint256[][] memory shares);

/// @notice Returns the keccak256 hash of `withdrawal`.
function calculateWithdrawalRoot(
Withdrawal memory withdrawal
Expand Down
16 changes: 9 additions & 7 deletions src/contracts/libraries/SlashingLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity ^0.8.27;

import "@openzeppelin/contracts/utils/math/Math.sol";
import "@openzeppelin-upgrades/contracts/utils/math/SafeCastUpgradeable.sol";

/// @dev the stakerScalingFactor and operatorMagnitude have initial default values to 1e18 as "1"
/// to preserve precision with uint256 math. We use `WAD` where these variables are used
Expand All @@ -22,12 +23,10 @@ uint64 constant WAD = 1e18;
* Note that `withdrawal.scaledShares` is scaled for the beaconChainETHStrategy to divide by the beaconChainScalingFactor upon queueing
* and multiply by the beaconChainScalingFactor upon withdrawal
*/

struct StakerScalingFactors {
uint256 depositScalingFactor;
// we need to know if the beaconChainScalingFactor is set because it can be set to 0 through 100% slashing
bool isBeaconChainScalingFactorSet;
uint184 depositScalingFactor;
uint64 beaconChainScalingFactor;
bool isBeaconChainScalingFactorSet;
}

using SlashingLib for StakerScalingFactors global;
Expand All @@ -36,6 +35,7 @@ using SlashingLib for StakerScalingFactors global;
library SlashingLib {
using Math for uint256;
using SlashingLib for uint256;
using SafeCastUpgradeable for uint256;

// WAD MATH

Expand Down Expand Up @@ -119,7 +119,8 @@ library SlashingLib {
/// forgefmt: disable-next-item
ssf.depositScalingFactor = uint256(WAD)
.divWad(ssf.getBeaconChainScalingFactor())
.divWad(maxMagnitude);
.divWad(maxMagnitude)
.toUint184();
return;
}
/**
Expand Down Expand Up @@ -150,10 +151,11 @@ library SlashingLib {

// Step 3: Calculate newStakerDepositScalingFactor
/// forgefmt: disable-next-item
uint256 newStakerDepositScalingFactor = newShares
uint184 newStakerDepositScalingFactor = newShares
.divWad(existingDepositShares + addedShares)
.divWad(maxMagnitude)
.divWad(uint256(ssf.getBeaconChainScalingFactor()));
.divWad(uint256(ssf.getBeaconChainScalingFactor()))
.toUint184();

ssf.depositScalingFactor = newStakerDepositScalingFactor;
}
Expand Down
34 changes: 32 additions & 2 deletions src/test/unit/DelegationUnit.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,7 @@ contract DelegationManagerUnitTests is EigenLayerUnitTestSetup, IDelegationManag
strategyArray[0] = strategy;

// Set scaling factors
(uint256 depositScalingFactor, bool isBeaconChainScalingFactorSet, uint64 beaconChainScalingFactor) = delegationManager.stakerScalingFactor(staker, strategy);
(uint184 depositScalingFactor, uint64 beaconChainScalingFactor, bool isBeaconChainScalingFactorSet) = delegationManager.stakerScalingFactor(staker, strategy);
StakerScalingFactors memory stakerScalingFactor = StakerScalingFactors({
depositScalingFactor: depositScalingFactor,
isBeaconChainScalingFactorSet: isBeaconChainScalingFactorSet,
Expand Down Expand Up @@ -3933,6 +3933,15 @@ contract DelegationManagerUnitTests_completeQueuedWithdrawal is DelegationManage

cheats.expectRevert(IPausable.CurrentlyPaused.selector);
delegationManager.completeQueuedWithdrawal(withdrawal, tokens, false);

IERC20[][] memory tokensArray = new IERC20[][](1);
tokensArray[0] = tokens;

bool[] memory receiveAsTokens = new bool[](1);
receiveAsTokens[0] = false;

cheats.expectRevert(IPausable.CurrentlyPaused.selector);
delegationManager.completeQueuedWithdrawals(tokensArray, receiveAsTokens, 1);
}

function test_Revert_WhenInputArrayLengthMismatch() public {
Expand All @@ -3952,10 +3961,21 @@ contract DelegationManagerUnitTests_completeQueuedWithdrawal is DelegationManage
// resize tokens array
tokens = new IERC20[](0);

cheats.prank(defaultStaker);
cheats.expectRevert(IDelegationManagerErrors.InputArrayLengthMismatch.selector);
delegationManager.completeQueuedWithdrawal(withdrawal, tokens, false);
}

IERC20[][] memory tokensArray = new IERC20[][](1);
tokensArray[0] = tokens;

bool[] memory receiveAsTokens = new bool[](1);
receiveAsTokens[0] = false;

cheats.prank(defaultStaker);
cheats.expectRevert(IDelegationManagerErrors.InputArrayLengthMismatch.selector);
delegationManager.completeQueuedWithdrawals(tokensArray, receiveAsTokens, 1);
}

function test_Revert_WhenWithdrawerNotCaller(address invalidCaller) filterFuzzedAddressInputs(invalidCaller) public {
cheats.assume(invalidCaller != defaultStaker);

Expand Down Expand Up @@ -4031,6 +4051,16 @@ contract DelegationManagerUnitTests_completeQueuedWithdrawal is DelegationManage
cheats.expectRevert(IDelegationManagerErrors.WithdrawalDelayNotElapsed.selector);
cheats.prank(defaultStaker);
delegationManager.completeQueuedWithdrawal(withdrawal, tokens, receiveAsTokens);

IERC20[][] memory tokensArray = new IERC20[][](1);
tokensArray[0] = tokens;

bool[] memory receiveAsTokensArray = new bool[](1);
receiveAsTokensArray[0] = false;

cheats.expectRevert(IDelegationManagerErrors.WithdrawalDelayNotElapsed.selector);
cheats.prank(defaultStaker);
delegationManager.completeQueuedWithdrawals(tokensArray, receiveAsTokensArray, 1);
}

/**
Expand Down

0 comments on commit 6b25887

Please sign in to comment.