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: WithdrawalRequestNFT #560

Merged
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
40 changes: 35 additions & 5 deletions contracts/0.8.9/WithdrawalQueue.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ pragma solidity 0.8.9;
import {WithdrawalQueueBase} from "./WithdrawalQueueBase.sol";

import {IERC20} from "@openzeppelin/contracts-v4.4/token/ERC20/IERC20.sol";
import {IERC721} from "@openzeppelin/contracts-v4.4/token/ERC721/IERC721.sol";
import {IERC20Permit} from "@openzeppelin/contracts-v4.4/token/ERC20/extensions/draft-IERC20Permit.sol";
import {SafeCast} from "@openzeppelin/contracts-v4.4/utils/math/SafeCast.sol";
import {SafeERC20} from "@openzeppelin/contracts-v4.4/token/ERC20/utils/SafeERC20.sol";
Expand Down Expand Up @@ -54,7 +53,7 @@ interface IWstETH is IERC20, IERC20Permit {
* @title A contract for handling stETH withdrawal request queue within the Lido protocol
* @author folkyatina
*/
contract WithdrawalQueue is AccessControlEnumerable, WithdrawalQueueBase, Versioned {
abstract contract WithdrawalQueue is AccessControlEnumerable, WithdrawalQueueBase, Versioned {
using SafeCast for uint256;
using SafeERC20 for IWstETH;
using SafeERC20 for IStETH;
Expand Down Expand Up @@ -305,10 +304,35 @@ contract WithdrawalQueue is AccessControlEnumerable, WithdrawalQueueBase, Versio
/// @param _claimWithdrawalInputs list of withdrawal request ids and hints to claim
function claimWithdrawals(ClaimWithdrawalInput[] calldata _claimWithdrawalInputs) external {
for (uint256 i = 0; i < _claimWithdrawalInputs.length; ++i) {
claimWithdrawal(_claimWithdrawalInputs[i].requestId, _claimWithdrawalInputs[i].hint);
_claimWithdrawalTo(_claimWithdrawalInputs[i].requestId, _claimWithdrawalInputs[i].hint, msg.sender);

_emitTransfer(msg.sender, address(0), _claimWithdrawalInputs[i].requestId);
}
}

/// @notice Claim `_requestId` request and transfer locked ether to the owner
/// @param _requestId request id to claim
/// @param _hint hint for checkpoint index to avoid extensive search over the checkpointHistory.
/// Can be found with `findClaimHint()` or `findClaimHintUnbounded()`
/// @param _recipient address where
function claimWithdrawalTo(uint256 _requestId, uint256 _hint, address _recipient) external {
_claimWithdrawalTo(_requestId, _hint, _recipient);

_emitTransfer(msg.sender, address(0), _requestId);
}

/**
* @notice Claim `_requestId` request and transfer locked ether to the owner
* @param _requestId request id to claim
* @dev will use `findClaimHintUnbounded()` to find a hint, what can lead to OOG
* Prefer `claimWithdrawal(uint256 _requestId, uint256 _hint)` to save gas
*/
function claimWithdrawal(uint256 _requestId) external {
_claimWithdrawalTo(_requestId, findClaimHintUnbounded(_requestId), 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]`
/// @param _requestIds ids of the requests sorted in the ascending order to get hints for
Expand Down Expand Up @@ -387,6 +411,8 @@ contract WithdrawalQueue is AccessControlEnumerable, WithdrawalQueueBase, Versio
return BUNKER_MODE_SINCE_TIMESTAMP_POSITION.getStorageUint256();
}

function _emitTransfer(address from, address to, uint256 _requestId) internal virtual;

/// @dev internal initialization helper. Doesn't check provided addresses intentionally
function _initialize(address _admin, address _pauser, address _resumer, address _finalizer) internal {
_initializeQueue();
Expand All @@ -409,7 +435,9 @@ contract WithdrawalQueue is AccessControlEnumerable, WithdrawalQueueBase, Versio

uint256 amountOfShares = STETH.getSharesByPooledEth(_amountOfStETH);

return _enqueue(_amountOfStETH.toUint128(), amountOfShares.toUint128(), _owner);
requestId = _enqueue(_amountOfStETH.toUint128(), amountOfShares.toUint128(), _owner);

_emitTransfer(address(0), _owner, requestId);
}

function _requestWithdrawalWstETH(uint256 _amountOfWstETH, address _owner) internal returns (uint256 requestId) {
Expand All @@ -418,7 +446,9 @@ contract WithdrawalQueue is AccessControlEnumerable, WithdrawalQueueBase, Versio

uint256 amountOfShares = STETH.getSharesByPooledEth(amountOfStETH);

return _enqueue(amountOfStETH.toUint128(), amountOfShares.toUint128(), _owner);
requestId = _enqueue(amountOfStETH.toUint128(), amountOfShares.toUint128(), _owner);

_emitTransfer(address(0), _owner, requestId);
}

function _checkWithdrawalRequestInput(uint256 _amountOfStETH, address _owner) internal view returns (address) {
Expand Down
62 changes: 15 additions & 47 deletions contracts/0.8.9/WithdrawalQueueBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,13 @@ abstract contract WithdrawalQueueBase {
uint256 indexed from, uint256 indexed to, uint256 amountOfETHLocked, uint256 sharesBurned, uint256 timestamp
);
event WithdrawalClaimed(
uint256 indexed requestId, address indexed receiver, address initiator, uint256 amountOfETH
uint256 indexed requestId, address owner, address indexed receiver, uint256 amountOfETH
folkyatina marked this conversation as resolved.
Show resolved Hide resolved
);
event WithdrawalRequestTransferred(uint256 indexed requestId, address newOwner, address oldOwner);


error ZeroAmountOfETH();
error ZeroShareRate();
error ZeroTimestamp();
error ZeroRecipient();
error InvalidOwner(address _owner, address _sender);
error InvalidOwnerAddress(address _owner);
error InvalidRequestId(uint256 _requestId);
Expand All @@ -93,7 +93,7 @@ abstract contract WithdrawalQueueBase {
error CantSendValueRecipientMayHaveReverted();
error SafeCastValueDoesNotFit(uint16 maximumBitSize);

/// @notice id of the last request. Equals to the length of the queue
/// @notice id of the last request.
function getLastRequestId() public view returns (uint256) {
return LAST_REQUEST_ID_POSITION.getStorageUint256();
}
Expand Down Expand Up @@ -133,7 +133,7 @@ abstract contract WithdrawalQueueBase {
* 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 _getRequestByOwner()[_owner].values();
return _getRequestsByOwner()[_owner].values();
}

struct WithdrawalRequestStatus {
Expand Down Expand Up @@ -275,14 +275,17 @@ abstract contract WithdrawalQueueBase {
* @param _hint hint for checkpoint index to avoid extensive search over the checkpointHistory.
* Can be found with `findClaimHint()` or `findClaimHintUnbounded()`
*/
function claimWithdrawal(uint256 _requestId, uint256 _hint) public {
function _claimWithdrawalTo(uint256 _requestId, uint256 _hint, address _recipient) internal {
if (_hint == 0) revert InvalidHint(_hint);

if (_requestId > getLastFinalizedRequestId()) revert RequestNotFinalized(_requestId);
uint256 lastCheckpointIndex = getLastCheckpointIndex();
if (_hint > lastCheckpointIndex) revert InvalidHint(_hint);

WithdrawalRequest storage request = _getQueue()[_requestId];
if (request.claimed) revert RequestAlreadyClaimed(_requestId);
if (msg.sender != request.owner) revert InvalidOwner(request.owner, msg.sender);
if (_recipient == address(0)) _recipient = request.owner;

request.claimed = true;

Expand All @@ -303,44 +306,9 @@ abstract contract WithdrawalQueueBase {

_setLockedEtherAmount(getLockedEtherAmount() - ethWithDiscount);

_sendValue(request.owner, ethWithDiscount);

emit WithdrawalClaimed(_requestId, request.owner, msg.sender, ethWithDiscount);
}

/**
* @notice Claim `_requestId` request and transfer locked ether to the owner
* @param _requestId request id to claim
* @dev will use `findClaimHintUnbounded()` to find a hint, what can lead to OOG
* Prefer `claimWithdrawal(uint256 _requestId, uint256 _hint)` to save gas
*/
function claimWithdrawal(uint256 _requestId) external {
claimWithdrawal(_requestId, findClaimHintUnbounded(_requestId));
}

/**
* @notice Transfer the right to claim withdrawal request to `_newRecipient`
* @dev should be called by the old recipient
* @param _requestId id of the request subject to change
* @param _newOwner new owner address for withdrawal request
*/
function transfer(uint256 _requestId, address _newOwner) external {
if (_newOwner == address(0)) revert InvalidOwnerAddress(_newOwner);
if (_newOwner == msg.sender) revert InvalidOwnerAddress(_newOwner);
if (_requestId == 0) revert InvalidRequestId(_requestId);
if (_requestId > getLastRequestId()) revert InvalidRequestId(_requestId);

WithdrawalRequest storage request = _getQueue()[_requestId];

if (request.owner != msg.sender) revert InvalidOwner(request.owner, msg.sender);
if (request.claimed) revert RequestAlreadyClaimed(_requestId);

request.owner = payable(_newOwner);

_getRequestByOwner()[_newOwner].add(_requestId);
_getRequestByOwner()[msg.sender].remove(_requestId);
_sendValue(payable(_recipient), ethWithDiscount);

emit WithdrawalRequestTransferred(_requestId, _newOwner, msg.sender);
emit WithdrawalClaimed(_requestId, msg.sender, _recipient, ethWithDiscount);
}

/**
Expand Down Expand Up @@ -457,7 +425,7 @@ abstract contract WithdrawalQueueBase {
_setLastRequestId(requestId);
_getQueue()[requestId] =
WithdrawalRequest(cumulativeStETH, cumulativeShares, payable(_owner), uint64(block.number), false);
_getRequestByOwner()[_owner].add(requestId);
_getRequestsByOwner()[_owner].add(requestId);

emit WithdrawalRequested(requestId, msg.sender, _owner, _amountOfStETH, _amountOfShares);
}
Expand Down Expand Up @@ -535,14 +503,14 @@ abstract contract WithdrawalQueueBase {
}
}

function _getRequestByOwner()
function _getRequestsByOwner()
internal
pure
returns (mapping(address => EnumerableSet.UintSet) storage requestsByRecipient)
returns (mapping(address => EnumerableSet.UintSet) storage requestsByOwner)
{
bytes32 position = REQUEST_BY_OWNER_POSITION;
assembly {
requestsByRecipient.slot := position
requestsByOwner.slot := position
}
}

Expand Down
Loading