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 all 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
174 changes: 74 additions & 100 deletions contracts/0.8.9/WithdrawalQueue.sol

Large diffs are not rendered by default.

343 changes: 152 additions & 191 deletions contracts/0.8.9/WithdrawalQueueBase.sol

Large diffs are not rendered by default.

237 changes: 237 additions & 0 deletions contracts/0.8.9/WithdrawalRequestNFT.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
// 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/IERC165.sol";

import {Strings} from "@openzeppelin/contracts-v4.4/utils/Strings.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 {AccessControlEnumerable} from "./utils/access/AccessControlEnumerable.sol";
import {UnstructuredRefStorage} from "./lib/UnstructuredRefStorage.sol";

/// @title NFT implementation on top of {WithdrawalQueue}
/// NFT is minted on every request and burned on claim
///
/// @author psirex, folkyatina
contract WithdrawalRequestNFT is IERC721, WithdrawalQueue {
using Strings for uint256;
using Address for address;
using EnumerableSet for EnumerableSet.UintSet;
using UnstructuredRefStorage for bytes32;

bytes32 internal constant TOKEN_APPROVALS_POSITION = keccak256("lido.WithdrawalNFT.tokenApprovals");
bytes32 internal constant OPERATOR_APPROVALS = keccak256("lido.WithdrawalNFT.operatorApprovals");

error ApprovalToOwner();
error NotOwnerOrApprovedForAll(address sender);
error NotOwnerOrApproved(address sender);
error TransferFromIncorrectOwner(address from, address realOwner);
error TransferToZeroAddress();
error TransferFromZeroAddress();
error TransferToNonIERC721Receiver(address);

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

/// See {IERC165-supportsInterface}.
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 override returns (uint256) {
if (_owner == address(0)) revert InvalidOwnerAddress(_owner);
return _getRequestsByOwner()[_owner].length();
}

/// @dev See {IERC721-ownerOf}.
function ownerOf(uint256 _requestId) public view override returns (address) {
if (_requestId == 0 || _requestId > getLastRequestId()) revert InvalidRequestId(_requestId);

WithdrawalRequest memory request = _getQueue()[_requestId];
if (request.claimed) revert RequestAlreadyClaimed(_requestId);

return request.owner;
}

/// @dev See {IERC721-approve}.
function approve(address _to, uint256 _requestId) public override {
address owner = ownerOf(_requestId);
if (_to == owner) revert ApprovalToOwner();
if (msg.sender != owner && !isApprovedForAll(owner, msg.sender)) revert NotOwnerOrApprovedForAll(msg.sender);

_approve(_to, _requestId);
}

/// @dev See {IERC721-getApproved}.
function getApproved(uint256 _requestId) public view override returns (address) {
if (!_existsAndNotClaimed(_requestId)) revert InvalidRequestId(_requestId);

return _getTokenApprovals()[_requestId];
}

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

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

/// @dev See {IERC721-safeTransferFrom}.
function safeTransferFrom(address _from, address _to, uint256 _requestId) public override {
safeTransferFrom(_from, _to, _requestId, "");
}

/// @dev See {IERC721-safeTransferFrom}.
function safeTransferFrom(address _from, address _to, uint256 _requestId, bytes memory _data) public override {
if (!_isApprovedOrOwner(msg.sender, _requestId)) revert NotOwnerOrApproved(msg.sender);
_safeTransfer(_from, _to, _requestId, _data);
}

/// @dev See {IERC721-transferFrom}.
function transferFrom(address _from, address _to, uint256 _requestId) public override {
if (!_isApprovedOrOwner(msg.sender, _requestId)) revert NotOwnerOrApproved(msg.sender);
_transfer(_from, _to, _requestId);

emit Transfer(_from, _to, _requestId);
}

/// @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 _requestId) internal {
if (_from == address(0)) revert TransferFromZeroAddress();
if (_to == address(0)) revert TransferToZeroAddress();
if (_requestId == 0 || _requestId > getLastRequestId()) revert InvalidRequestId(_requestId);

WithdrawalRequest storage request = _getQueue()[_requestId];

if (_from != request.owner) revert TransferFromIncorrectOwner(_from, request.owner);
if (request.claimed) revert RequestAlreadyClaimed(_requestId);

delete _getTokenApprovals()[_requestId];
request.owner = payable(_to);

_getRequestsByOwner()[_to].add(_requestId);
_getRequestsByOwner()[_from].remove(_requestId);
}

/// @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 _requestId, bytes memory _data) internal {
_transfer(_from, _to, _requestId);
require(
_checkOnERC721Received(_from, _to, _requestId, _data), "ERC721: transfer to non ERC721Receiver implementer"
);

emit Transfer(_from, _to, _requestId);
}

/// @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 _requestId 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 _requestId, bytes memory _data)
private
returns (bool)
{
if (_to.isContract()) {
try IERC721Receiver(_to).onERC721Received(msg.sender, _from, _requestId, _data) returns (bytes4 retval) {
return retval == IERC721Receiver.onERC721Received.selector;
} catch (bytes memory reason) {
if (reason.length == 0) {
revert TransferToNonIERC721Receiver(_to);
} 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 _requestId) internal view returns (bool) {
address owner = ownerOf(_requestId);
return (_spender == owner || isApprovedForAll(owner, _spender) || getApproved(_requestId) == _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 _requestId) internal override {
emit Transfer(_from, _to, _requestId);
}

/// @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 _requestId) internal virtual {
_getTokenApprovals()[_requestId] = _to;
emit Approval(ownerOf(_requestId), _to, _requestId);
}

/// @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) {
return TOKEN_APPROVALS_POSITION.storageMapUint256Address();
}

function _getOperatorApprovals() internal pure returns (mapping(address => mapping(address => bool)) storage) {
return OPERATOR_APPROVALS.storageMapAddressMapAddressBool();
}
}
12 changes: 12 additions & 0 deletions contracts/0.8.9/lib/UnstructuredRefStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,16 @@ library UnstructuredRefStorage {
) {
assembly { result.slot := _position }
}

function storageMapUint256Address(bytes32 _position) internal pure returns (
mapping(uint256 => address) storage result
) {
assembly { result.slot := _position }
}

function storageMapAddressMapAddressBool(bytes32 _position) internal pure returns (
mapping(address => mapping(address => bool)) storage result
) {
assembly { result.slot := _position }
}
}
2 changes: 1 addition & 1 deletion contracts/0.8.9/utils/PausableUntil.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ contract PausableUntil {
using UnstructuredStorage for bytes32;

/// Contract resume/pause control storage slot
bytes32 public constant RESUME_SINCE_TIMESTAMP_POSITION = keccak256("lido.PausableUntil.resumeSinceTimestamp");
bytes32 internal constant RESUME_SINCE_TIMESTAMP_POSITION = keccak256("lido.PausableUntil.resumeSinceTimestamp");
/// Special value for the infinite pause
uint256 public constant PAUSE_INFINITELY = type(uint256).max;

Expand Down
Loading