Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: strategy configs #392

Merged
merged 15 commits into from
Jan 23, 2024
20 changes: 14 additions & 6 deletions docs/core/DelegationManager.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,13 @@ This document organizes methods according to the following themes (click each to
* `mapping(address => mapping(IStrategy => uint256)) public operatorShares`: Tracks the current balance of shares an Operator is delegated according to each strategy. Updated by both the `StrategyManager` and `EigenPodManager` when a Staker's delegatable balance changes.
* Because Operators are delegated to themselves, an Operator's own restaked assets are reflected in these balances.
* A similar mapping exists in the `StrategyManager`, but the `DelegationManager` additionally tracks beacon chain ETH delegated via the `EigenPodManager`. The "beacon chain ETH" strategy gets its own special address for this mapping: `0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0`.
* `uint256 public withdrawalDelayBlocks`:
* `uint256 public minWithdrawalDelayBlocks`:
* As of M2, this is 50400 (roughly 1 week)
* Stakers must wait this amount of time before a withdrawal can be completed
* For all strategies including native beacon chain ETH, Stakers at minimum must wait this amount of time before a withdrawal can be completed.
To withdraw a specific strategy, it may require additional time depending on the strategy's withdrawal delay. See `strategyWithdrawalDelayBlocks` below.
* `mapping(IStrategy => uint256) public strategyWithdrawalDelayBlocks`:
* This mapping tracks the withdrawal delay for each strategy. This mapping value only comes into affect
if strategyWithdrawalDelayBlocks[strategy] > minWithdrawalDelayBlocks. If the strategyWithdrawalDelayBlocks[strategy] is less than or equal to minWithdrawalDelayBlocks, then the minWithdrawalDelayBlocks is used.
* `mapping(bytes32 => bool) public pendingWithdrawals;`:
* `Withdrawals` are hashed and set to `true` in this mapping when a withdrawal is initiated. The hash is set to false again when the withdrawal is completed. A per-staker nonce provides a way to distinguish multiple otherwise-identical withdrawals.

Expand Down Expand Up @@ -236,7 +240,7 @@ function undelegate(

If the Staker has active shares in either the `EigenPodManager` or `StrategyManager`, they are removed while the withdrawal is in the queue - and an individual withdrawal is queued for each strategy removed.

The withdrawals can be completed by the Staker after `withdrawalDelayBlocks`. This does not require the Staker to "fully exit" from the system -- the Staker may choose to receive their shares back in full once withdrawals are completed (see [`completeQueuedWithdrawal`](#completequeuedwithdrawal) for details).
The withdrawals can be completed by the Staker after max(`minWithdrawalDelayBlocks`, `strategyWithdrawalDelayBlocks[strategy]`) where `strategy` is any of the Staker's delegated strategies. This does not require the Staker to "fully exit" from the system -- the Staker may choose to receive their shares back in full once withdrawals are completed (see [`completeQueuedWithdrawal`](#completequeuedwithdrawal) for details).

Note that becoming an Operator is irreversible! Although Operators can withdraw, they cannot use this method to undelegate from themselves.

Expand Down Expand Up @@ -278,7 +282,9 @@ Allows the caller to queue one or more withdrawals of their held shares across a

All shares being withdrawn (whether via the `EigenPodManager` or `StrategyManager`) are removed while the withdrawals are in the queue.

Withdrawals can be completed by the `withdrawer` after `withdrawalDelayBlocks`, and does not require the `withdrawer` to "fully exit" from the system -- they may choose to receive their shares back in full once the withdrawal is completed (see [`completeQueuedWithdrawal`](#completequeuedwithdrawal) for details).
Withdrawals can be completed by the `withdrawer` after max(`minWithdrawalDelayBlocks`, `strategyWithdrawalDelayBlocks[strategy]`) such that `strategy` represents the queued strategies to be withdrawn, and does not require the `withdrawer` to "fully exit" from the system -- they may choose to receive their shares back in full once the withdrawal is completed (see [`completeQueuedWithdrawal`](#completequeuedwithdrawal) for details).

Note that for any `strategy` s.t `StrategyManager.thirdPartyTransfersForbidden(strategy) == true` the `withdrawer` must be the same address as the `staker` as this setting disallows users to deposit or withdraw on behalf of other users. (see [`thirdPartyTransfersForbidden`](./StrategyManager.md) for details).

*Effects*:
* For each withdrawal:
Expand All @@ -295,6 +301,7 @@ Withdrawals can be completed by the `withdrawer` after `withdrawalDelayBlocks`,
* `strategies.length` MUST equal `shares.length`
* `strategies.length` MUST NOT be equal to 0
* The `withdrawer` MUST NOT be 0
* For all strategies being withdrawn, the `withdrawer` MUST be the same address as the `staker` if `StrategyManager.thirdPartyTransfersForbidden(strategy) == true`
* See [`EigenPodManager.removeShares`](./EigenPodManager.md#eigenpodmanagerremoveshares)
* See [`StrategyManager.removeShares`](./StrategyManager.md#removeshares)

Expand All @@ -312,7 +319,7 @@ function completeQueuedWithdrawal(
nonReentrant
```

After waiting `withdrawalDelayBlocks`, this allows the `withdrawer` of a `Withdrawal` to finalize a withdrawal and receive either (i) the underlying tokens of the strategies being withdrawn from, or (ii) the shares being withdrawn. This choice is dependent on the passed-in parameter `receiveAsTokens`.
After waiting max(`minWithdrawalDelayBlocks`, `strategyWithdrawalDelayBlocks[strategy]`) number of blocks, this allows the `withdrawer` of a `Withdrawal` to finalize a withdrawal and receive either (i) the underlying tokens of the strategies being withdrawn from, or (ii) the shares being withdrawn. This choice is dependent on the passed-in parameter `receiveAsTokens`.

For each strategy/share pair in the `Withdrawal`:
* If the `withdrawer` chooses to receive tokens:
Expand Down Expand Up @@ -345,7 +352,8 @@ For each strategy/share pair in the `Withdrawal`:
*Requirements*:
* Pause status MUST NOT be set: `PAUSED_EXIT_WITHDRAWAL_QUEUE`
* The hash of the passed-in `Withdrawal` MUST correspond to a pending withdrawal
* At least `withdrawalDelayBlocks` MUST have passed before `completeQueuedWithdrawal` is called
* At least `minWithdrawalDelayBlocks` MUST have passed before `completeQueuedWithdrawal` is called
* For all strategies in the `Withdrawal`, at least `strategyWithdrawalDelayBlocks[strategy]` MUST have passed before `completeQueuedWithdrawal` is called
* Caller MUST be the `withdrawer` specified in the `Withdrawal`
* If `receiveAsTokens`:
* The caller MUST pass in the underlying `IERC20[] tokens` being withdrawn in the appropriate order according to the strategies in the `Withdrawal`.
Expand Down
2 changes: 2 additions & 0 deletions docs/core/StrategyManager.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ This document organizes methods according to the following themes (click each to
* `mapping(address => IStrategy[]) public stakerStrategyList`: Maintains a list of the strategies a Staker holds a nonzero number of shares in.
* Updated as needed when Stakers deposit and withdraw: if a Staker has a zero balance in a Strategy, it is removed from the list. Likewise, if a Staker deposits into a Strategy and did not previously have a balance, it is added to the list.
* `mapping(IStrategy => bool) public strategyIsWhitelistedForDeposit`: The `strategyWhitelister` is (as of M2) a permissioned role that can be changed by the contract owner. The `strategyWhitelister` has currently whitelisted 3 `StrategyBaseTVLLimits` contracts in this mapping, one for each supported LST.
* `mapping(IStrategy => bool) public thirdPartyTransfersForbidden`: The `strategyWhitelister` can disable third party transfers for a given strategy. This means that if `thirdPartyTransfersForbidden[strategy] == true`, then you cannot deposit on behalf of another Staker and award them shares. You also cannot withraw shares on behalf of another Staker. (see [`DelegationManager.queueWithdrawals`](./DelegationManager#queueWithdrawals.md))
8sunyuan marked this conversation as resolved.
Show resolved Hide resolved

#### Helpful definitions

Expand Down Expand Up @@ -97,6 +98,7 @@ function depositIntoStrategyWithSignature(

*Requirements*: See `depositIntoStrategy` above. Additionally:
* Caller MUST provide a valid, unexpired signature over the correct fields
* `thirdPartyTransfersForbidden[strategy]` MUST be false

---

Expand Down
11 changes: 10 additions & 1 deletion script/milestone/M2Deploy.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -325,8 +325,17 @@ contract M2Deploy is Script, Test {
0
);

IStrategy[] memory strategyArray = new IStrategy[](0);
uint256[] memory withdrawalDelayBlocksArray = new uint256[](0);
cheats.expectRevert(bytes("Initializable: contract is already initialized"));
DelegationManager(address(delegation)).initialize(address(this), PauserRegistry(address(this)), 0, 0);
DelegationManager(address(delegation)).initialize(
address(this),
PauserRegistry(address(this)),
0, // initialPausedStatus
0, // minWithdrawalDelayBLocks
strategyArray,
withdrawalDelayBlocksArray
);

cheats.expectRevert(bytes("Initializable: contract is already initialized"));
EigenPodManager(address(eigenPodManager)).initialize(
Expand Down
95 changes: 79 additions & 16 deletions src/contracts/core/DelegationManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -68,18 +68,21 @@ contract DelegationManager is Initializable, OwnableUpgradeable, Pausable, Deleg

/**
* @dev Initializes the addresses of the initial owner, pauser registry, and paused status.
* withdrawalDelayBlocks is set only once here
* strategyWithdrawalDelayBlocks is set only once here
8sunyuan marked this conversation as resolved.
Show resolved Hide resolved
*/
function initialize(
address initialOwner,
IPauserRegistry _pauserRegistry,
uint256 initialPausedStatus,
uint256 _withdrawalDelayBlocks
uint256 _minWithdrawalDelayBlocks,
IStrategy[] calldata _strategiesToSetDelayBlocks,
uint256[] calldata _withdrawalDelayBlocks
) external initializer {
_initializePauser(_pauserRegistry, initialPausedStatus);
_DOMAIN_SEPARATOR = _calculateDomainSeparator();
_transferOwnership(initialOwner);
_initializeWithdrawalDelayBlocks(_withdrawalDelayBlocks);
_initializeMinWithdrawalDelayBlocks(_minWithdrawalDelayBlocks);
_setStrategyWithdrawalDelayBlocks(_strategiesToSetDelayBlocks, _withdrawalDelayBlocks);
}

/*******************************************************************************
Expand Down Expand Up @@ -266,7 +269,7 @@ contract DelegationManager is Initializable, OwnableUpgradeable, Pausable, Deleg
return withdrawalRoots;
}

/**
/**
* Allows a staker to withdraw some shares. Withdrawn shares/strategies are immediately removed
* from the staker. If the staker is delegated, withdrawn shares/strategies are also removed from
* their operator.
Expand All @@ -277,13 +280,12 @@ contract DelegationManager is Initializable, OwnableUpgradeable, Pausable, Deleg
QueuedWithdrawalParams[] calldata queuedWithdrawalParams
) external onlyWhenNotPaused(PAUSED_ENTER_WITHDRAWAL_QUEUE) returns (bytes32[] memory) {
bytes32[] memory withdrawalRoots = new bytes32[](queuedWithdrawalParams.length);
address operator = delegatedTo[msg.sender];

for (uint256 i = 0; i < queuedWithdrawalParams.length; i++) {
require(queuedWithdrawalParams[i].strategies.length == queuedWithdrawalParams[i].shares.length, "DelegationManager.queueWithdrawal: input length mismatch");
require(queuedWithdrawalParams[i].withdrawer != address(0), "DelegationManager.queueWithdrawal: must provide valid withdrawal address");

address operator = delegatedTo[msg.sender];

// Remove shares from staker's strategies and place strategies/shares in queue.
// If the staker is delegated to an operator, the operator's delegated shares are also reduced
// NOTE: This will fail if the staker doesn't have the shares implied by the input parameters
Expand Down Expand Up @@ -499,6 +501,20 @@ contract DelegationManager is Initializable, OwnableUpgradeable, Pausable, Deleg
emit OperatorAVSRegistrationStatusUpdated(operator, msg.sender, OperatorAVSRegistrationStatus.UNREGISTERED);
}

/**
* @notice Called by owner to set the minimum withdrawal delay blocks for each passed in strategy
* Note that the min number of blocks to complete a withdrawal of a strategy is
* MAX(minWithdrawalDelayBlocks, strategyWithdrawalDelayBlocks[strategy])
* @param strategies The strategies to set the minimum withdrawal delay blocks for
* @param withdrawalDelayBlocks The minimum withdrawal delay blocks to set for each strategy
*/
function setStrategyWithdrawalDelayBlocks(
IStrategy[] calldata strategies,
uint256[] calldata withdrawalDelayBlocks
) external onlyOwner {
_setStrategyWithdrawalDelayBlocks(strategies, withdrawalDelayBlocks);
}

/*******************************************************************************
INTERNAL FUNCTIONS
*******************************************************************************/
Expand Down Expand Up @@ -617,23 +633,23 @@ contract DelegationManager is Initializable, OwnableUpgradeable, Pausable, Deleg

require(
pendingWithdrawals[withdrawalRoot],
"DelegationManager.completeQueuedAction: action is not in queue"
"DelegationManager._completeQueuedWithdrawal: action is not in queue"
);

require(
withdrawal.startBlock + withdrawalDelayBlocks <= block.number,
"DelegationManager.completeQueuedAction: withdrawalDelayBlocks period has not yet passed"
withdrawal.startBlock + minWithdrawalDelayBlocks <= block.number,
"DelegationManager._completeQueuedWithdrawal: minWithdrawalDelayBlocks period has not yet passed"
);

require(
msg.sender == withdrawal.withdrawer,
"DelegationManager.completeQueuedAction: only withdrawer can complete action"
"DelegationManager._completeQueuedWithdrawal: only withdrawer can complete action"
);

if (receiveAsTokens) {
require(
tokens.length == withdrawal.strategies.length,
"DelegationManager.completeQueuedAction: input length mismatch"
"DelegationManager._completeQueuedWithdrawal: input length mismatch"
);
}

Expand All @@ -644,6 +660,11 @@ contract DelegationManager is Initializable, OwnableUpgradeable, Pausable, Deleg
// by re-awarding shares in each strategy.
if (receiveAsTokens) {
for (uint256 i = 0; i < withdrawal.strategies.length; ) {
require(
withdrawal.startBlock + strategyWithdrawalDelayBlocks[withdrawal.strategies[i]] <= block.number,
"DelegationManager._completeQueuedWithdrawal: withdrawalDelayBlocks period has not yet passed for this strategy"
);

_withdrawSharesAsTokens({
staker: withdrawal.staker,
withdrawer: msg.sender,
Expand All @@ -657,6 +678,11 @@ contract DelegationManager is Initializable, OwnableUpgradeable, Pausable, Deleg
} else {
address currentOperator = delegatedTo[msg.sender];
for (uint256 i = 0; i < withdrawal.strategies.length; ) {
require(
withdrawal.startBlock + strategyWithdrawalDelayBlocks[withdrawal.strategies[i]] <= block.number,
"DelegationManager._completeQueuedWithdrawal: withdrawalDelayBlocks period has not yet passed for this strategy"
);

/** When awarding podOwnerShares in EigenPodManager, we need to be sure to only give them back to the original podOwner.
* Other strategy shares can + will be awarded to the withdrawer.
*/
Expand Down Expand Up @@ -717,6 +743,7 @@ contract DelegationManager is Initializable, OwnableUpgradeable, Pausable, Deleg
/**
* @notice Removes `shares` in `strategies` from `staker` who is currently delegated to `operator` and queues a withdrawal to the `withdrawer`.
* @dev If the `operator` is indeed an operator, then the operator's delegated shares in the `strategies` are also decreased appropriately.
* @dev If `withdrawer` is not the same address as `staker`, then thirdPartyTransfersForbidden[strategy] must be set to false in the StrategyManager.
*/
function _removeSharesAndQueueWithdrawal(
address staker,
Expand Down Expand Up @@ -751,6 +778,10 @@ contract DelegationManager is Initializable, OwnableUpgradeable, Pausable, Deleg
*/
eigenPodManager.removeShares(staker, shares[i]);
} else {
require(
staker == withdrawer || !strategyManager.thirdPartyTransfersForbidden(strategies[i]),
"DelegationManager._removeSharesAndQueueWithdrawal: withdrawer must be same address as staker if thirdPartyTransfersForbidden are set"
);
// this call will revert if `shares[i]` exceeds the Staker's current shares in `strategies[i]`
strategyManager.removeShares(staker, strategies[i], shares[i]);
}
Expand Down Expand Up @@ -797,13 +828,45 @@ contract DelegationManager is Initializable, OwnableUpgradeable, Pausable, Deleg
}
}

function _initializeWithdrawalDelayBlocks(uint256 _withdrawalDelayBlocks) internal {
function _initializeMinWithdrawalDelayBlocks(uint256 _minWithdrawalDelayBlocks) internal {
require(
_minWithdrawalDelayBlocks <= MAX_WITHDRAWAL_DELAY_BLOCKS,
"DelegationManager._initializeMinWithdrawalDelayBlocks: _minWithdrawalDelayBlocks cannot be > MAX_WITHDRAWAL_DELAY_BLOCKS"
);
emit MinWithdrawalDelayBlocksSet(minWithdrawalDelayBlocks, _minWithdrawalDelayBlocks);
minWithdrawalDelayBlocks = _minWithdrawalDelayBlocks;
}

/**
* @notice Sets the withdrawal delay blocks for each strategy in `_strategiesToSetDelayBlocks` to `_withdrawalDelayBlocks`.
* gets called when initializing contract or by calling `setStrategyWithdrawalDelayBlocks`
*/
function _setStrategyWithdrawalDelayBlocks(
IStrategy[] calldata _strategiesToSetDelayBlocks,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could just call this param _strategies

The context makes it pretty obvious what the list is for, so the current name is super long

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uint256[] calldata _withdrawalDelayBlocks
) internal {
require(
_withdrawalDelayBlocks <= MAX_WITHDRAWAL_DELAY_BLOCKS,
"DelegationManager._initializeWithdrawalDelayBlocks: _withdrawalDelayBlocks cannot be > MAX_WITHDRAWAL_DELAY_BLOCKS"
_strategiesToSetDelayBlocks.length == _withdrawalDelayBlocks.length,
"DelegationManager._setStrategyWithdrawalDelayBlocks: input length mismatch"
);
emit WithdrawalDelayBlocksSet(withdrawalDelayBlocks, _withdrawalDelayBlocks);
withdrawalDelayBlocks = _withdrawalDelayBlocks;
uint256 numStrats = _strategiesToSetDelayBlocks.length;
for (uint256 i = 0; i < numStrats; ++i) {
IStrategy strategy = _strategiesToSetDelayBlocks[i];
uint256 prevStrategyWithdrawalDelayBlocks = strategyWithdrawalDelayBlocks[strategy];
uint256 newStrategyWithdrawalDelayBlocks = _withdrawalDelayBlocks[i];
require(
newStrategyWithdrawalDelayBlocks <= MAX_WITHDRAWAL_DELAY_BLOCKS,
8sunyuan marked this conversation as resolved.
Show resolved Hide resolved
"DelegationManager._setStrategyWithdrawalDelayBlocks: _withdrawalDelayBlocks cannot be > MAX_WITHDRAWAL_DELAY_BLOCKS"
);

// set the new withdrawal delay blocks
strategyWithdrawalDelayBlocks[strategy] = newStrategyWithdrawalDelayBlocks;
emit StrategyWithdrawalDelayBlocksSet(
strategy,
prevStrategyWithdrawalDelayBlocks,
newStrategyWithdrawalDelayBlocks
);
}
}

/*******************************************************************************
Expand Down
Loading
Loading