diff --git a/src/contracts/utils/Rescuable721.sol b/src/contracts/utils/Rescuable721.sol new file mode 100644 index 0000000..df59a71 --- /dev/null +++ b/src/contracts/utils/Rescuable721.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.8; + +import {IRescuable721, IERC721} from './interfaces/IRescuable721.sol'; +import {Rescuable} from './Rescuable.sol'; + +/** + * @title Rescuable721 + * @author defijesus.eth + * @notice abstract contract that extend Rescuable with the methods to rescue ERC721 tokens from a contract + */ +abstract contract Rescuable721 is Rescuable, IRescuable721 { + + /// @inheritdoc IRescuable721 + function emergency721TokenTransfer( + address erc721Token, + address to, + uint256 tokenId + ) external virtual onlyRescueGuardian { + IERC721(erc721Token).transferFrom(address(this), to, tokenId); + + emit ERC721Rescued(msg.sender, erc721Token, to, tokenId); + } +} diff --git a/src/contracts/utils/interfaces/IRescuable721.sol b/src/contracts/utils/interfaces/IRescuable721.sol new file mode 100644 index 0000000..8a66c99 --- /dev/null +++ b/src/contracts/utils/interfaces/IRescuable721.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.8; + +/** + * @title IRescuable721 + * @author defijesus.eth + * @notice interface containing the objects, events and methods definitions of the Rescuable721 contract + */ +interface IRescuable721 { + /** + * @notice emitted when erc721 tokens get rescued + * @param caller address that triggers the rescue + * @param token address of the rescued token + * @param to address that will receive the rescued tokens + * @param tokenId the id of the token rescued + */ + event ERC721Rescued( + address indexed caller, + address indexed token, + address indexed to, + uint256 tokenId + ); + + /** + * @notice method called to rescue a ERC721 token sent erroneously to the contract. Only callable by owner + * @param erc721Token address of the token to rescue + * @param to address to send the token + * @param tokenId of token to rescue + */ + function emergency721TokenTransfer(address erc721Token, address to, uint256 tokenId) external; +} + +interface IERC721 { + function transferFrom(address from, address to, uint256 tokenId) external; +} diff --git a/src/mocks/ERC721.sol b/src/mocks/ERC721.sol new file mode 100644 index 0000000..ec5281b --- /dev/null +++ b/src/mocks/ERC721.sol @@ -0,0 +1,937 @@ +// SPDX-License-Identifier: MIT +// taken from https://github.com/Vectorized/solady/commit/496b9aceb690f2681648f43929f632b8403bfd7f + +pragma solidity ^0.8.4; + +/// @notice Simple ERC721 implementation with storage hitchhiking. +/// @author Solady (https://github.com/vectorized/solady/blob/main/src/tokens/ERC721.sol) +/// @author Modified from Solmate (https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC721.sol) +/// @author Modified from OpenZeppelin (https://github.com/OpenZeppelin/openzeppelin-contracts/tree/master/contracts/token/ERC721/ERC721.sol) +/// +/// @dev Note: +/// - The ERC721 standard allows for self-approvals. +/// For performance, this implementation WILL NOT revert for such actions. +/// Please add any checks with overrides if desired. +/// - For performance, methods are made payable where permitted by the ERC721 standard. +/// - The `safeTransfer` functions use the identity precompile (0x4) +/// to copy memory internally. +/// +/// If you are overriding: +/// - NEVER violate the ERC721 invariant: +/// the balance of an owner MUST always be equal to their number of ownership slots. +/// The transfer functions do not have an underflow guard for user token balances. +/// - Make sure all variables written to storage are properly cleaned +// (e.g. the bool value for `isApprovedForAll` MUST be either 1 or 0 under the hood). +/// - Check that the overridden function is actually used in the function you want to +/// change the behavior of. Much of the code has been manually inlined for performance. +abstract contract ERC721 { + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CONSTANTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev An account can hold up to 4294967295 tokens. + uint256 internal constant _MAX_ACCOUNT_BALANCE = 0xffffffff; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CUSTOM ERRORS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Only the token owner or an approved account can manage the token. + error NotOwnerNorApproved(); + + /// @dev The token does not exist. + error TokenDoesNotExist(); + + /// @dev The token already exists. + error TokenAlreadyExists(); + + /// @dev Cannot query the balance for the zero address. + error BalanceQueryForZeroAddress(); + + /// @dev Cannot mint or transfer to the zero address. + error TransferToZeroAddress(); + + /// @dev The token must be owned by `from`. + error TransferFromIncorrectOwner(); + + /// @dev The recipient's balance has overflowed. + error AccountBalanceOverflow(); + + /// @dev Cannot safely transfer to a contract that does not implement + /// the ERC721Receiver interface. + error TransferToNonERC721ReceiverImplementer(); + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* EVENTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Emitted when token `id` is transferred from `from` to `to`. + event Transfer(address indexed from, address indexed to, uint256 indexed id); + + /// @dev Emitted when `owner` enables `account` to manage the `id` token. + event Approval(address indexed owner, address indexed account, uint256 indexed id); + + /// @dev Emitted when `owner` enables or disables `operator` to manage all of their tokens. + event ApprovalForAll(address indexed owner, address indexed operator, bool isApproved); + + /// @dev `keccak256(bytes("Transfer(address,address,uint256)"))`. + uint256 private constant _TRANSFER_EVENT_SIGNATURE = + 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef; + + /// @dev `keccak256(bytes("Approval(address,address,uint256)"))`. + uint256 private constant _APPROVAL_EVENT_SIGNATURE = + 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925; + + /// @dev `keccak256(bytes("ApprovalForAll(address,address,bool)"))`. + uint256 private constant _APPROVAL_FOR_ALL_EVENT_SIGNATURE = + 0x17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c31; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* STORAGE */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev The ownership data slot of `id` is given by: + /// ``` + /// mstore(0x00, id) + /// mstore(0x1c, _ERC721_MASTER_SLOT_SEED) + /// let ownershipSlot := add(id, add(id, keccak256(0x00, 0x20))) + /// ``` + /// Bits Layout: + /// - [0..159] `addr` + /// - [160..255] `extraData` + /// + /// The approved address slot is given by: `add(1, ownershipSlot)`. + /// + /// See: https://notes.ethereum.org/%40vbuterin/verkle_tree_eip + /// + /// The balance slot of `owner` is given by: + /// ``` + /// mstore(0x1c, _ERC721_MASTER_SLOT_SEED) + /// mstore(0x00, owner) + /// let balanceSlot := keccak256(0x0c, 0x1c) + /// ``` + /// Bits Layout: + /// - [0..31] `balance` + /// - [32..255] `aux` + /// + /// The `operator` approval slot of `owner` is given by: + /// ``` + /// mstore(0x1c, or(_ERC721_MASTER_SLOT_SEED, operator)) + /// mstore(0x00, owner) + /// let operatorApprovalSlot := keccak256(0x0c, 0x30) + /// ``` + uint256 private constant _ERC721_MASTER_SLOT_SEED = 0x7d8825530a5a2e7a << 192; + + /// @dev Pre-shifted and pre-masked constant. + uint256 private constant _ERC721_MASTER_SLOT_SEED_MASKED = 0x0a5a2e7a00000000; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* ERC721 METADATA */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Returns the token collection name. + function name() public view virtual returns (string memory); + + /// @dev Returns the token collection symbol. + function symbol() public view virtual returns (string memory); + + /// @dev Returns the Uniform Resource Identifier (URI) for token `id`. + function tokenURI(uint256 id) public view virtual returns (string memory); + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* ERC721 */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Returns the owner of token `id`. + /// + /// Requirements: + /// - Token `id` must exist. + function ownerOf(uint256 id) public view virtual returns (address result) { + result = _ownerOf(id); + /// @solidity memory-safe-assembly + assembly { + if iszero(result) { + mstore(0x00, 0xceea21b6) // `TokenDoesNotExist()`. + revert(0x1c, 0x04) + } + } + } + + /// @dev Returns the number of tokens owned by `owner`. + /// + /// Requirements: + /// - `owner` must not be the zero address. + function balanceOf(address owner) public view virtual returns (uint256 result) { + /// @solidity memory-safe-assembly + assembly { + // Revert if the `owner` is the zero address. + if iszero(owner) { + mstore(0x00, 0x8f4eb604) // `BalanceQueryForZeroAddress()`. + revert(0x1c, 0x04) + } + mstore(0x1c, _ERC721_MASTER_SLOT_SEED) + mstore(0x00, owner) + result := and(sload(keccak256(0x0c, 0x1c)), _MAX_ACCOUNT_BALANCE) + } + } + + /// @dev Returns the account approved to manage token `id`. + /// + /// Requirements: + /// - Token `id` must exist. + function getApproved(uint256 id) public view virtual returns (address result) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, id) + mstore(0x1c, _ERC721_MASTER_SLOT_SEED) + let ownershipSlot := add(id, add(id, keccak256(0x00, 0x20))) + if iszero(shl(96, sload(ownershipSlot))) { + mstore(0x00, 0xceea21b6) // `TokenDoesNotExist()`. + revert(0x1c, 0x04) + } + result := sload(add(1, ownershipSlot)) + } + } + + /// @dev Sets `account` as the approved account to manage token `id`. + /// + /// Requirements: + /// - Token `id` must exist. + /// - The caller must be the owner of the token, + /// or an approved operator for the token owner. + /// + /// Emits an {Approval} event. + function approve(address account, uint256 id) public payable virtual { + _approve(msg.sender, account, id); + } + + /// @dev Returns whether `operator` is approved to manage the tokens of `owner`. + function isApprovedForAll(address owner, address operator) + public + view + virtual + returns (bool result) + { + /// @solidity memory-safe-assembly + assembly { + mstore(0x1c, operator) + mstore(0x08, _ERC721_MASTER_SLOT_SEED_MASKED) + mstore(0x00, owner) + result := sload(keccak256(0x0c, 0x30)) + } + } + + /// @dev Sets whether `operator` is approved to manage the tokens of the caller. + /// + /// Emits an {ApprovalForAll} event. + function setApprovalForAll(address operator, bool isApproved) public virtual { + /// @solidity memory-safe-assembly + assembly { + // Convert to 0 or 1. + isApproved := iszero(iszero(isApproved)) + // Update the `isApproved` for (`msg.sender`, `operator`). + mstore(0x1c, operator) + mstore(0x08, _ERC721_MASTER_SLOT_SEED_MASKED) + mstore(0x00, caller()) + sstore(keccak256(0x0c, 0x30), isApproved) + // Emit the {ApprovalForAll} event. + mstore(0x00, isApproved) + // forgefmt: disable-next-item + log3(0x00, 0x20, _APPROVAL_FOR_ALL_EVENT_SIGNATURE, caller(), shr(96, shl(96, operator))) + } + } + + /// @dev Transfers token `id` from `from` to `to`. + /// + /// Requirements: + /// + /// - Token `id` must exist. + /// - `from` must be the owner of the token. + /// - `to` cannot be the zero address. + /// - The caller must be the owner of the token, or be approved to manage the token. + /// + /// Emits a {Transfer} event. + function transferFrom(address from, address to, uint256 id) public payable virtual { + _beforeTokenTransfer(from, to, id); + /// @solidity memory-safe-assembly + assembly { + // Clear the upper 96 bits. + let bitmaskAddress := shr(96, not(0)) + from := and(bitmaskAddress, from) + to := and(bitmaskAddress, to) + // Load the ownership data. + mstore(0x00, id) + mstore(0x1c, or(_ERC721_MASTER_SLOT_SEED, caller())) + let ownershipSlot := add(id, add(id, keccak256(0x00, 0x20))) + let ownershipPacked := sload(ownershipSlot) + let owner := and(bitmaskAddress, ownershipPacked) + // Revert if the token does not exist, or if `from` is not the owner. + if iszero(mul(owner, eq(owner, from))) { + // `TokenDoesNotExist()`, `TransferFromIncorrectOwner()`. + mstore(shl(2, iszero(owner)), 0xceea21b6a1148100) + revert(0x1c, 0x04) + } + // Load, check, and update the token approval. + { + mstore(0x00, from) + let approvedAddress := sload(add(1, ownershipSlot)) + // Revert if the caller is not the owner, nor approved. + if iszero(or(eq(caller(), from), eq(caller(), approvedAddress))) { + if iszero(sload(keccak256(0x0c, 0x30))) { + mstore(0x00, 0x4b6e7f18) // `NotOwnerNorApproved()`. + revert(0x1c, 0x04) + } + } + // Delete the approved address if any. + if approvedAddress { sstore(add(1, ownershipSlot), 0) } + } + // Update with the new owner. + sstore(ownershipSlot, xor(ownershipPacked, xor(from, to))) + // Decrement the balance of `from`. + { + let fromBalanceSlot := keccak256(0x0c, 0x1c) + sstore(fromBalanceSlot, sub(sload(fromBalanceSlot), 1)) + } + // Increment the balance of `to`. + { + mstore(0x00, to) + let toBalanceSlot := keccak256(0x0c, 0x1c) + let toBalanceSlotPacked := add(sload(toBalanceSlot), 1) + // Revert if `to` is the zero address, or if the account balance overflows. + if iszero(mul(to, and(toBalanceSlotPacked, _MAX_ACCOUNT_BALANCE))) { + // `TransferToZeroAddress()`, `AccountBalanceOverflow()`. + mstore(shl(2, iszero(to)), 0xea553b3401336cea) + revert(0x1c, 0x04) + } + sstore(toBalanceSlot, toBalanceSlotPacked) + } + // Emit the {Transfer} event. + log4(codesize(), 0x00, _TRANSFER_EVENT_SIGNATURE, from, to, id) + } + _afterTokenTransfer(from, to, id); + } + + /// @dev Equivalent to `safeTransferFrom(from, to, id, "")`. + function safeTransferFrom(address from, address to, uint256 id) public payable virtual { + transferFrom(from, to, id); + if (_hasCode(to)) _checkOnERC721Received(from, to, id, ""); + } + + /// @dev Transfers token `id` from `from` to `to`. + /// + /// Requirements: + /// + /// - Token `id` must exist. + /// - `from` must be the owner of the token. + /// - `to` cannot be the zero address. + /// - The caller must be the owner of the token, or be approved to manage the token. + /// - If `to` refers to a smart contract, it must implement + /// {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. + /// + /// Emits a {Transfer} event. + function safeTransferFrom(address from, address to, uint256 id, bytes calldata data) + public + payable + virtual + { + transferFrom(from, to, id); + if (_hasCode(to)) _checkOnERC721Received(from, to, id, data); + } + + /// @dev Returns true if this contract implements the interface defined by `interfaceId`. + /// See: https://eips.ethereum.org/EIPS/eip-165 + /// This function call must use less than 30000 gas. + function supportsInterface(bytes4 interfaceId) public view virtual returns (bool result) { + /// @solidity memory-safe-assembly + assembly { + let s := shr(224, interfaceId) + // ERC165: 0x01ffc9a7, ERC721: 0x80ac58cd, ERC721Metadata: 0x5b5e139f. + result := or(or(eq(s, 0x01ffc9a7), eq(s, 0x80ac58cd)), eq(s, 0x5b5e139f)) + } + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* INTERNAL QUERY FUNCTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Returns if token `id` exists. + function _exists(uint256 id) internal view virtual returns (bool result) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, id) + mstore(0x1c, _ERC721_MASTER_SLOT_SEED) + result := iszero(iszero(shl(96, sload(add(id, add(id, keccak256(0x00, 0x20))))))) + } + } + + /// @dev Returns the owner of token `id`. + /// Returns the zero address instead of reverting if the token does not exist. + function _ownerOf(uint256 id) internal view virtual returns (address result) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, id) + mstore(0x1c, _ERC721_MASTER_SLOT_SEED) + result := shr(96, shl(96, sload(add(id, add(id, keccak256(0x00, 0x20)))))) + } + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* INTERNAL DATA HITCHHIKING FUNCTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + // For performance, no events are emitted for the hitchhiking setters. + // Please emit your own events if required. + + /// @dev Returns the auxiliary data for `owner`. + /// Minting, transferring, burning the tokens of `owner` will not change the auxiliary data. + /// Auxiliary data can be set for any address, even if it does not have any tokens. + function _getAux(address owner) internal view virtual returns (uint224 result) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x1c, _ERC721_MASTER_SLOT_SEED) + mstore(0x00, owner) + result := shr(32, sload(keccak256(0x0c, 0x1c))) + } + } + + /// @dev Set the auxiliary data for `owner` to `value`. + /// Minting, transferring, burning the tokens of `owner` will not change the auxiliary data. + /// Auxiliary data can be set for any address, even if it does not have any tokens. + function _setAux(address owner, uint224 value) internal virtual { + /// @solidity memory-safe-assembly + assembly { + mstore(0x1c, _ERC721_MASTER_SLOT_SEED) + mstore(0x00, owner) + let balanceSlot := keccak256(0x0c, 0x1c) + let packed := sload(balanceSlot) + sstore(balanceSlot, xor(packed, shl(32, xor(value, shr(32, packed))))) + } + } + + /// @dev Returns the extra data for token `id`. + /// Minting, transferring, burning a token will not change the extra data. + /// The extra data can be set on a non-existent token. + function _getExtraData(uint256 id) internal view virtual returns (uint96 result) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, id) + mstore(0x1c, _ERC721_MASTER_SLOT_SEED) + result := shr(160, sload(add(id, add(id, keccak256(0x00, 0x20))))) + } + } + + /// @dev Sets the extra data for token `id` to `value`. + /// Minting, transferring, burning a token will not change the extra data. + /// The extra data can be set on a non-existent token. + function _setExtraData(uint256 id, uint96 value) internal virtual { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, id) + mstore(0x1c, _ERC721_MASTER_SLOT_SEED) + let ownershipSlot := add(id, add(id, keccak256(0x00, 0x20))) + let packed := sload(ownershipSlot) + sstore(ownershipSlot, xor(packed, shl(160, xor(value, shr(160, packed))))) + } + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* INTERNAL MINT FUNCTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Mints token `id` to `to`. + /// + /// Requirements: + /// + /// - Token `id` must not exist. + /// - `to` cannot be the zero address. + /// + /// Emits a {Transfer} event. + function _mint(address to, uint256 id) internal virtual { + _beforeTokenTransfer(address(0), to, id); + /// @solidity memory-safe-assembly + assembly { + // Clear the upper 96 bits. + to := shr(96, shl(96, to)) + // Load the ownership data. + mstore(0x00, id) + mstore(0x1c, _ERC721_MASTER_SLOT_SEED) + let ownershipSlot := add(id, add(id, keccak256(0x00, 0x20))) + let ownershipPacked := sload(ownershipSlot) + // Revert if the token already exists. + if shl(96, ownershipPacked) { + mstore(0x00, 0xc991cbb1) // `TokenAlreadyExists()`. + revert(0x1c, 0x04) + } + // Update with the owner. + sstore(ownershipSlot, or(ownershipPacked, to)) + // Increment the balance of the owner. + { + mstore(0x00, to) + let balanceSlot := keccak256(0x0c, 0x1c) + let balanceSlotPacked := add(sload(balanceSlot), 1) + // Revert if `to` is the zero address, or if the account balance overflows. + if iszero(mul(to, and(balanceSlotPacked, _MAX_ACCOUNT_BALANCE))) { + // `TransferToZeroAddress()`, `AccountBalanceOverflow()`. + mstore(shl(2, iszero(to)), 0xea553b3401336cea) + revert(0x1c, 0x04) + } + sstore(balanceSlot, balanceSlotPacked) + } + // Emit the {Transfer} event. + log4(codesize(), 0x00, _TRANSFER_EVENT_SIGNATURE, 0, to, id) + } + _afterTokenTransfer(address(0), to, id); + } + + /// @dev Mints token `id` to `to`, and updates the extra data for token `id` to `value`. + /// Does NOT check if token `id` already exists (assumes `id` is auto-incrementing). + /// + /// Requirements: + /// + /// - `to` cannot be the zero address. + /// + /// Emits a {Transfer} event. + function _mintAndSetExtraDataUnchecked(address to, uint256 id, uint96 value) internal virtual { + _beforeTokenTransfer(address(0), to, id); + /// @solidity memory-safe-assembly + assembly { + // Clear the upper 96 bits. + to := shr(96, shl(96, to)) + // Update with the owner and extra data. + mstore(0x00, id) + mstore(0x1c, _ERC721_MASTER_SLOT_SEED) + sstore(add(id, add(id, keccak256(0x00, 0x20))), or(shl(160, value), to)) + // Increment the balance of the owner. + { + mstore(0x00, to) + let balanceSlot := keccak256(0x0c, 0x1c) + let balanceSlotPacked := add(sload(balanceSlot), 1) + // Revert if `to` is the zero address, or if the account balance overflows. + if iszero(mul(to, and(balanceSlotPacked, _MAX_ACCOUNT_BALANCE))) { + // `TransferToZeroAddress()`, `AccountBalanceOverflow()`. + mstore(shl(2, iszero(to)), 0xea553b3401336cea) + revert(0x1c, 0x04) + } + sstore(balanceSlot, balanceSlotPacked) + } + // Emit the {Transfer} event. + log4(codesize(), 0x00, _TRANSFER_EVENT_SIGNATURE, 0, to, id) + } + _afterTokenTransfer(address(0), to, id); + } + + /// @dev Equivalent to `_safeMint(to, id, "")`. + function _safeMint(address to, uint256 id) internal virtual { + _safeMint(to, id, ""); + } + + /// @dev Mints token `id` to `to`. + /// + /// Requirements: + /// + /// - Token `id` must not exist. + /// - `to` cannot be the zero address. + /// - If `to` refers to a smart contract, it must implement + /// {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. + /// + /// Emits a {Transfer} event. + function _safeMint(address to, uint256 id, bytes memory data) internal virtual { + _mint(to, id); + if (_hasCode(to)) _checkOnERC721Received(address(0), to, id, data); + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* INTERNAL BURN FUNCTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Equivalent to `_burn(address(0), id)`. + function _burn(uint256 id) internal virtual { + _burn(address(0), id); + } + + /// @dev Destroys token `id`, using `by`. + /// + /// Requirements: + /// + /// - Token `id` must exist. + /// - If `by` is not the zero address, + /// it must be the owner of the token, or be approved to manage the token. + /// + /// Emits a {Transfer} event. + function _burn(address by, uint256 id) internal virtual { + address owner = ownerOf(id); + _beforeTokenTransfer(owner, address(0), id); + /// @solidity memory-safe-assembly + assembly { + // Clear the upper 96 bits. + by := shr(96, shl(96, by)) + // Load the ownership data. + mstore(0x00, id) + mstore(0x1c, or(_ERC721_MASTER_SLOT_SEED, by)) + let ownershipSlot := add(id, add(id, keccak256(0x00, 0x20))) + let ownershipPacked := sload(ownershipSlot) + // Reload the owner in case it is changed in `_beforeTokenTransfer`. + owner := shr(96, shl(96, ownershipPacked)) + // Revert if the token does not exist. + if iszero(owner) { + mstore(0x00, 0xceea21b6) // `TokenDoesNotExist()`. + revert(0x1c, 0x04) + } + // Load and check the token approval. + { + mstore(0x00, owner) + let approvedAddress := sload(add(1, ownershipSlot)) + // If `by` is not the zero address, do the authorization check. + // Revert if the `by` is not the owner, nor approved. + if iszero(or(iszero(by), or(eq(by, owner), eq(by, approvedAddress)))) { + if iszero(sload(keccak256(0x0c, 0x30))) { + mstore(0x00, 0x4b6e7f18) // `NotOwnerNorApproved()`. + revert(0x1c, 0x04) + } + } + // Delete the approved address if any. + if approvedAddress { sstore(add(1, ownershipSlot), 0) } + } + // Clear the owner. + sstore(ownershipSlot, xor(ownershipPacked, owner)) + // Decrement the balance of `owner`. + { + let balanceSlot := keccak256(0x0c, 0x1c) + sstore(balanceSlot, sub(sload(balanceSlot), 1)) + } + // Emit the {Transfer} event. + log4(codesize(), 0x00, _TRANSFER_EVENT_SIGNATURE, owner, 0, id) + } + _afterTokenTransfer(owner, address(0), id); + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* INTERNAL APPROVAL FUNCTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Returns whether `account` is the owner of token `id`, or is approved to manage it. + /// + /// Requirements: + /// - Token `id` must exist. + function _isApprovedOrOwner(address account, uint256 id) + internal + view + virtual + returns (bool result) + { + /// @solidity memory-safe-assembly + assembly { + result := 1 + // Clear the upper 96 bits. + account := shr(96, shl(96, account)) + // Load the ownership data. + mstore(0x00, id) + mstore(0x1c, or(_ERC721_MASTER_SLOT_SEED, account)) + let ownershipSlot := add(id, add(id, keccak256(0x00, 0x20))) + let owner := shr(96, shl(96, sload(ownershipSlot))) + // Revert if the token does not exist. + if iszero(owner) { + mstore(0x00, 0xceea21b6) // `TokenDoesNotExist()`. + revert(0x1c, 0x04) + } + // Check if `account` is the `owner`. + if iszero(eq(account, owner)) { + mstore(0x00, owner) + // Check if `account` is approved to manage the token. + if iszero(sload(keccak256(0x0c, 0x30))) { + result := eq(account, sload(add(1, ownershipSlot))) + } + } + } + } + + /// @dev Returns the account approved to manage token `id`. + /// Returns the zero address instead of reverting if the token does not exist. + function _getApproved(uint256 id) internal view virtual returns (address result) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, id) + mstore(0x1c, _ERC721_MASTER_SLOT_SEED) + result := sload(add(1, add(id, add(id, keccak256(0x00, 0x20))))) + } + } + + /// @dev Equivalent to `_approve(address(0), account, id)`. + function _approve(address account, uint256 id) internal virtual { + _approve(address(0), account, id); + } + + /// @dev Sets `account` as the approved account to manage token `id`, using `by`. + /// + /// Requirements: + /// - Token `id` must exist. + /// - If `by` is not the zero address, `by` must be the owner + /// or an approved operator for the token owner. + /// + /// Emits a {Approval} event. + function _approve(address by, address account, uint256 id) internal virtual { + assembly { + // Clear the upper 96 bits. + let bitmaskAddress := shr(96, not(0)) + account := and(bitmaskAddress, account) + by := and(bitmaskAddress, by) + // Load the owner of the token. + mstore(0x00, id) + mstore(0x1c, or(_ERC721_MASTER_SLOT_SEED, by)) + let ownershipSlot := add(id, add(id, keccak256(0x00, 0x20))) + let owner := and(bitmaskAddress, sload(ownershipSlot)) + // Revert if the token does not exist. + if iszero(owner) { + mstore(0x00, 0xceea21b6) // `TokenDoesNotExist()`. + revert(0x1c, 0x04) + } + // If `by` is not the zero address, do the authorization check. + // Revert if `by` is not the owner, nor approved. + if iszero(or(iszero(by), eq(by, owner))) { + mstore(0x00, owner) + if iszero(sload(keccak256(0x0c, 0x30))) { + mstore(0x00, 0x4b6e7f18) // `NotOwnerNorApproved()`. + revert(0x1c, 0x04) + } + } + // Sets `account` as the approved account to manage `id`. + sstore(add(1, ownershipSlot), account) + // Emit the {Approval} event. + log4(codesize(), 0x00, _APPROVAL_EVENT_SIGNATURE, owner, account, id) + } + } + + /// @dev Approve or remove the `operator` as an operator for `by`, + /// without authorization checks. + /// + /// Emits an {ApprovalForAll} event. + function _setApprovalForAll(address by, address operator, bool isApproved) internal virtual { + /// @solidity memory-safe-assembly + assembly { + // Clear the upper 96 bits. + by := shr(96, shl(96, by)) + operator := shr(96, shl(96, operator)) + // Convert to 0 or 1. + isApproved := iszero(iszero(isApproved)) + // Update the `isApproved` for (`by`, `operator`). + mstore(0x1c, or(_ERC721_MASTER_SLOT_SEED, operator)) + mstore(0x00, by) + sstore(keccak256(0x0c, 0x30), isApproved) + // Emit the {ApprovalForAll} event. + mstore(0x00, isApproved) + log3(0x00, 0x20, _APPROVAL_FOR_ALL_EVENT_SIGNATURE, by, operator) + } + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* INTERNAL TRANSFER FUNCTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Equivalent to `_transfer(address(0), from, to, id)`. + function _transfer(address from, address to, uint256 id) internal virtual { + _transfer(address(0), from, to, id); + } + + /// @dev Transfers token `id` from `from` to `to`. + /// + /// Requirements: + /// + /// - Token `id` must exist. + /// - `from` must be the owner of the token. + /// - `to` cannot be the zero address. + /// - If `by` is not the zero address, + /// it must be the owner of the token, or be approved to manage the token. + /// + /// Emits a {Transfer} event. + function _transfer(address by, address from, address to, uint256 id) internal virtual { + _beforeTokenTransfer(from, to, id); + /// @solidity memory-safe-assembly + assembly { + // Clear the upper 96 bits. + let bitmaskAddress := shr(96, not(0)) + from := and(bitmaskAddress, from) + to := and(bitmaskAddress, to) + by := and(bitmaskAddress, by) + // Load the ownership data. + mstore(0x00, id) + mstore(0x1c, or(_ERC721_MASTER_SLOT_SEED, by)) + let ownershipSlot := add(id, add(id, keccak256(0x00, 0x20))) + let ownershipPacked := sload(ownershipSlot) + let owner := and(bitmaskAddress, ownershipPacked) + // Revert if the token does not exist, or if `from` is not the owner. + if iszero(mul(owner, eq(owner, from))) { + // `TokenDoesNotExist()`, `TransferFromIncorrectOwner()`. + mstore(shl(2, iszero(owner)), 0xceea21b6a1148100) + revert(0x1c, 0x04) + } + // Load, check, and update the token approval. + { + mstore(0x00, from) + let approvedAddress := sload(add(1, ownershipSlot)) + // If `by` is not the zero address, do the authorization check. + // Revert if the `by` is not the owner, nor approved. + if iszero(or(iszero(by), or(eq(by, from), eq(by, approvedAddress)))) { + if iszero(sload(keccak256(0x0c, 0x30))) { + mstore(0x00, 0x4b6e7f18) // `NotOwnerNorApproved()`. + revert(0x1c, 0x04) + } + } + // Delete the approved address if any. + if approvedAddress { sstore(add(1, ownershipSlot), 0) } + } + // Update with the new owner. + sstore(ownershipSlot, xor(ownershipPacked, xor(from, to))) + // Decrement the balance of `from`. + { + let fromBalanceSlot := keccak256(0x0c, 0x1c) + sstore(fromBalanceSlot, sub(sload(fromBalanceSlot), 1)) + } + // Increment the balance of `to`. + { + mstore(0x00, to) + let toBalanceSlot := keccak256(0x0c, 0x1c) + let toBalanceSlotPacked := add(sload(toBalanceSlot), 1) + // Revert if `to` is the zero address, or if the account balance overflows. + if iszero(mul(to, and(toBalanceSlotPacked, _MAX_ACCOUNT_BALANCE))) { + // `TransferToZeroAddress()`, `AccountBalanceOverflow()`. + mstore(shl(2, iszero(to)), 0xea553b3401336cea) + revert(0x1c, 0x04) + } + sstore(toBalanceSlot, toBalanceSlotPacked) + } + // Emit the {Transfer} event. + log4(codesize(), 0x00, _TRANSFER_EVENT_SIGNATURE, from, to, id) + } + _afterTokenTransfer(from, to, id); + } + + /// @dev Equivalent to `_safeTransfer(from, to, id, "")`. + function _safeTransfer(address from, address to, uint256 id) internal virtual { + _safeTransfer(from, to, id, ""); + } + + /// @dev Transfers token `id` from `from` to `to`. + /// + /// Requirements: + /// + /// - Token `id` must exist. + /// - `from` must be the owner of the token. + /// - `to` cannot be the zero address. + /// - The caller must be the owner of the token, or be approved to manage the token. + /// - 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 id, bytes memory data) + internal + virtual + { + _transfer(address(0), from, to, id); + if (_hasCode(to)) _checkOnERC721Received(from, to, id, data); + } + + /// @dev Equivalent to `_safeTransfer(by, from, to, id, "")`. + function _safeTransfer(address by, address from, address to, uint256 id) internal virtual { + _safeTransfer(by, from, to, id, ""); + } + + /// @dev Transfers token `id` from `from` to `to`. + /// + /// Requirements: + /// + /// - Token `id` must exist. + /// - `from` must be the owner of the token. + /// - `to` cannot be the zero address. + /// - If `by` is not the zero address, + /// it must be the owner of the token, or be approved to manage the token. + /// - 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 by, address from, address to, uint256 id, bytes memory data) + internal + virtual + { + _transfer(by, from, to, id); + if (_hasCode(to)) _checkOnERC721Received(from, to, id, data); + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* HOOKS FOR OVERRIDING */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Hook that is called before any token transfers, including minting and burning. + function _beforeTokenTransfer(address from, address to, uint256 id) internal virtual {} + + /// @dev Hook that is called after any token transfers, including minting and burning. + function _afterTokenTransfer(address from, address to, uint256 id) internal virtual {} + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* PRIVATE HELPERS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Returns if `a` has bytecode of non-zero length. + function _hasCode(address a) private view returns (bool result) { + /// @solidity memory-safe-assembly + assembly { + result := extcodesize(a) // Can handle dirty upper bits. + } + } + + /// @dev Perform a call to invoke {IERC721Receiver-onERC721Received} on `to`. + /// Reverts if the target does not support the function correctly. + function _checkOnERC721Received(address from, address to, uint256 id, bytes memory data) + private + { + /// @solidity memory-safe-assembly + assembly { + // Prepare the calldata. + let m := mload(0x40) + let onERC721ReceivedSelector := 0x150b7a02 + mstore(m, onERC721ReceivedSelector) + mstore(add(m, 0x20), caller()) // The `operator`, which is always `msg.sender`. + mstore(add(m, 0x40), shr(96, shl(96, from))) + mstore(add(m, 0x60), id) + mstore(add(m, 0x80), 0x80) + let n := mload(data) + mstore(add(m, 0xa0), n) + if n { pop(staticcall(gas(), 4, add(data, 0x20), n, add(m, 0xc0), n)) } + // Revert if the call reverts. + if iszero(call(gas(), to, 0, add(m, 0x1c), add(n, 0xa4), m, 0x20)) { + if returndatasize() { + // Bubble up the revert if the call reverts. + returndatacopy(m, 0x00, returndatasize()) + revert(m, returndatasize()) + } + } + // Load the returndata and compare it. + if iszero(eq(mload(m), shl(224, onERC721ReceivedSelector))) { + mstore(0x00, 0xd1a57ed6) // `TransferToNonERC721ReceiverImplementer()`. + revert(0x1c, 0x04) + } + } + } +} + + +// a minimal implementation of soladys ERC721 with a mint function +contract MockERC721 is ERC721 { + function mint(address to, uint256 tokenUri) external { + _mint(to, tokenUri); + } + + function name() public view override returns (string memory) { + return ""; + } + + /// @dev Returns the token collection symbol. + function symbol() public view override returns (string memory) { + return ""; + } + + /// @dev Returns the Uniform Resource Identifier (URI) for token `id`. + function tokenURI(uint256 id) public view override returns (string memory) { + return ""; + } + +} \ No newline at end of file diff --git a/test/Rescuable721.t.sol b/test/Rescuable721.t.sol new file mode 100644 index 0000000..13d05a6 --- /dev/null +++ b/test/Rescuable721.t.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'forge-std/Test.sol'; +import {Address} from '../src/contracts/oz-common/Address.sol'; +import {MockERC721, ERC721} from '../src/mocks/ERC721.sol'; +import {Rescuable721} from '../src/contracts/utils/Rescuable721.sol'; + +contract MockReceiver721TokensContract is Rescuable721 { + address public immutable ALLOWED; + constructor (address allowedAddress) { + ALLOWED = allowedAddress; + } + + function whoCanRescue() public view override returns (address) { + return ALLOWED; + } +} + +contract Rescue721Test is Test { + address public constant ALLOWED = address(1023579); + + MockERC721 public testToken; + MockReceiver721TokensContract public tokensReceiver; + + event ERC721Rescued( + address indexed caller, + address indexed token, + address indexed to, + uint256 tokenId + ); + + function setUp() public { + testToken = new MockERC721(); + tokensReceiver = new MockReceiver721TokensContract(ALLOWED); + } + + function testFuzzEmergencyTokenTransfer(address randomWallet, address recipient) public { + vm.assume(randomWallet != address(0)); + vm.assume(recipient != address(0)); + testToken.mint(randomWallet, 1); + hoax(randomWallet); + testToken.transferFrom(randomWallet, address(tokensReceiver), 1); + + assertEq(testToken.balanceOf(address(tokensReceiver)), 1); + + hoax(ALLOWED); + vm.expectEmit(true, true, false, true); + emit ERC721Rescued(ALLOWED, address(testToken), recipient, 1); + tokensReceiver.emergency721TokenTransfer(address(testToken), recipient, 1); + + assertEq(testToken.balanceOf(address(tokensReceiver)), 0); + assertEq(testToken.balanceOf(address(recipient)), 1); + } + + function testFuzzEmergencyTokenTransferWhenNotOwner(address randomWallet, address recipient) public { + vm.assume(randomWallet != address(0)); + vm.assume(recipient != address(0)); + testToken.mint(randomWallet, 1); + hoax(randomWallet); + testToken.transferFrom(randomWallet, address(tokensReceiver), 1); + + assertEq(testToken.balanceOf(address(tokensReceiver)), 1); + + vm.expectRevert(); + tokensReceiver.emergency721TokenTransfer(address(testToken), recipient, 1); + } +}