Skip to content

Commit

Permalink
feat: track staker withdrawals (#864)
Browse files Browse the repository at this point in the history
* feat: track staker withdrawals

* chore: forge fmt

* feat: track staker withdrawals

* chore: forge fmt

* fix: `pendingWithdrawals` arrangement

* fix: ci
  • Loading branch information
0xClandestine authored Nov 4, 2024
1 parent 2639e35 commit 88b5f17
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 29 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/testinparallel.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
- name: Run Forge build
run: |
forge --version
forge build --sizes
forge build
id: build

- name: Run unit tests
Expand Down
57 changes: 53 additions & 4 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 @@ -258,11 +259,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 @@ -458,7 +474,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 @@ -505,8 +521,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 @@ -646,9 +666,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, depositSharesToWithdraw);
return withdrawalRoot;
}
Expand Down Expand Up @@ -779,6 +802,32 @@ 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(
Withdrawal memory withdrawal
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 @@ -82,7 +84,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 @@ -100,6 +103,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 @@ -121,5 +133,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 @@ -283,10 +283,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 @@ -302,9 +314,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 @@ -488,6 +500,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 @@ -130,7 +130,8 @@ library SlashingLib {
/// forgefmt: disable-next-item
ssf.depositScalingFactor = uint256(WAD)
.divWad(ssf.getBeaconChainScalingFactor())
.divWad(maxMagnitude);
.divWad(maxMagnitude)
.toUint184();
return;
}
/**
Expand Down Expand Up @@ -161,10 +162,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
2 changes: 1 addition & 1 deletion src/test/integration/IntegrationBase.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -1163,7 +1163,7 @@ abstract contract IntegrationBase is IntegrationDeployer {

/// @dev Looks up the staker's beacon chain scaling factor
function _getBeaconChainScalingFactor(User staker) internal view returns (uint64) {
(,bool isBeaconChainScalingFactorSet, uint64 beaconChainScalingFactor)= delegationManager.stakerScalingFactor(address(staker), BEACONCHAIN_ETH_STRAT);
(, uint64 beaconChainScalingFactor, bool isBeaconChainScalingFactorSet)= delegationManager.stakerScalingFactor(address(staker), BEACONCHAIN_ETH_STRAT);
return isBeaconChainScalingFactorSet ? beaconChainScalingFactor : WAD;
}

Expand Down
46 changes: 38 additions & 8 deletions src/test/unit/DelegationUnit.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,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 @@ -2907,7 +2907,7 @@ contract DelegationManagerUnitTests_Undelegate is DelegationManagerUnitTests {
});

StakerScalingFactors memory ssf = StakerScalingFactors({
depositScalingFactor: depositScalingFactor,
depositScalingFactor: uint184(depositScalingFactor),
isBeaconChainScalingFactorSet: false,
beaconChainScalingFactor: 0
});
Expand Down Expand Up @@ -3210,12 +3210,12 @@ contract DelegationManagerUnitTests_queueWithdrawals is DelegationManagerUnitTes
uint256 delegatedSharesAfter = delegationManager.operatorShares(defaultOperator, strategies[0]);

{
(uint256 depositScalingFactor, bool isBeaconChainScalingFactorSet, uint64 beaconChainScalingFactor) = delegationManager.stakerScalingFactor(defaultStaker, strategyMock);
(uint256 depositScalingFactor, uint64 beaconChainScalingFactor, bool isBeaconChainScalingFactorSet) = delegationManager.stakerScalingFactor(defaultStaker, strategyMock);
ssf = StakerScalingFactors({
depositScalingFactor: depositScalingFactor,
isBeaconChainScalingFactorSet: isBeaconChainScalingFactorSet,
beaconChainScalingFactor: beaconChainScalingFactor
});
depositScalingFactor: uint184(depositScalingFactor),
beaconChainScalingFactor: beaconChainScalingFactor,
isBeaconChainScalingFactorSet: isBeaconChainScalingFactorSet
});
}
uint256 sharesWithdrawn = withdrawalAmount.toShares(ssf, 5e17);
assertEq(nonceBefore + 1, nonceAfter, "staker nonce should have incremented");
Expand Down Expand Up @@ -3368,6 +3368,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 @@ -3387,10 +3396,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 @@ -3466,6 +3486,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 88b5f17

Please sign in to comment.