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 16 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
234 changes: 234 additions & 0 deletions contracts/0.8.9/WithdrawalNFT.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
// SPDX-FileCopyrightText: 2023 Lido <[email protected]>, OpenZeppelin
// SPDX-License-Identifier: GPL-3.0

/* See contracts/COMPILERS.md */
pragma solidity 0.8.9;

import {IERC721} from "@openzeppelin/contracts-v4.4/token/ERC721/IERC721.sol";
import {IERC721Receiver} from "@openzeppelin/contracts-v4.4/token/ERC721/IERC721Receiver.sol";
import {IERC165} from "@openzeppelin/contracts-v4.4/utils/introspection/ERC165.sol";

import {Strings} from "@openzeppelin/contracts-v4.4/utils/Strings.sol";
import {ERC165} from "@openzeppelin/contracts-v4.4/utils/introspection/ERC165.sol";
import {EnumerableSet} from "@openzeppelin/contracts-v4.4/utils/structs/EnumerableSet.sol";
import {Address} from "@openzeppelin/contracts-v4.4/utils/Address.sol";

import {IWstETH, WithdrawalQueue} from "./WithdrawalQueue.sol";
import {IAccessControlEnumerable, AccessControlEnumerable} from "./utils/access/AccessControlEnumerable.sol";
folkyatina marked this conversation as resolved.
Show resolved Hide resolved

/// @title NFT implementation around {WithdrawalRequest}
folkyatina marked this conversation as resolved.
Show resolved Hide resolved
/// @author psirex, folkyatina
contract WithdrawalNFT is IERC721, WithdrawalQueue {
using Strings for uint256;
using Address for address;
using EnumerableSet for EnumerableSet.UintSet;

bytes32 public constant TOKEN_APPROVALS_POSITION = keccak256("lido.WithdrawalNFT.tokenApprovals");
folkyatina marked this conversation as resolved.
Show resolved Hide resolved
bytes32 public constant OPERATOR_APPROVALS = keccak256("lido.WithdrawalNFT.operatorApprovals");

/// @param _wstETH address of WstETH contract
constructor(address _wstETH) WithdrawalQueue(IWstETH(_wstETH)) {}

function supportsInterface(bytes4 interfaceId)
public
view
virtual
override(IERC165, AccessControlEnumerable)
returns (bool)
{
return interfaceId == type(IERC721).interfaceId || super.supportsInterface(interfaceId);
}

/// @dev See {IERC721-balanceOf}.
function balanceOf(address _owner) public view returns (uint256) {
if (_owner == address(0)) revert InvalidOwnerAddress(_owner);
return _getRequestsByOwner()[_owner].length();
}

/// @dev See {IERC721-ownerOf}.
function ownerOf(uint256 _tokenId) public view returns (address) {
if (_tokenId == 0 || _tokenId > getLastRequestId()) revert InvalidRequestId(_tokenId);
return _getQueue()[_tokenId].owner;
}

/// @dev See {IERC721-approve}.
function approve(address to, uint256 tokenId) public {
folkyatina marked this conversation as resolved.
Show resolved Hide resolved
address owner = ownerOf(tokenId);
require(to != owner, "ERC721: approval to current owner");

require(
msg.sender == owner || isApprovedForAll(owner, msg.sender),
"ERC721: approve caller is not owner nor approved for all"
);

_approve(to, tokenId);
}

/// @dev See {IERC721-getApproved}.
function getApproved(uint256 tokenId) public view returns (address) {
require(_existsAndNotClaimed(tokenId), "ERC721: approved query for nonexistent or claimed token");

return _getTokenApprovals()[tokenId];
}

/// @dev See {IERC721-setApprovalForAll}.
function setApprovalForAll(address operator, bool approved) public {
_setApprovalForAll(msg.sender, operator, approved);
}

/// @dev See {IERC721-isApprovedForAll}.
function isApprovedForAll(address owner, address operator) public view returns (bool) {
return _getOperatorApprovals()[owner][operator];
}

/// @dev See {IERC721-safeTransferFrom}.
function safeTransferFrom(address from, address to, uint256 tokenId) public override {
safeTransferFrom(from, to, tokenId, "");
}

/// @dev See {IERC721-safeTransferFrom}.
function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public override {
require(_isApprovedOrOwner(msg.sender, tokenId), "ERC721: caller is not token owner or approved");
_safeTransfer(from, to, tokenId, data);
}

/// @dev See {IERC721-transferFrom}.
function transferFrom(address from, address to, uint256 tokenId) public override {
require(_isApprovedOrOwner(msg.sender, tokenId), "ERC721: caller is not token owner or approved");
_transfer(from, to, tokenId);

emit Transfer(from, to, tokenId);
}

/// @dev Transfers `tokenId` from `from` to `to`.
/// As opposed to {transferFrom}, this imposes no restrictions on msg.sender.
///
/// Requirements:
///
/// - `from` cannot be the zero address.
/// - `to` cannot be the zero address.
/// - `tokenId` token must be owned by `from`.
function _transfer(address from, address to, uint256 tokenId) internal {
require(from != address(0), "ERC721: transfer from zero address");
require(to != address(0), "ERC721: transfer to the zero address");
folkyatina marked this conversation as resolved.
Show resolved Hide resolved
require(tokenId > 0 && tokenId <= getLastRequestId(), "ERC721: transfer nonexistent token");

WithdrawalRequest storage request = _getQueue()[tokenId];

require(request.owner == from, "ERC721: transfer from incorrect owner");

if (request.claimed) revert RequestAlreadyClaimed(tokenId);

delete _getTokenApprovals()[tokenId];
request.owner = payable(to);

_getRequestsByOwner()[to].add(tokenId);
_getRequestsByOwner()[from].remove(tokenId);
}

/// @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients
/// are aware of the ERC721 protocol to prevent tokens from being forever locked.
/// `data` is additional data, it has no specified format and it is sent in call to `to`.
///
/// Requirements:
///
/// - `from` cannot be the zero address.
/// - `to` cannot be the zero address.
/// - `tokenId` token must exist and be owned by `from`.
/// - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
///
/// Emits a {Transfer} event.
function _safeTransfer(address from, address to, uint256 tokenId, bytes memory data) internal {
_transfer(from, to, tokenId);
require(_checkOnERC721Received(from, to, tokenId, data), "ERC721: transfer to non ERC721Receiver implementer");

emit Transfer(from, to, tokenId);
}

/// @dev Internal function to invoke {IERC721Receiver-onERC721Received} on a target address.
/// The call is not executed if the target address is not a contract.
///
/// @param from address representing the previous owner of the given token ID
/// @param to target address that will receive the tokens
/// @param tokenId uint256 ID of the token to be transferred
/// @param data bytes optional data to send along with the call
/// @return bool whether the call correctly returned the expected magic value
function _checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory data)
private
returns (bool)
{
if (to.isContract()) {
try IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, data) returns (bytes4 retval) {
return retval == IERC721Receiver.onERC721Received.selector;
} catch (bytes memory reason) {
if (reason.length == 0) {
revert("ERC721: transfer to non ERC721Receiver implementer");
} else {
/// @solidity memory-safe-assembly
assembly {
revert(add(32, reason), mload(reason))
}
}
}
} else {
return true;
}
}

