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

ERC4907 #370

Merged
merged 26 commits into from
Jul 17, 2022
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
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
18 changes: 15 additions & 3 deletions contracts/ERC721A.sol
Original file line number Diff line number Diff line change
Expand Up @@ -598,7 +598,7 @@ contract ERC721A is IERC721A {
/**
* @dev Returns whether the `approvedAddress` is equals to `from` or `msgSender`.
*/
function _isOwnerOrApproved(
function _isSinglyApprovedOrOwner(
Vectorized marked this conversation as resolved.
Show resolved Hide resolved
Vectorized marked this conversation as resolved.
Show resolved Hide resolved
address approvedAddress,
address from,
address msgSender
Expand All @@ -613,6 +613,18 @@ contract ERC721A is IERC721A {
}
}

/**
* @dev Returns whether `spender` is allowed to manage `tokenId`.
*
* Requirements:
*
* - `tokenId` must exist.
*/
function _isApprovedOrOwner(address spender, uint256 tokenId) internal view virtual returns (bool) {
Vectorized marked this conversation as resolved.
Show resolved Hide resolved
address owner = ownerOf(tokenId);
return (spender == owner || isApprovedForAll(owner, spender) || getApproved(tokenId) == spender);
}

/**
* @dev Transfers `tokenId` from `from` to `to`.
*
Expand All @@ -635,7 +647,7 @@ contract ERC721A is IERC721A {
(uint256 approvedAddressSlot, address approvedAddress) = _getApprovedAddress(tokenId);

// The nested ifs save around 20+ gas over a compound boolean condition.
if (!_isOwnerOrApproved(approvedAddress, from, _msgSenderERC721A()))
if (!_isSinglyApprovedOrOwner(approvedAddress, from, _msgSenderERC721A()))
if (!isApprovedForAll(from, _msgSenderERC721A())) revert TransferCallerNotOwnerNorApproved();

if (to == address(0)) revert TransferToZeroAddress();
Expand Down Expand Up @@ -712,7 +724,7 @@ contract ERC721A is IERC721A {

if (approvalCheck) {
// The nested ifs save around 20+ gas over a compound boolean condition.
if (!_isOwnerOrApproved(approvedAddress, from, _msgSenderERC721A()))
if (!_isSinglyApprovedOrOwner(approvedAddress, from, _msgSenderERC721A()))
if (!isApprovedForAll(from, _msgSenderERC721A())) revert TransferCallerNotOwnerNorApproved();
}

Expand Down
115 changes: 115 additions & 0 deletions contracts/extensions/ERC4907A.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// SPDX-License-Identifier: MIT
// ERC721A Contracts v4.1.0
// Creator: Chiru Labs

pragma solidity ^0.8.4;

import './IERC4907A.sol';
import '../ERC721A.sol';

/**
* @dev ERC4907 compliant extension of ERC721A.
*
* The ERC4907 standard https://eips.ethereum.org/EIPS/eip-4907[ERC4907] allows
* owners and authorized addresses to add a time-limited role
* with restricted permissions to ERC721 tokens.
*/
abstract contract ERC4907A is ERC721A, IERC4907A {
// The bit position of `expires` in packed user info.
uint256 private constant _BITPOS_EXPIRES = 160;

// Mapping from token ID to user info.
//
// Bits Layout:
// - [0..159] `user`
// - [160..223] `expires`
Vectorized marked this conversation as resolved.
Show resolved Hide resolved
mapping(uint256 => uint256) private _packedUserInfo;

/**
* @dev Sets the `user` and `expires` for `tokenId`.
* The zero address indicates there is no user.
*
* Requirements:
*
* - The caller must own `tokenId` or be an approved operator.
*/
function setUser(
uint256 tokenId,
address user,
uint64 expires
) public {
if (!_isApprovedOrOwner(msg.sender, tokenId)) revert SetUserCallerNotOwnerNorApproved();

_packedUserInfo[tokenId] = (uint256(expires) << _BITPOS_EXPIRES) | uint256(uint160(user));

emit UpdateUser(tokenId, user, expires);
}

/**
* @dev Returns the user address for `tokenId`.
* The zero address indicates that there is no user or if the user is expired.
*/
function userOf(uint256 tokenId) public view returns (address) {
uint256 packed = _packedUserInfo[tokenId];
assembly {
let expiry := shr(_BITPOS_EXPIRES, packed)
// `expires >= block.timestamp ? 1 : 0`.
let notExpired := iszero(lt(expiry, timestamp()))
Vectorized marked this conversation as resolved.
Show resolved Hide resolved
// Set `packed` to zero if the user has expired.
packed := mul(packed, notExpired)
}
return address(uint160(packed));
Vectorized marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* @dev Returns the user's expires of `tokenId`.
*/
function userExpires(uint256 tokenId) public view returns (uint256) {
return _packedUserInfo[tokenId] >> _BITPOS_EXPIRES;
}

/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721A, IERC721A) returns (bool) {
// The interface ID for ERC4907 is `0xad092b5c`.
// See: https://eips.ethereum.org/EIPS/eip-4907
return super.supportsInterface(interfaceId) || interfaceId == 0xad092b5c;
}

/**
* @dev Returns the user address for `tokenId`, ignoring the expiry status.
*/
function _explicitUserOf(uint256 tokenId) internal view returns (address) {
return address(uint160(_packedUserInfo[tokenId]));
}

/**
* @dev Overrides the `_beforeTokenTransfers` hook to clear the user info upon transfer.
*/
function _beforeTokenTransfers(
address from,
address to,
uint256 startTokenId,
uint256 quantity
) internal virtual override {
super._beforeTokenTransfers(from, to, startTokenId, quantity);

bool mayNeedClearing;
Copy link

@0xanders 0xanders Jul 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// for gas savings, the following lines of code can be deleted
// if the new owner want to change the user to zero address ,
// the new owner could call setUser

        bool mayNeedClearing;
        assembly {
            // Branchless `!(from == address(0) || from == to)`.
            // Saves 60+ gas.
            // The addresses are masked with `_BITMASK_ADDRESS` to
            // clear any non-zero excess upper bits.
            mayNeedClearing := iszero(
                or(
                    // Whether it is a mint (i.e. `from == address(0)`).
                    iszero(and(from, _BITMASK_ADDRESS)),
                    // Whether the owner is unchanged (i.e. `from == to`).
                    eq(and(from, _BITMASK_ADDRESS), and(to, _BITMASK_ADDRESS))
                )
            )
        }

        if (mayNeedClearing) {
            // If either `user` or `expires` are non-zero.
            if (_packedUserInfo[startTokenId] != 0) {
                delete _packedUserInfo[startTokenId];
                emit UpdateUser(startTokenId, address(0), 0);
            }
        }

Copy link
Collaborator Author

@Vectorized Vectorized Jul 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah. It’s just to stay faithful to the original implementation.

I personally feel that resetting user upon transfer like the original code is a good default behaviour.

But I also can see very valid use cases for keeping the user by default.

Let me think again with the team if it is better to reset or keep the user by default.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cygaar After some marinating, I'm feeling it's better to remove the auto user reset, as suggested.

Let the users add it on their own if they want.

// For branchless boolean. Saves 60+ gas.
assembly {
// The amount of bits to left shift to clear the upper bits of addresses.
let addrShift := sub(256, _BITPOS_EXPIRES)
// Equivalent to `quantity == 1 && !(from == address(0) || from == to)`.
let isMint := iszero(shl(addrShift, from))
let fromEqTo := eq(shl(addrShift, from), shl(addrShift, to))
mayNeedClearing := and(eq(quantity, 1), iszero(or(isMint, fromEqTo)))
}

if (mayNeedClearing)
if (_packedUserInfo[startTokenId] != 0) {
delete _packedUserInfo[startTokenId];
emit UpdateUser(startTokenId, address(0), 0);
}
}
}
48 changes: 48 additions & 0 deletions contracts/extensions/IERC4907A.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// SPDX-License-Identifier: MIT
// ERC721A Contracts v4.1.0
// Creator: Chiru Labs

pragma solidity ^0.8.4;

import '../IERC721A.sol';

/**
* @dev Interface of an ERC4907A compliant contract.
*/
interface IERC4907A is IERC721A {
/**
* The caller must own the token or be an approved operator.
*/
error SetUserCallerNotOwnerNorApproved();

/**
* @dev Emitted when the `user` of an NFT or the `expires` of the `user` is changed.
* The zero address for user indicates that there is no user address.
*/
event UpdateUser(uint256 indexed tokenId, address indexed user, uint64 expires);

/**
* @dev Sets the `user` and `expires` for `tokenId`.
* The zero address indicates there is no user.
*
* Requirements:
*
* - The caller must own `tokenId` or be an approved operator.
*/
function setUser(
uint256 tokenId,
address user,
uint64 expires
) external;

/**
* @dev Returns the user address for `tokenId`.
* The zero address indicates that there is no user or if the user is expired.
*/
function userOf(uint256 tokenId) external view returns (address);

/**
* @dev Returns the user's expires of `tokenId`.
*/
function userExpires(uint256 tokenId) external view returns (uint256);
}
7 changes: 7 additions & 0 deletions contracts/interfaces/IERC4907A.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: MIT
// ERC721A Contracts v4.1.0
// Creator: Chiru Labs

pragma solidity ^0.8.4;

import '../extensions/IERC4907A.sol';
32 changes: 32 additions & 0 deletions contracts/mocks/ERC4907AMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: MIT
// ERC721A Contracts v4.1.0
// Creators: Chiru Labs

pragma solidity ^0.8.4;

import '../extensions/ERC4907A.sol';

contract ERC4907AMock is ERC721A, ERC4907A {
constructor(string memory name_, string memory symbol_) ERC721A(name_, symbol_) {}

function safeMint(address to, uint256 quantity) public {
Vectorized marked this conversation as resolved.
Show resolved Hide resolved
_safeMint(to, quantity);
}

function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721A, ERC4907A) returns (bool) {
return super.supportsInterface(interfaceId);
}

function _beforeTokenTransfers(
address from,
address to,
uint256 startTokenId,
uint256 quantity
) internal virtual override(ERC721A, ERC4907A) {
super._beforeTokenTransfers(from, to, startTokenId, quantity);
}

function explicitUserOf(uint256 tokenId) public view returns (address) {
return _explicitUserOf(tokenId);
}
}
Loading