8

The Non-Fungible Token Standard (described in EIP-721) specifies, among other things, the signature of the transferFrom() function: function transferFrom(address _from, address _to, uint256 _tokenId) external payable;

Besides, it says that the function "throws if _from is not the current owner" of _tokenId.

Now, why is the _from argument required, at all, if it is easily deducible from _tokenId (by passing it to the ownerOf() function)? Passing _from seems redundant, and the the internal comparison of the argument to the "actual" owner of _tokenId - wasteful.

Why not just check whether the transferFrom() caller has been approved by the ownerOf(_tokenId) to transfer either the _tokenId itself (via approve()) - or all of the owner's NFTs from the collection (via setApprovalForAll())?

If the caller isn't approved to transfer the NFT, it doesn't matter whether msg.sender == _ownerOf(_tokenId). And if they are approved, then, what is the added value of forcing the caller to also pass the _from argument (which can be obtained by simply calling ownerOf(_tokenId))?

3 Answers 3

2
+50

Great observation on the _from parameter! I never noticed this before.

Here are the two key reasons for its inclusion in the transferFrom() function in ERC-721:

As you know, ERC-721 is strongly inspired by ERC-20, _from parameter is included in both standards for the following reasons:

  • Simplifying things for developers by sharing a similar design across both standards, making them easier to use and understand.
  • Ensuring consistency across token standards: With the _from parameter in ERC-721, developers can more easily work with various token types, as they share the same signature (0x23b872dd): bytes4(keccak256('transferFrom(address,address,uint256)')) == 0x23b872dd

In summary, the _from parameter in ERC-721 maintains consistency with other token standards like ERC-20 and simplifies working with diverse tokens for developers.

It was mentioned previously that including the _from parameter could save gas by avoiding using ownerOf(uint256 tokenId). However, this function is used anyway on transferFrom in OpenZeppelin implementation:

function _isApprovedOrOwner(address spender, uint256 tokenId) internal view virtual returns (bool) {
    address owner = ERC721.ownerOf(tokenId);
    return (spender == owner || isApprovedForAll(owner, spender) || getApproved(tokenId) == spender);
}

Considering this, I don't see any benefits in terms of gas savings by including _from parameter in transferFrom().

EDIT:

Here is an Answer from the lead author of ERC-721, which provides additional insight into the reasoning behind this design choice:

"We did not include a transfer function because it may be ambiguous whether you mean to transfer something that you are the owner of versus something you are authorized to get.

The current owner must always be specified, because of security reasons (specifically, front-running). This is why transferFrom is the closest to the wire you can get."

Here's a simplified summary of the additional information provided:

  • The transfer function was not included to avoid ambiguity between transferring owned tokens and transferring authorized tokens.
  • Specifying the current owner (_from parameter) is necessary for security reasons, particularly to prevent front-running attacks.

By requiring the _from parameter, the function ensures no ambiguity between transferring an NFT you own and one you are authorized to transfer, making the process more secure.

14
  • Thank you for the answer, @AdamBoudj! There's really 1 reason I see in your post, however - the cross-standard consistency. The simplification of the Developers' Experience is just a side-effect of the former. :P
    – Iaroslav
    Commented Apr 1, 2023 at 20:42
  • 1
    It's intriguing to consider how addressing this issue during EIP-721 development could have led to more efficient solutions. Introducing a new transfer function, similar to the one from ERC20, could be a non-breaking change that optimizes and simplifies the implementation. Striking a balance between simplicity and efficiency is vital, and your insights could maybe contribute to refining actual or future token standards.
    – Adam Boudj
    Commented Apr 1, 2023 at 21:16
  • 1
    @GiuseppeBertone even if the interface doesn't require a specific implementation, including the _from parameter influences how implementations are designed, it might not be a fault of the interface itself, but it does encourage a certain approach.
    – Adam Boudj
    Commented Apr 1, 2023 at 22:28
  • 1
    @Iaroslav I've just updated my answer with additional information
    – Adam Boudj
    Commented Apr 7, 2023 at 19:57
  • 1
    @Iaroslav I agree with you, I hope he'll answer to your message and provide more information 👍
    – Adam Boudj
    Commented Apr 8, 2023 at 9:57
0

I belive, it's for providing an additional safety net for operators.

Operators (aka. isApprovedForAll) are addresses who are automatically approved for all NFTs of another address (whereas approvals are only for specific NFTs).

Imagine the following use case where we don't need to supply _from):

  • Theoretical function signature: function transfer(address to, uint256 tokenId) { ... }

  • You write a smart contract that acts as a market place.

  • To make it easy for your users, therefore your contract is set as their operator.

  • You only allow transfers between your users.

Now User 0xA wants to transfer to user 0xB and you proceed with the transaction because it's valid. (User 0xA owns the NFT it wants to sell to 0xB)

