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 |
A standardized way to pass information (address registry, uint256 tokenId, address paymentToken, uint256 paymentValue) with the transfer of royalties payment when supporting EIP-2981.
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.
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:
-
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.
-
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)
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
This standard being used on a receiver contract:
function supportsInterface (bytes4 interfaceId) public override view returns (bool) {
return type(ERCInteractiveNFTURI).interfaceId == interfaceId || super.supportsInterface(interfaceId);
}
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}('');
}
}
}
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));
}
}
}
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;
}
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.
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 and related rights waived via CC0.
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?