Skip to content

Instantly share code, notes, and snippets.

@dievardump
Last active February 13, 2023 16:48
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dievardump/f7b21f511f595a552becfb7a475eb133 to your computer and use it in GitHub Desktop.
Save dievardump/f7b21f511f595a552becfb7a475eb133 to your computer and use it in GitHub Desktop.
Draft - EIP-2981 Extension - Token Identification At Payment
eip title authors discussions-to status type category created requires
NFT - EIP-2981 Extension - Token Identification At Payment
Simon Fremaux (@dievardump)
Draft
ERC
2021-06-22
2981

Simple Summary

A standardized way to pass information (address registry, uint256 tokenId, address paymentToken, uint256 paymentValue) with the transfer of royalties payment when supporting EIP-2981.

Abstract

This standard extends the EIP-2981 specification and add a callback onERC2981Received(address, uint256, address, uint256) on contracts receiving royalties payments, to allow for Marketplace contracts to pass critical information with the call made to pay royalties.

By adding the registry (NFT contract address), the token id, the payment token and the payment value, we allow cheaper management of multiple royalties recipients and easier tracking and accounting of royalties payment.

Motivation

EIP-2981 has been designed to be the most easy to grasp and build on. However as we've seen during the discussions and since its finalization in the comments, something really needed is missing: multiple recipients.

For this, it has been said that this should be the role of a Splitter Contract to do the job when receiving payments.

However this has 2 bad sides:

  1. being costly for creators, since the payment does not identify for which token it's made for, creators would have to create one contract per collaboration. More, some collaboration could involve the same people, but different payout allocations, forcing again another contract deployment (and therefore cost) from the creators.

  2. being hard to do accounting on those royalties payment. Because there is actually nothing to identify these transfers as being payment for royalties, people could have a very hard time knowing, understanding and explaining why they received transfer (not everyone knows how to read etherscan, and being dependent on Etherscan API is the opposite of what our space searches to achieve).

As we've said during the EIP-2981 creation discussion, we will expect contracts to allocate enough gas to the royalty payment transfer in order to not fail in the case of a recipient that is a contract (Gnosis Safe, Collab splitter, ...)

What I suggest here instead of using a simple empty call, would be to call a specific function onERC2981Received(address, uint256, address) when doing the payment.

This would work the exact same way EIP2981 already does, but allow for way cheaper collab management on the creators side, because adding registry and tokenId would allow everyone to use one big Splitter Registry instead of creating one splitting contract per token.

This would also allow recipient contracts to perform some actions like firing an event for consumers to know a payment token has been received (because again, without centralized tools like Etherscan it would not really be possible for users to know a random ERC20 has been sent to the splitter without indexing all of ethereum Transfer events)

Specification

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.

The key word consumers defines any contract that introspects for this EIP support before sending royalties over.

Implementers of this standard MUST have all of the following events and functions:

pragma solidity ^0.8.0;

///
/// @dev Implementation of EIP2981Receiver
///
interface EIP2981Receiver {
    ///
    /// bytes4(keccak256("onERC2981Received(address,uint256,address,uint256)")) == 0xe91d1c6c
    /// bytes4 private constant _INTERFACE_ID_2981RECEIVER = 0xe91d1c6c;

    /// @notice Called at time of royalties payment
    /// @param registry the NFT contract address
    /// @param tokenId the NFT id
    /// @param paymentToken the token used as payment. Should be address(0) if payment is in msg.value
    /// @param paymentValue the amount of the payment. This should match the msg.value or the value transfered in ERC20
    /// @return `bytes4(keccak256("onERC2981Received(address,uint256,address,uint256)"))` if received as expected
    function onERC2981Received(address registry, uint256 tokenId, address paymentToken, uint256 paymentValue)
        external
        returns (bytes4);
}

In the case of a payment with an ERC20 token (paymentToken != address(0)), the caller of onERC2981Received MUST give an allowance to the royaltyRecipient to transferFrom. Thus allowing royaltyRecipient to do the transfer itself in the onERC2981Received callback and ensure that paymentValue was actually transfered

Examples

This standard being used on a receiver contract:

supportInterface override for introspection

    function supportsInterface (bytes4 interfaceId) public override view returns (bool) {
        return type(ERCInteractiveNFTURI).interfaceId == interfaceId  || super.supportsInterface(interfaceId);
    }

