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

ERC4907 #370

merged 26 commits into from
Jul 17, 2022

Conversation

Vectorized
Copy link
Collaborator

@Vectorized Vectorized commented Jul 12, 2022

https://eips.ethereum.org/EIPS/eip-4907

@emojidao @0xanders

Notes:

  • File is called ERC4907A.sol because it inherits from ERC721A.sol.

  • The ERC-4907 standard is a fully valid extension of ERC-721.

  • User info is retained after transfer, unlike the reference implementation which resets it after each transfer to a different address.

    • The standard permits the user info to remain the same upon transfer.

    • Retaining the user info by default saves more gas overall.

    • The new owner can choose to reset the user if they want.

    • Good for use cases such as retaining user info while sending the token to a personal cold wallet.

    • The _beforeTokenTransfers function can be overriden by devs to alter this behavior.

  • Unlike the reference implementation, we do not have an internal mapping of structs.

    • Instead, we provide a _explicitUserOf which returns the address of the user, regardless of expiry status.

Some minor changes to ERC721A.sol:

  • Add _isApprovedOrOwner(address spender, uint256 tokenId).

  • Renamed private function _isOwnerOrApproved to _isSinglyApprovedOrOwner for more clarity.

@Vectorized Vectorized marked this pull request as ready for review July 12, 2022 19:01
@Vectorized Vectorized requested a review from cygaar July 12, 2022 23:40
Comment on lines 55 to 66
function userOf(uint256 tokenId) public view returns (address) {
uint256 packed = _packedUserInfo[tokenId];
assembly {
// Branchless `packed *= block.timestamp <= expires ? 1 : 0`.
packed := mul(
packed,
// `block.timestamp <= expires ? 1 : 0`.
lt(shl(_BITPOS_EXPIRES, timestamp()), packed)
)
}
return address(uint160(packed));
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Explanation

First, we know that:

packed = (expires << 160) | user

The straightforward way of implementing block.timestamp <= expires is:

!(block.timestamp > (packed >> 160)) which in Yul, is: iszero(gt(timestamp(), shr(160, packed)))


Now, notice that if user != address(0):

(block.timestamp << 160) < ((expires << 160) | user) is true for block.timestamp <= expires.

And when user == address(0):

address(uint160((expires << 160) | user)) evaluates to address(0) anyway.

So that's how we avoid the extra iszero opcode.


Having an optimized userOf function is important for on-chain verification by third party contracts.

) 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

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.

@0xanders
Copy link

Awesome work, thanks @Vectorized

@0xanders
Copy link

Suggestion: Add an readme file erc4907a.md to the docs folder.

@Vectorized
Copy link
Collaborator Author

@0xanders Definitely. Readying a separate PR for that soon. :)

Thanks for your work on the EIP. Simple, practical, elegant.

contracts/ERC721A.sol Outdated Show resolved Hide resolved
contracts/ERC721A.sol Outdated Show resolved Hide resolved
contracts/ERC721A.sol Outdated Show resolved Hide resolved
contracts/extensions/ERC4907A.sol Outdated Show resolved Hide resolved
contracts/extensions/ERC4907A.sol Show resolved Hide resolved
contracts/mocks/ERC4907AMock.sol Outdated Show resolved Hide resolved
@Vectorized Vectorized requested a review from cygaar July 17, 2022 20:06
contracts/ERC721A.sol Outdated Show resolved Hide resolved
contracts/ERC721A.sol Outdated Show resolved Hide resolved
@Vectorized
Copy link
Collaborator Author

Vectorized commented Jul 17, 2022

I think we can remove the _isApprovedOrOwner.

I forgot we removed it cuz it can be easily substituted with other functions.

@Vectorized
Copy link
Collaborator Author

I think I'll undo any changes to ERC721A.sol in this PR, and move them over to PR #377 instead.

Copy link
Collaborator

@cygaar cygaar left a comment

Choose a reason for hiding this comment

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

One comment but otherwise lgtm. We should add in docs in the future

uint64 expires
) public virtual {
address owner = ownerOf(tokenId);
if (_msgSenderERC721A() != owner)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's add a comment here mentioning the gas savings of not having branching similar to what we have in 721A

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The if-if-if will branch in this case.

The innards of ERC721A uses assembly to compute msg.sender == owner || msg.sender == approvedAddress branchlessly for transfers and burns.
In those very specific scenarios, we have approvedAddress already loaded for determining if we need to clear the slot, so we may as recycle it in a branchless way.

Outside of transfers and burns (i.e. if we don't need to decide whether to clear the approvedAddress slot),
an if-if-if will be better, since it is way more likely for users to approve all tokens for an operator, than approve on a per-token-basis.

Copy link
Collaborator

Choose a reason for hiding this comment

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

What's the reason behind just making it 3 if statements instead of a single if?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The following actually costs more gas

if (_msgSenderERC721A() == owner || 
    isApprovedForAll(owner, _msgSenderERC721A() ||
    getApproved(tokenId) == _msgSenderERC721A())

@Vectorized Vectorized merged commit 32b83e3 into chiru-labs:main Jul 17, 2022
@Vectorized Vectorized deleted the feature/4907 branch July 18, 2022 00:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants