-
Notifications
You must be signed in to change notification settings - Fork 194
/
WithdrawalQueue.sol
415 lines (353 loc) · 19.3 KB
/
WithdrawalQueue.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
// SPDX-FileCopyrightText: 2023 Lido <info@lido.fi>
// SPDX-License-Identifier: GPL-3.0
/* See contracts/COMPILERS.md */
pragma solidity 0.8.9;
import {WithdrawalQueueBase} from "./WithdrawalQueueBase.sol";
import {IERC20} from "@openzeppelin/contracts-v4.4/token/ERC20/IERC20.sol";
import {IERC20Permit} from "@openzeppelin/contracts-v4.4/token/ERC20/extensions/draft-IERC20Permit.sol";
import {EnumerableSet} from "@openzeppelin/contracts-v4.4/utils/structs/EnumerableSet.sol";
import {AccessControlEnumerable} from "./utils/access/AccessControlEnumerable.sol";
import {UnstructuredStorage} from "./lib/UnstructuredStorage.sol";
import {PausableUntil} from "./utils/PausableUntil.sol";
import {Versioned} from "./utils/Versioned.sol";
/// @notice Interface defining a Lido liquid staking pool
/// @dev see also [Lido liquid staking pool core contract](https://docs.lido.fi/contracts/lido)
interface IStETH is IERC20, IERC20Permit {
function getSharesByPooledEth(uint256 _pooledEthAmount) external view returns (uint256);
}
/// @notice Interface defining a Lido liquid staking pool wrapper
/// @dev see WstETH.sol for full docs
interface IWstETH is IERC20, IERC20Permit {
function unwrap(uint256 _wstETHAmount) external returns (uint256);
function getStETHByWstETH(uint256 _wstETHAmount) external view returns (uint256);
function stETH() external view returns (IStETH);
}
/// @title A contract for handling stETH withdrawal request queue within the Lido protocol
/// @author folkyatina
abstract contract WithdrawalQueue is AccessControlEnumerable, PausableUntil, WithdrawalQueueBase, Versioned {
using UnstructuredStorage for bytes32;
using EnumerableSet for EnumerableSet.UintSet;
/// Bunker mode activation timestamp
bytes32 internal constant BUNKER_MODE_SINCE_TIMESTAMP_POSITION =
keccak256("lido.WithdrawalQueue.bunkerModeSinceTimestamp");
/// Special value for timestamp when bunker mode is inactive (i.e., protocol in turbo mode)
uint256 public constant BUNKER_MODE_DISABLED_TIMESTAMP = type(uint256).max;
// ACL
bytes32 public constant PAUSE_ROLE = keccak256("PAUSE_ROLE");
bytes32 public constant RESUME_ROLE = keccak256("RESUME_ROLE");
bytes32 public constant FINALIZE_ROLE = keccak256("FINALIZE_ROLE");
bytes32 public constant ORACLE_ROLE = keccak256("ORACLE_ROLE");
/// @notice minimal amount of stETH that is possible to withdraw
uint256 public constant MIN_STETH_WITHDRAWAL_AMOUNT = 100;
/// @notice maximum amount of stETH that is possible to withdraw by a single request
/// Prevents accumulating too much funds per single request fulfillment in the future.
/// @dev To withdraw larger amounts, it's recommended to split it to several requests
uint256 public constant MAX_STETH_WITHDRAWAL_AMOUNT = 1000 * 1e18;
/// @notice Lido stETH token address
IStETH public immutable STETH;
/// @notice Lido wstETH token address
IWstETH public immutable WSTETH;
event InitializedV1(address _admin);
event BunkerModeEnabled(uint256 _sinceTimestamp);
event BunkerModeDisabled();
error AdminZeroAddress();
error RequestAmountTooSmall(uint256 _amountOfStETH);
error RequestAmountTooLarge(uint256 _amountOfStETH);
error InvalidReportTimestamp();
error RequestIdsNotSorted();
error ZeroRecipient();
error ArraysLengthMismatch(uint256 _firstArrayLength, uint256 _secondArrayLength);
/// @param _wstETH address of WstETH contract
constructor(IWstETH _wstETH) {
// init immutables
WSTETH = _wstETH;
STETH = WSTETH.stETH();
}
/// @notice Initialize the contract storage explicitly.
/// @param _admin admin address that can change every role.
/// @dev Reverts if `_admin` equals to `address(0)`
/// @dev NB! It's initialized in paused state by default and should be resumed explicitly to start
/// @dev NB! Bunker mode is disabled by default
function initialize(address _admin) external {
if (_admin == address(0)) revert AdminZeroAddress();
_initialize(_admin);
}
/// @notice Resume withdrawal requests placement and finalization
/// Contract is deployed in paused state and should be resumed explicitly
function resume() external {
_checkRole(RESUME_ROLE, msg.sender);
_resume();
}
/// @notice Pause withdrawal requests placement and finalization. Claiming finalized requests will still be available
/// @param _duration pause duration in seconds (use `PAUSE_INFINITELY` for unlimited)
/// @dev Reverts if contract is already paused
/// @dev Reverts reason if sender has no `PAUSE_ROLE`
/// @dev Reverts if zero duration is passed
function pauseFor(uint256 _duration) external onlyRole(PAUSE_ROLE) {
_pauseFor(_duration);
}
/// @notice Pause withdrawal requests placement and finalization. Claiming finalized requests will still be available
/// @param _pauseUntilInclusive the last second to pause until inclusive
/// @dev Reverts if the timestamp is in the past
/// @dev Reverts if sender has no `PAUSE_ROLE`
/// @dev Reverts if contract is already paused
function pauseUntil(uint256 _pauseUntilInclusive) external onlyRole(PAUSE_ROLE) {
_pauseUntil(_pauseUntilInclusive);
}
/// @notice Request the batch of stETH for withdrawal. Approvals for the passed amounts should be done before.
/// @param _amounts an array of stETH amount values.
/// The standalone withdrawal request will be created for each item in the passed list.
/// @param _owner address that will be able to manage the created requests.
/// If `address(0)` is passed, `msg.sender` will be used as owner.
/// @return requestIds an array of the created withdrawal request ids
function requestWithdrawals(uint256[] calldata _amounts, address _owner)
public
returns (uint256[] memory requestIds)
{
_checkResumed();
if (_owner == address(0)) _owner = msg.sender;
requestIds = new uint256[](_amounts.length);
for (uint256 i = 0; i < _amounts.length; ++i) {
_checkWithdrawalRequestAmount(_amounts[i]);
requestIds[i] = _requestWithdrawal(_amounts[i], _owner);
}
}
/// @notice Request the batch of wstETH for withdrawal. Approvals for the passed amounts should be done before.
/// @param _amounts an array of wstETH amount values.
/// The standalone withdrawal request will be created for each item in the passed list.
/// @param _owner address that will be able to manage the created requests.
/// If `address(0)` is passed, `msg.sender` will be used as an owner.
/// @return requestIds an array of the created withdrawal request ids
function requestWithdrawalsWstETH(uint256[] calldata _amounts, address _owner)
public
returns (uint256[] memory requestIds)
{
_checkResumed();
if (_owner == address(0)) _owner = msg.sender;
requestIds = new uint256[](_amounts.length);
for (uint256 i = 0; i < _amounts.length; ++i) {
requestIds[i] = _requestWithdrawalWstETH(_amounts[i], _owner);
}
}
struct PermitInput {
uint256 value;
uint256 deadline;
uint8 v;
bytes32 r;
bytes32 s;
}
/// @notice Request the batch of stETH for withdrawal using EIP-2612 Permit
/// @param _amounts an array of stETH amount values
/// The standalone withdrawal request will be created for each item in the passed list.
/// @param _owner address that will be able to manage the created requests.
/// If `address(0)` is passed, `msg.sender` will be used as an owner.
/// @param _permit data required for the stETH.permit() method to set the allowance
/// @return requestIds an array of the created withdrawal request ids
function requestWithdrawalsWithPermit(uint256[] calldata _amounts, address _owner, PermitInput calldata _permit)
external
returns (uint256[] memory requestIds)
{
STETH.permit(msg.sender, address(this), _permit.value, _permit.deadline, _permit.v, _permit.r, _permit.s);
return requestWithdrawals(_amounts, _owner);
}
/// @notice Request the batch of wstETH for withdrawal using EIP-2612 Permit
/// @param _amounts an array of wstETH amount values
/// The standalone withdrawal request will be created for each item in the passed list.
/// @param _owner address that will be able to manage the created requests.
/// If `address(0)` is passed, `msg.sender` will be used as an owner.
/// @param _permit data required for the wtETH.permit() method to set the allowance
/// @return requestIds an array of the created withdrawal request ids
function requestWithdrawalsWstETHWithPermit(
uint256[] calldata _amounts,
address _owner,
PermitInput calldata _permit
) external returns (uint256[] memory requestIds) {
WSTETH.permit(msg.sender, address(this), _permit.value, _permit.deadline, _permit.v, _permit.r, _permit.s);
return requestWithdrawalsWstETH(_amounts, _owner);
}
/// @notice Returns all withdrawal requests that belongs to the `_owner` address
///
/// WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
/// to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
/// this function has an unbounded cost, and using it as part of a state-changing function may render the function
/// uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
function getWithdrawalRequests(address _owner) external view returns (uint256[] memory requestsIds) {
return _getRequestsByOwner()[_owner].values();
}
/// @notice Returns status for requests with provided ids
/// @param _requestIds array of withdrawal request ids
function getWithdrawalStatus(uint256[] calldata _requestIds)
external
view
returns (WithdrawalRequestStatus[] memory statuses)
{
statuses = new WithdrawalRequestStatus[](_requestIds.length);
for (uint256 i = 0; i < _requestIds.length; ++i) {
statuses[i] = _getStatus(_requestIds[i]);
}
}
/// @notice Returns amount of ether available for claim for each provided request id
/// @param _requestIds array of request ids
/// @param _hints checkpoint hints. can be found with `findCheckpointHints(_requestIds, 1, getLastCheckpointIndex())`
/// @return claimableEthValues amount of claimable ether for each request, amount is equal to 0 if request
/// is not finalized or already claimed
function getClaimableEther(uint256[] calldata _requestIds, uint256[] calldata _hints)
external
view
returns (uint256[] memory claimableEthValues)
{
claimableEthValues = new uint256[](_requestIds.length);
for (uint256 i = 0; i < _requestIds.length; ++i) {
claimableEthValues[i] = _getClaimableEther(_requestIds[i], _hints[i]);
}
}
/// @notice Claim a batch of withdrawal requests if they are finalized sending ether to `_recipient`
/// @param _requestIds array of request ids to claim
/// @param _hints checkpoint hint for each id. Can be obtained with `findCheckpointHints()`
/// @param _recipient address where claimed ether will be sent to
/// @dev
/// Reverts if recipient is equal to zero
/// Reverts if requestIds and hints arrays length differs
/// Reverts if any requestId or hint in arguments are not valid
/// Reverts if any request is not finalized or already claimed
/// Reverts if msg sender is not an owner of the requests
function claimWithdrawalsTo(uint256[] calldata _requestIds, uint256[] calldata _hints, address _recipient)
external
{
if (_recipient == address(0)) revert ZeroRecipient();
if (_requestIds.length != _hints.length) {
revert ArraysLengthMismatch(_requestIds.length, _hints.length);
}
for (uint256 i = 0; i < _requestIds.length; ++i) {
_claim(_requestIds[i], _hints[i], _recipient);
_emitTransfer(msg.sender, address(0), _requestIds[i]);
}
}
/// @notice Claim a batch of withdrawal requests if they are finalized sending locked ether to the owner
/// @param _requestIds array of request ids to claim
/// @param _hints checkpoint hint for each id. Can be obtained with `findCheckpointHints()`
/// @dev
/// Reverts if requestIds and hints arrays length differs
/// Reverts if any requestId or hint in arguments are not valid
/// Reverts if any request is not finalized or already claimed
/// Reverts if msg sender is not an owner of the requests
function claimWithdrawals(uint256[] calldata _requestIds, uint256[] calldata _hints) external {
if (_requestIds.length != _hints.length) {
revert ArraysLengthMismatch(_requestIds.length, _hints.length);
}
for (uint256 i = 0; i < _requestIds.length; ++i) {
_claim(_requestIds[i], _hints[i], msg.sender);
_emitTransfer(msg.sender, address(0), _requestIds[i]);
}
}
/// @notice Claim one`_requestId` request once finalized sending locked ether to the owner
/// @param _requestId request id to claim
/// @dev use unbounded loop to find a hint, which can lead to OOG
/// @dev
/// Reverts if requestId or hint are not valid
/// Reverts if request is not finalized or already claimed
/// Reverts if msg sender is not an owner of request
function claimWithdrawal(uint256 _requestId) external {
_claim(_requestId, _findCheckpointHint(_requestId, 1, getLastCheckpointIndex()), msg.sender);
_emitTransfer(msg.sender, address(0), _requestId);
}
/// @notice Finds the list of hints for the given `_requestIds` searching among the checkpoints with indices
/// in the range `[_firstIndex, _lastIndex]`.
/// NB! Array of request ids should be sorted
/// NB! `_firstIndex` should be greater than 0, because checkpoint list is 1-based array
/// Usage: findCheckpointHints(_requestIds, 1, getLastCheckpointIndex())
/// @param _requestIds ids of the requests sorted in the ascending order to get hints for
/// @param _firstIndex left boundary of the search range. Should be greater than 0
/// @param _lastIndex right boundary of the search range. Should be less than or equal to getLastCheckpointIndex()
/// @return hintIds array of hints used to find required checkpoint for the request
function findCheckpointHints(uint256[] calldata _requestIds, uint256 _firstIndex, uint256 _lastIndex)
external
view
returns (uint256[] memory hintIds)
{
hintIds = new uint256[](_requestIds.length);
uint256 prevRequestId = 0;
for (uint256 i = 0; i < _requestIds.length; ++i) {
if (_requestIds[i] < prevRequestId) revert RequestIdsNotSorted();
hintIds[i] = _findCheckpointHint(_requestIds[i], _firstIndex, _lastIndex);
_firstIndex = hintIds[i];
prevRequestId = _requestIds[i];
}
}
/// @notice Update bunker mode state and last report timestamp on oracle report
/// @dev should be called by oracle
///
/// @param _isBunkerModeNow is bunker mode reported by oracle
/// @param _bunkerStartTimestamp timestamp of start of the bunker mode
/// @param _currentReportTimestamp timestamp of the current report ref slot
function onOracleReport(bool _isBunkerModeNow, uint256 _bunkerStartTimestamp, uint256 _currentReportTimestamp)
external
{
_checkRole(ORACLE_ROLE, msg.sender);
if (_bunkerStartTimestamp >= block.timestamp) revert InvalidReportTimestamp();
if (_currentReportTimestamp >= block.timestamp) revert InvalidReportTimestamp();
_setLastReportTimestamp(_currentReportTimestamp);
bool isBunkerModeWasSetBefore = isBunkerModeActive();
// on bunker mode state change
if (_isBunkerModeNow != isBunkerModeWasSetBefore) {
// write previous timestamp to enable bunker or max uint to disable
if (_isBunkerModeNow) {
BUNKER_MODE_SINCE_TIMESTAMP_POSITION.setStorageUint256(_bunkerStartTimestamp);
emit BunkerModeEnabled(_bunkerStartTimestamp);
} else {
BUNKER_MODE_SINCE_TIMESTAMP_POSITION.setStorageUint256(BUNKER_MODE_DISABLED_TIMESTAMP);
emit BunkerModeDisabled();
}
}
}
/// @notice Check if bunker mode is active
function isBunkerModeActive() public view returns (bool) {
return bunkerModeSinceTimestamp() < BUNKER_MODE_DISABLED_TIMESTAMP;
}
/// @notice Get bunker mode activation timestamp
/// @dev returns `BUNKER_MODE_DISABLED_TIMESTAMP` if bunker mode is disable (i.e., protocol in turbo mode)
function bunkerModeSinceTimestamp() public view returns (uint256) {
return BUNKER_MODE_SINCE_TIMESTAMP_POSITION.getStorageUint256();
}
/// @notice Should emit ERC721 Transfer event in the inheriting contract
function _emitTransfer(address from, address to, uint256 _requestId) internal virtual;
/// @dev internal initialization helper. Doesn't check provided addresses intentionally
function _initialize(address _admin) internal {
_initializeQueue();
_pauseFor(PAUSE_INFINITELY);
_initializeContractVersionTo(1);
_grantRole(DEFAULT_ADMIN_ROLE, _admin);
BUNKER_MODE_SINCE_TIMESTAMP_POSITION.setStorageUint256(BUNKER_MODE_DISABLED_TIMESTAMP);
emit InitializedV1(_admin);
}
function _requestWithdrawal(uint256 _amountOfStETH, address _owner) internal returns (uint256 requestId) {
STETH.transferFrom(msg.sender, address(this), _amountOfStETH);
uint256 amountOfShares = STETH.getSharesByPooledEth(_amountOfStETH);
requestId = _enqueue(uint128(_amountOfStETH), uint128(amountOfShares), _owner);
_emitTransfer(address(0), _owner, requestId);
}
function _requestWithdrawalWstETH(uint256 _amountOfWstETH, address _owner) internal returns (uint256 requestId) {
WSTETH.transferFrom(msg.sender, address(this), _amountOfWstETH);
uint256 amountOfStETH = WSTETH.unwrap(_amountOfWstETH);
_checkWithdrawalRequestAmount(amountOfStETH);
uint256 amountOfShares = STETH.getSharesByPooledEth(amountOfStETH);
requestId = _enqueue(uint128(amountOfStETH), uint128(amountOfShares), _owner);
_emitTransfer(address(0), _owner, requestId);
}
function _checkWithdrawalRequestAmount(uint256 _amountOfStETH) internal pure {
if (_amountOfStETH < MIN_STETH_WITHDRAWAL_AMOUNT) {
revert RequestAmountTooSmall(_amountOfStETH);
}
if (_amountOfStETH > MAX_STETH_WITHDRAWAL_AMOUNT) {
revert RequestAmountTooLarge(_amountOfStETH);
}
}
/// @notice returns claimable ether under the request. Returns 0 if request is not finalized or claimed
function _getClaimableEther(uint256 _requestId, uint256 _hint) internal view returns (uint256) {
if (_requestId == 0 || _requestId > getLastRequestId()) revert InvalidRequestId(_requestId);
if (_requestId > getLastFinalizedRequestId()) return 0;
WithdrawalRequest storage request = _getQueue()[_requestId];
if (request.claimed) return 0;
return _calculateClaimableEther(request, _requestId, _hint);
}
}