royalties payment call with native token

    function purchaseWithETH(address registry, uint256 tokenId) public {
        // do whatever is needed to do the purchase
        // ...
        
        // if the NFT contract supports EIP2981
        if (ERC2981(registry).supportsInterface(type(ERC2981).interfaceId) ){
          (address recipient, uint256 amount) = ERC2981(registry).royaltyInfo(tokenId, msg.value);

          // if recipient is a contract and supports EIP2981Receiver
          if (recipient.code.length > 0 && EIP2981Receiver(recipient).supportsInterface(type(EIP2981Receiver).interfaceId) {
            bool success = EIP2981Receiver(registry).onERC2981Receive.call{value: amount}(registry, tokenId, address(0), amount);
            require(success, "Royalties payment error");
          } else {
            registry.call{value: amount}('');
          }
        }
    }

royalties payment call with ERC20

    function purchaseWithERC20(address registry, uint256 tokenId, address erc20, uint256 value) public {
        // do whatever is needed to do the purchase
        // ...
        
        // if the NFT contract supports EIP2981
        if (ERC2981(registry).supportsInterface(type(ERC2981).interfaceId) ){
          (address recipient, uint256 amount) = ERC2981(registry).royaltyInfo(tokenId, value);
          
         

          // if recipient is a contract and supports EIP2981Receiver
          if (recipient.code.length > 0 && EIP2981Receiver(recipient).supportsInterface(type(EIP2981Receiver).interfaceId) {
            // transfer the ERC20 to current contract
            require(IERC20(erc20).transferFrom(msg.sender, address(this), amount));
            // give allowance of `amount` to `recipient`
            IERC20(erc20).approve(recipient, amount);
            // call the callaback
            try EIP2981Receiver(recipient).onERC2981Received(registry, tokenId, erc20, amount) returns (bytes4 retval) {
                require(retval == EIP2981Receiver.onERC2981Received.selector, "EIP2981Receiver: refused royalties payment");
            } catch (bytes memory reason) {
                if (reason.length == 0) {
                    revert("EIP2981Receiver: Error when transfering royalties payment");
                } else {
                    /// @solidity memory-safe-assembly
                    assembly {
                        revert(add(32, reason), mload(reason))
                    }
                }
            }
            // verify the ERC20 has been taken -> if the allowance is not 0, it means it wasn't
            require(
              0 == IERC20(erc20).allowance(address(this), recipient), 
              "EIP2981Receiver payment error"
            );
          } else {
            // transfer the ERC20
            require(IERC20(erc20).transferFrom(msg.sender, recipient, amount));
          }
        }
    }

onERC2981Received implementers

    event RoyaltiesReceived(address registry, uint256 tokenId, address paymentToken, uint256 paymentValue);
    function onERC2981Received(address registry, uint256 tokenId, address paymentToken, uint256 paymentValue)
        external
        returns (bytes4) {
        // verify that msg.value is the right one if paymentToken is nativ chain token
        if (paymentToken == address(0)) { 
            require(msg.value == paymentValue, "!WRONG_VALUE!"); 
        }
        else { 
            // or that we can transfer the balance in ERC20
            require(IERC20(paymentToken).transferFrom(msg.sender, address(this), paymentValue) == true, "!WRONG_VALUE!"); 
        }
        
        _received[registry][tokenId][paymentToken] += paymentValue;
        emit RoyaltiesReceived(registry, tokenId, paymentToken, paymentValue);
        return this.onERC2981Received.selector;
    }

Rationale

Collaboration / Payment splitting is requested by creators

Creators on all platforms have been requesting for builders to ease collaboration and royalties payments.

However the only solution possible with ERC2981 is for them to spend gas creating as much contract splitters as they do collaborations OR to create separate addresses for each collaboration, trusting whoever is in charge to do the things right.

This EIP would allow to help creators to do collaboration with the less fees possible.

Accounting / Following of payments should be easily possible on-chain

Today the only answer I received when asking "But how will someone know they got tokens as royalties payment for a specific tokenId" in the ERC2981 discussions was "People can look on Etherscan and try to see transfers of tokens in the same transaction"

This is not a solution. This shouldn't actually be an answer coming from web3 builders.
No one, especially non tech people, should be dependant of a centralized platform like Etherscan to know that they received royalties payments from a marketplace.

Adding a callback will allow any receiving contracts to be able to notify creators with an event or some other way, without being dependant of third-party tools.

Copyright

Copyright and related rights waived via CC0.

@MaxflowO2
Copy link

MaxflowO2 commented Jan 5, 2022

figuring I play with this all the time... I'm game to mess with this a bit, but ERC2981 is one address, one payment are we talking about a PaymentSpliiter.sol via OZ modification that I can rewrite a bit and use removePayee(address) and what not? Also shouldn't you call ERC165 in there as an import as well?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment