2

From OpenZeppelin's ERC721 contract:

    function _safeTransfer(address from, address to, uint256 tokenId, bytes memory _data) internal virtual {
        _transfer(from, to, tokenId);
        require(_checkOnERC721Received(from, to, tokenId, _data), "ERC721: transfer to non ERC721Receiver implementer");
    }

In the above code, what purpose does require(...) server after _transfer() as the token will be transferred anyways? I would like to details regarding why such an approach is used.

Moreover, if to is a contract, _checkOnERC721Received returns true always. what does this signify? Is any ethereum contract compatible to hold ERC721 safely out-of-the-box, even without implement ERC721Receiver ?

2

The EIP-721 suggests an optional interface named ERC721TokenReceiver :

/// @dev Note: the ERC-165 identifier for this interface is 0x150b7a02.
interface ERC721TokenReceiver {
/// @notice Handle the receipt of an NFT
/// @dev The ERC721 smart contract calls this function on the recipient
///  after a `transfer`. This function MAY throw to revert and reject the
///  transfer. Return of other than the magic value MUST result in the
///  transaction being reverted.
///  Note: the contract address is always the message sender.
/// @param _operator The address which called `safeTransferFrom` function
/// @param _from The address which previously owned the token
/// @param _tokenId The NFT identifier which is being transferred
/// @param _data Additional data with no specified format
/// @return `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`
///  unless throwing
function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes _data) external returns(bytes4);
}

This interface has only one function named onERC721Received which returns a constant value equal to bytes4(keccak256("onERC721Received(address,address,uint256,bytes)")). You can find an exemple of implementation here : https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/utils/ERC721Holder.sol.

What's the purpose of this interface ?

If someone sends tokens to a smart contract not supporting the ERC721 standard, the NFT could be lost forever.

The EIP-721 implements a safeTransferFrom function to prevent such a bad situation. This method does basically the same thing that transferFrom, except it calls a private function named _checkOnERC721Received at the end of the execution.

How does _checkOnERC721Received work ?

_checkOnERC721Received has the following behavior :

  1. if the receiving address to is an EOA, it returns true
  2. if the receiving address to is a smart contract, it will call on the contract the onERC721Received method seen above and check the result :
  • if the contract returns the magic value, _checkOnERC721Received returns true
  • else it returns false and the _safeTransfer call fails (as the require condition is not met).

Why does _checkOnERC721Received is called at the end ?

To respect the Checks-Effects-Interactions pattern and prevent reentrancy attacks : _safeTransfer first update the ERC721 contract states, then it calls the external smart contract.

2
  • thanks, @clement but this does not answer my question that why require is called later after sending _trasfer and how does it differ if require is called before transfer – hack3r_0m Mar 9 at 15:05
  • @hack3r_0m Check the last point of my answer : inside the require statement there is a potential call to an external smart contract; this is good practice to call external contracts at the end after updating the calling contract state. This is a security measure to prevent attacks such as reentrancy. – clement Mar 9 at 17:55
2

First of all, a transaction will revert unless all conditions are successfully met. In this example, it means that you can perform the _transfer() successfully, but if the require afterwards fails, the whole transaction will be reverted (including the previous transfer).

Once said that, this require is checking whether the destination address is a contract:

  • If not, it will return true and transfer will be done.
  • If it is a contract, it will check whether the contract is compatible with the ERC721 standard. If it is compatible, it will return true; otherwise, it will return false and the transaction will be reverted in order to avoid sending ERC721 tokens to a non ERC721-compatible contract (equivalent to burning such as tokens).
2
  • so what is the scope of require() if statements executed above it can also be reverted? thanks for correct on the smart contract part :) I didn't not ! while reading code. – hack3r_0m Mar 9 at 5:54
  • It's a good point. I normally put all requires at the beginning of a function to do an early revert if any condition is not met -> this way, the gas consumed will be lower as compared to executing unnecessary statements that can be reverted afterwards. There is probably a good reason for doing this, but you may ask at OpenZeppelin's forum. – Sergi Juanati Mar 9 at 8:25

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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