ERC721 (and ERC1155), the token standards that define the most common NFT contracts on EVM chains, draw heavy inspiration from ERC20. This is apparent in its allowance mechanism. Users call either approve()
or setApprovalForAll()
to grant another address the ability to transfer an NFT token from themselves in a future transaction/call. If the interaction that performs the NFT transfer is sent by another user (not the owner), this makes perfect sense. But if the transaction is actually sent by the token owner, there is no need to use allowances at all!
The ERC721 standard defines an onERC721Received()
handler function that lets the recipient take execution control during a call to safeTransferFrom()
. Additionally, an arbitrary bytes
data parameter can also be passed along through the transfer call into the handler. This data parameter is usually decoded by the handler and provides application-specific context on the purpose of the transfer.
To demonstrate the advantages of the pattern, let's see at what it looks like with a conventional, allowance based approach for a fictional, custodial NFT auction protocol.
- Seller makes a transaction (1) calling
nft.approve(auctionHouse, tokenId)
for the token being listed, grantingauctionHouse
an allowance to transfertokenId
on their behalf. - Seller makes another transaction (2) calling
auctionHouse.list(nft, tokenId, listOptions)
.auctionHouse
callsnft.transferFrom(msg.sender, address(this), tokenId)
to take custody of the token and starts an auction based onlistOptions
config.
Compare that approach to a much simpler one using the receive hook.
- Seller makes a single transaction calling
nft.safeTransferFrom(seller, auctionHouse, tokenId, abi.encode(listOptions))
.- The NFT contract calls
auctionHouse.onERC721Recevied(..., data)
.auctionHouse
decodesdata
(listOptions = abi.decode(data, ListOptions)
) and starts an auction based on the decoded config.
- The NFT contract calls
So, in this case, using transfer hooks is one less transaction for the user 😎.
onERC721Received()
is declared as as:
function onERC721Received(address operator, address from, uint256 tokenId, bytes data) external returns(bytes4);
- This function called on the recipient of the token.
- This call is made after the token is transferred.
operator
is the address that calledsafeTransferFrom()
, which in this pattern will always betx.origin
(owner).from
is the original owner of the token, which in this pattern will also betx.origin
.msg.sender
will be the ERC721 token contract.- Anyone can call this function, so depending on your product's expectations, you may want to enforce that the
msg.sender
is a known NFT contract.
- Anyone can call this function, so depending on your product's expectations, you may want to enforce that the
data
is any arbitrary data the caller ofsafeTransferFrom()
passes in. Do not expect it to always be well-formed because of this.- This handler is not
payable
, so if you also need to collect ETH from the user (unlikely) in the same transaction, you may need to have the user set up a WETH allowance in advance. - The return value must be
onERC721Received.selector
(0x150b7a02
).
ERC1155 tokens also support a similar mechanism through its onERC1155Received()
and onERC1155BatchReceived()
hooks, with nearly identical semantics.
- The official Mooncats wrapper uses
onERC721Received()
to turn unofficial wrapped mooncats into official wrapped mooncats when someone transfers them to the contract. - The ERC721 orders feature of the 0x Exhange Protocol accepts a complementary NFT buy order encoded in the
data
param to perform a swap at the same time as the transfer.
The included demo is a basic, no-frills NFT auction house contract that uses onERC721Received()
to start an auction in a single transaction.