8

I'm studying OpenZeppelin's ERC721 token implementation and I'm having a hard time making sense of ERC721Receiver.sol and ERC721Holder.sol and how they relate to eachother.

In the file ERC721BasicToken.sol only the file ERC721Receiver.sol is imported.
When ERC721Receiver.sol only declares the function onERC721Received and ERC721Holder.sol is the contract defining it.

You might have to take a quick look at the implementation on GitHub to see what I'm talking about.

It it supposed to be like that?
Or is that a bug in the implementation?
Also what really is the point of having this functionality and is it really necessary?
Including examples would be very nice.

Relevant Code:
ERC721Receiver.sol

pragma solidity ^0.4.23;


/// @title ERC721 token receiver interface
/// @dev Interface for any contract that wants to support safeTransfers from ERC721 asset contracts.
contract ERC721Receiver {
    /// @dev Magic value to be returned upon successful reception of an NFT
    ///  Equals to `bytes4(keccak256("onERC721Received(address,uint256,bytes)"))`,
    ///  which can be also obtained as `ERC721Receiver(0).onERC721Received.selector`
    bytes4 constant ERC721_RECEIVED = 0xf0b9e5ba;

    /// @notice Handle the receipt of an NFT
    /// @dev The ERC721 smart contract calls this function on the recipient
    ///  after a `safetransfer`. This function MAY throw to revert and reject the
    ///  transfer. This function MUST use 50,000 gas or less. Return of other
    ///  than the magic value MUST result in the transaction being reverted.
    ///  Note: the contract address is always the message sender.
    /// @param _from The sending address
    /// @param _tokenId The NFT identifier which is being transfered
    /// @param _data Additional data with no specified format
    /// @return 
    `bytes4(keccak256("onERC721Received(address,uint256,bytes)"))`
    function onERC721Received(address _from, uint256 _tokenId, bytes _data) public returns(bytes4);
}

ERC721Holder.sol

pragma solidity ^0.4.23;

import "./ERC721Receiver.sol";


contract ERC721Holder is ERC721Receiver {
    function onERC721Received(address, uint256, bytes) public returns(bytes4) {
        return ERC721_RECEIVED;
    }
}

ERC721BasicToken.sol

pragma solidity ^0.4.23;

import "./ERC721Basic.sol";
import "./ERC721Receiver.sol";
import "./SafeMath.sol";
import "./AddressUtils.sol";


/// @title ERC721 Non-Fungible Token Standard basic implementation
/// @dev see https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md
contract ERC721BasicToken is ERC721Basic {

    .
    .
    .

    // Equals to `bytes4(keccak256("onERC721Received(address,uint256,bytes)"))`
    // which can be also obtained as `ERC721Receiver(0).onERC721Received.selector`
    bytes4 constant ERC721_RECEIVED = 0xf0b9e5ba;

    .
    .
    .

    function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes _data) public canTransfer(_tokenId) {
        transferFrom(_from, _to, _tokenId);
        // solium-disable-next-line arg-overflow
        require(checkAndCallSafeTransfer(_from, _to, _tokenId, _data));
    }

    .
    .
    .

    /// @dev Internal function to invoke `onERC721Received` on a target address
    /// @dev The call is not executed if the target address is not a contract
    /// @param _from address representing the previous owner of the given token ID
    /// @param _to target address that will receive the tokens
    /// @param _tokenId uint256 ID of the token to be transferred
    /// @param _data bytes optional data to send along with the call
    /// @return whether the call correctly returned the expected magic value
    function checkAndCallSafeTransfer(address _from, address _to, uint256 _tokenId, bytes _data) internal returns (bool) {
        if (!_to.isContract()) {
            return true;
        }
        bytes4 retval = ERC721Receiver(_to).onERC721Received(_from, _tokenId, _data);
        return (retval == ERC721_RECEIVED);
    }

}

2 Answers 2

9

When using the safeTransferFrom function to send ERC721 tokens to a contract address, it will fail unless the receiving contract properly implements the ERC721TokenReceiver interface. (See the ERC721 Standard for details).

Any implementation of ERC721TokenReceiver will have the onERC721Received function and will return bytes4(keccak256("onERC721Received(address,uint256,bytes)")).

It appears that in the OpenZeppelin example that you linked, they're extending this interface into an abstract contract (ERC721Receiver.sol), and then optimising it. So rather than making the caller calculate bytes4(keccak256("onERC721Received(address,uint256,bytes)")) every time (which is a fixed value), they're precalculating it and storing it in a variable.

As you observed, they then implement the function in ERC721Holder.sol. You could technically just implement it in ERC721Receiver, but I think the Holder contract is just an example, whereas the Receiver abstract contract is something they expect you to always use.

You may have conditions where you want the onERC721Received function to throw (this is allowed by the standard), so if the function were defined in ERC721Receiver.sol you'd have to overwrite it in your implementation.

5
  • Ok I think I get it. I just don't understand why they named it ERC721Holder.sol, that completely threw me off. Also on this line: bytes4 retval = ERC721Receiver(_to).onERC721Received(_from, _tokenId, _data);, why is the value _to passed to the contract? What purpose does that serve? Is that a call to the contract that I would be interacting with in a transaction? Or is that a call to my ERC721Receiver? I guess I'm lost at how the method is actually interacting with another contract. Commented May 18, 2018 at 5:40
  • 1
    It's called ERC721Holder because it can "hold" ERC721 tokens (or more accurately, it can receive them). As for the line you mentioned, ERC721Receiver(_to) isn't passing anything, it's saying "there is an ERC721Receiver contract at the address '_to'". Then it calls onERC721Received on that specific contract. Commented May 18, 2018 at 6:08
  • Now I get it. Awesome. Commented May 18, 2018 at 6:12
  • Can I say: If I want to implement my own auction (or etc) in DApp, only then I have to add ERC721Holder as parent for my auction script? Otherwise we can just ignore this.
    – Richard Fu
    Commented Aug 1, 2018 at 2:54
  • @JohnDeBord, yes you have valid point: function onERC721Received(address, uint256, bytes) public returns(bytes4) { return ERC721_RECEIVED; } . This function has parameters and which are not used in it. Just returns predefined variable. So, no need for this params to pass in function. or doing so, does not have any impact. I assume dev wanted to dynamically calculate the bytes4 hash. But due to spending that gas, he thought why not just return pre-defined value!!! But I am sure there is no need to pass those params _from, _tokenId, _data Commented Aug 4, 2019 at 4:01
12

I know this thread has been here for two years, but I think the question has not been fully answered. See another post I put up here.

The real question is even having this onERC721Received function implemented in the receiving contract, is it the whole story or just the beginning?

I think it is just the beginning. The function is only used to tell the calling contract that "Hey, Calling Contract, I am aware of the necessity of implementing functionalities that the tokens you are going to transfer to me are going to be safe and able to transfer out." The calling contract is still not 100% sure if this is true, but it can only trust upon the fact that if the receiving contract is aware of the necessity, it would be responsible to take proper actions upon receiving NFTs.

That said, more can be done on the receiving side to make the calling contract be more certain that the receiving contract is doing all the right things.

1
  • Yes the same was my understanding too that onERC721Received or ERC165.supportsinterface does not give any full proof guarantee to sender that the assets they are sending will be held properly.
    – CaptPython
    Commented Aug 16, 2022 at 5:33

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.