/// @dev Returns whether `spender` is allowed to manage `tokenId`.
///
/// Requirements:
///
/// - `tokenId` must exist.
function _isApprovedOrOwner(address spender, uint256 tokenId) internal view returns (bool) {
address owner = ownerOf(tokenId);
return (spender == owner || isApprovedForAll(owner, spender) || getApproved(tokenId) == spender);
}

//
// Internal getters and setters
//

/// @dev a little crutch to emit { Transfer } on request and on claim like ERC721 states
function _emitTransfer(address from, address to, uint256 tokenId) internal override {
emit Transfer(from, to, tokenId);
}

/// @dev Returns whether `_requestId` exists and not claimed.
function _existsAndNotClaimed(uint256 _requestId) internal view returns (bool) {
return _requestId > 0 && _requestId <= getLastRequestId() && !_getQueue()[_requestId].claimed;
}

/// @dev Approve `to` to operate on `tokenId`
/// Emits a {Approval} event.
function _approve(address to, uint256 tokenId) internal virtual {
_getTokenApprovals()[tokenId] = to;
emit Approval(ownerOf(tokenId), to, tokenId);
}

/// @dev Approve `operator` to operate on all of `owner` tokens
/// Emits a {ApprovalForAll} event.
function _setApprovalForAll(address owner, address operator, bool approved) internal virtual {
require(owner != operator, "ERC721: approve to caller");
_getOperatorApprovals()[owner][operator] = approved;
emit ApprovalForAll(owner, operator, approved);
}

function _getTokenApprovals() internal pure returns (mapping(uint256 => address) storage tokenApprovals) {
folkyatina marked this conversation as resolved.
Show resolved Hide resolved
bytes32 position = TOKEN_APPROVALS_POSITION;
assembly {
tokenApprovals.slot := position
}
}

function _getOperatorApprovals()
internal
pure
returns (mapping(address => mapping(address => bool)) storage operatorApprovals)
{
bytes32 position = TOKEN_APPROVALS_POSITION;
assembly {
operatorApprovals.slot := position
}
}
}
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
Loading