[Ethereum] What’s the point of ERC721Receiver.sol and ERC721Holder.sol in OpenZeppelin’s implementation

erc-721openzeppelinsolidity

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);
    }

}

Best Answer

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.

Related Topic