So far so good (the transaction is successful), however, since youre smart contract still has approval of the NFT (since it's an operator for both users), 0xA could exploit you now. Note that 0xA does not own the NFT anymore, but now 0xA just sells the same NFT to 0xC. This would succeed even though it shouldn't, because your contract is still the operator of both addresses and therefore allowed to do this transaction.

Now, if you provide as the _from argument always the msg.sender of your selling user, the transaction would fail the second time, since 0xA is not the owner anymore (0xB is).

In the end it comes all down to design and personal preference... You would have to do an additonal check in your own smart contract to prevent this issue and you are right: In a lot of cases you don't need that behaviour but it is really useful once you have a smart contract functioning as an operator for multiple NFT holders.

4
  • Your example doesn't sound quite right, but let's try diving into it... How exactly would 0xA sell the NFT that has previously been transferred to 0xB? Given that the blockchain txs are atomic and unparralel, after the transfer from 0xA to 0xB, 0xA would no longer be the owner of the NFT - and, therefore, wouldn't be able to do anything with it (including, selling it to 0xC). The latter is so not because of the "market place" contract from your example, but because the contract of the NFT collection itself wouldn't allow this (if it's implemented correctly, of course).
    – Iaroslav
    Commented Mar 29, 2023 at 23:55
  • It should still work out... Your contract (SC) has full access of all NFTs of your ERC-721 implementation for 0xA & 0xB. When the NFT gets transferred, to 0xB, SC can still transfer it, now if no additional safety checks are provided in SC, SC would try to transfer the NFT as invoked by 0xA, and since SC is also the operator of 0xB it would succeed. Everything can be avoided by putting a check in SC, but as I said it’s a precaution to give operator SCs as little room for failure as possible. Making the sender explicit will prevent a lot of vulnerabilities in scenarios like this.
    – Throvn
    Commented Mar 30, 2023 at 7:17
  • requiring the address interacting with an NFT through your contract to actually be allowed to do anything with the NFT (by being either the owner or the approved operator for it) is essential for the correct functioning of your contract. Just like the NFT Collection contract must make sure the tx signer is allowed to interact with the NFTs from the Collection, your marketplace contract must also care about this. Making sure the msg.sender is allowed to act on an NFT can be checked independently of the passed _from argument, and, therefore, _from is not needed, IMO...
    – Iaroslav
    Commented Mar 30, 2023 at 9:52
  • Yes I agree, "in the end it comes all down to personal preference". The standard decided to do it the explicit way and always makes sure that the sender knows exactly which transfer it is doing. Regarding unnecessary gas consumption I think this is negligible however, I would also be interested to see how it added up over time.
    – Throvn
    Commented Mar 30, 2023 at 10:10
0

If we stick with the standard interface, one of the reasons is to save gas.

Saving gas is always important, but especially for functions like transfer and transferFrom, which will likely be the most used functions of the contract.

If you don't specify a _from parameter, the smart contract must - as you said - always find the owner and then likely check for the allowance. If you specify the _from parameter, checking only for the allowance is enough in most cases because only the owner can have previously set that allowance.

And because reading from a ĐApp in Ethereum is a free operation, and on the contrary, it's always a paid operation from a smart contract, it's cheaper if it's the ĐApp takes care of calling the ownerOf function and then passing that value in the _from parameter.

Here is a fast implementation of the transferFrom function leveraging the _from parameter to save gas. I started from the first reference implementation cited by the EIP-721 (the 0xcert's one, now @nibbstack's one), and in this specific case, you save ~950 GAS (max, ~610 average). You will see a much more considerable saving if you come from OpenZeppelin's implementation. This is just a fast example, and it can be improved more with enough time and patience.

Side note: technically, the ownerOf function described in the standard is external; therefore, it's meant to be called from a ĐApp and the other functions of the same contract cannot call it. With a typical implementation, the rest of the functions would have likely read the ownership of a token accessing a corresponding mapping variable inside the contract.

11
  • 1
    This is not true. ERC721.ownerOf(tokenId); is actually called within transferFrom itself when the internal function _isApprovedOrOwner is used. And the funny thing, is that ownerOf is called three times per transferFrom.
    – Adam Boudj
    Commented Apr 1, 2023 at 8:28
  • I agree with @AdamBoudj: figuring out the actual owner of the NFT inside the transfer and transferFrom functions is essential for being able to properly verify whether the caller is, actually, allowed to perform the action on the NFT.
    – Iaroslav
    Commented Apr 1, 2023 at 20:28
  • 1
    I don't know the implementation you are referring to, but the interface does not dictate the implementation. The reason to have the _from parameter is to save gas, if who implement the interface does not leverage it that is another story ;) Commented Apr 1, 2023 at 22:03
  • @GiuseppeBertone, fair enough: we shouldn't be talking about specific implementations here, but, rather, about the original standard. However, if ownerOf() wouldn't be called inside the transferFrom() implementation, how could you make sure the tx creator is allowed to transfer the respective NFT?
    – Iaroslav
    Commented Apr 2, 2023 at 11:18
  • I'm not suggesting not checking ownership at all, but doing it when needed, and here the _from parameter comes to help. In pseudo-code, it is: "if msg.sender != _from check allowance; otherwise check ownership". If I have the allowance, the ownership is not relevant. And because the owner rarely uses the transferFrom function - they can use the transfer function for that - in the long run, the gas saving is tangible. Commented Apr 2, 2023 at 20:59

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