Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ERC721 full implementation #803

Merged
merged 32 commits into from
Mar 23, 2018
Merged

Conversation

spalladino
Copy link
Contributor

@spalladino spalladino commented Mar 7, 2018

Fixes #640
Fixes #809

Continues the work of @facuspagnuolo in #682

Current status:

  • ERC721 implementation is complete

Pending discussions:

  • exists was added to the interface here, though will not be part of the standard
  • The 50K gas stipend for the onERC721Received call was removed (see here)

require(_from != address(0));
require(_to != address(0));
require(_to != ownerOf(_tokenId));
require(ownerOf(_tokenId) == _from);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is checked in the next line in clearApproval and removeToken. It can probably be skipped here as it is redundant.

tokenOwner[_tokenId] = 0;
}

function checkSafeTransfer(address _from, address _to, uint256 _tokenId, bytes _data) internal returns (bool) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughts on renaming this to checkAndCallSafeTransfer?


function checkSafeTransfer(address _from, address _to, uint256 _tokenId, bytes _data) internal returns (bool) {
return !_to.isContract() ||
(ERC721Receiver(_to).onERC721Received.gas(SAFE_TRANSFER_GAS_STIPEND)(_from, _tokenId, _data) == ERC721_RECEIVED);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned in gitter I think we should discuss this STIPEND more and its pros and cons.

@fulldecent
Copy link
Contributor

I am giving a cursory review and everything looks good so far.

@carloschida
Copy link

Congrats on the awesome job! The implementation and tests seem correct. I have only two comments on it.

doMint/addToken and doBurn/removeToken

Consider a contract that is ERC721BasicToken and that implements a public method that calls doMint without any further requirements. Like this, virtually anyone could mint a token to their own account.
My suggestion is to make contract ERC721BasicToken is Ownable and add the modifier onlyOwner to doMint.
In the same line of thought, I'd make addToken a private method so that tokens cannot be added without being minted, ie only via doMint so that the totalTokens count is correctly updated and that the event Transfer is always transmitted.

The same logic applies for the pair doBurn/removeToken.

Practical way of initial massive mint

Consider the following contract:

contract MyERC721 is ERC721BasicToken {
    function MyERC721(uint256 _initialSupply) public {
        for (uint256 i = 0; i < _initialSupply; i++)
            doMint(msg.sender, i);
    }
}

Or even this if we want to save some gas:

contract MyERC721 is ERC721BasicToken {
    function MyERC721(uint256 _initialSupply) public {
        for (uint256 i = 0; i < _initialSupply; i++) {
            tokenOwner[i] = msg.sender;
        }
        ownedTokensCount[msg.sender] = _initialSupply;
        totalTokens = _initialSupply;
    }
}

Both are forbiddingly expensive and limited when it comes to _initialSupply. The maximum number I could assign it in the migrations was 263 (for the second one): deployer.deploy(MyERC721, 263) spent ~0.7 ETH.

My final goal is to make a refundable, capped crowdsale of a ERC721BasicToken. Probably my strategy is wrong. If you guys have a better idea, I'd appreciate your insights.

Copy link

@carloschida carloschida left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments on the assumption that the desired behaviour is that only the owner should be able to mint tokens.

* @param _to address representing the new owner of the given token ID
* @param _tokenId uint256 ID of the token to be added to the tokens list of the given address
*/
function addToken(address _to, uint256 _tokenId) internal {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider making it private rather than internal so that every addition is forced to be made via doMint. In this way we also make sure that the totalTokenscount is always updated and that the corresponding event Transfer is transmitted.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that the ERC721Token.sol full implementation would break if we make this method private, since the ownedTokens fields need to be updated there.

* @param _from address representing the previous owner of the given token ID
* @param _tokenId uint256 ID of the token to be removed from the tokens list of the given address
*/
function removeToken(address _from, uint256 _tokenId) internal {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Foe the same reasons as in addToken, consider changing it to private.

* @param _to The address that will own the minted token
* @param _tokenId uint256 ID of the token to be minted by the msg.sender
*/
function doMint(address _to, uint256 _tokenId) internal {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding the modifier onlyOwner (implying that the contract should be Ownable) so that not anyone can mint tokens.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That'll probably come in a Mintable extension (similar to how ERC20 is structured). I didn't want to enforce ownership for the base implementation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason we went with doMint rather than _mint? likewise for doBurn vs _burn. and likewise for the various do* internal functions throughout the contracts.

Copy link
Contributor Author

@spalladino spalladino Mar 21, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personal preference exclusively. As we were using leading underscores for parameters in functions, I wanted to use something different for the internal methods that actually executed the action, and I had seen the do pattern being used in other languages (such as Java). But I can switch to underscore if preferred.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You see that do thing come up occasionally. Personally I found it adds little semantically. Every method "does" something, that's why they're methods, start using it often and you can find whole objects full of "doMethods".

Underscore is fairly common in solidity as some kind of "private" be it as a prefix or post depending on the needs. I was able to understand this usage quickly on my first exposure to https://github.com/OpenZeppelin/zeppelin-solidity/blob/1eea95f9acebada0360f1f4be7c442325db27fa6/contracts/token/ERC721/ERC721Token.sol#L120

Initial response was "Why do they have a mint method which isn't actually used and this isn't a Mintable?" Then I see addToken and that I'm being given an opportunity to inject other logic into this minting flow (something I functionally require to add extras to an NFT at mint-time).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

personally I prefer the _ approach since it's separate, in my head, from the function arguments, but I don't have a strong opinion. My only note is that this will very much inform future contract standard—I'd expect future additions to have do* instead of _* prefixes as people read the example code and work off of it.

@spalladino
Copy link
Contributor Author

@carloschida thanks for the comments! Our goal is to provide a base implementation here, without forcing a particular ownership model for minting (that's why the methods are internal). We'll probably be providing a Mintable extension (similar to ERC20's) after this is merged. Stay tuned!

@carloschida
Copy link

carloschida commented Mar 9, 2018

@spalladino Thanks for the prompt response. I see. It does make sense to stay agnostic on that matter.
Nevertheless, I'd still consider it risky that a token can be added or burnt without changing adequately the total supply and without transmitting the corresponding events (here I mean only the internal -> private changes on those methods).

facuspagnuolo and others added 11 commits March 9, 2018 17:19
- Tests for new features are pending
- ERC721 is abstract, since it requires metadata implementation
- Move some methods into DeprecatedERC721 contract
- Reorganise base vs full implementation
- Pending tokenByIndex
- Remove restrictions from mock mint and burn calls
This allows token implementation to be non-abstract
We only want to keep the interface, for interacting with already deployed contracts
@shrugs
Copy link
Contributor

shrugs commented Mar 22, 2018

re: transfer to self; I don't really have an opinion on whether or not it should no-op; the spec also doesn't specify. My only opinion is that if we do decide to no-op, it should clear approvals, according to spec (at least, as I've interpreted it).

At the time of any transfer, the approved address for that NFT (if any) is reset to none.

re: gas; If 50k is arbitrary, I'd prefer no limit at all. Who made the original decision on 50k, and what's their rationale?

@spalladino
Copy link
Contributor Author

re: gas; If 50k is arbitrary, I'd prefer no limit at all. Who made the original decision on 50k, and what's their rationale?

@shrugs here is a short discussion on gitter regarding gas, which ends on @eordano agreeing on removing it.

facuspagnuolo
facuspagnuolo previously approved these changes Mar 22, 2018
Copy link
Contributor

@facuspagnuolo facuspagnuolo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

* @return whether there is code in the target address
*/
function isContract(address addr) internal view returns (bool) {
uint size;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: uint256 explicitly?


contract ERC721ReceiverMock is ERC721Receiver {
bytes4 retval;
bool reverts;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the linter will cry if you don't make visibility explicit here

*/
function ownerOf(uint256 _tokenId) public view returns (address) {
address owner = tokenOwner[_tokenId];
require(owner != address(0));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

giving we are using AddressUtils here, what do you think about adding a function like isNotZero or just requireNonZeroAddress or sth like that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather leave that for another refactor


if (_safe) {
require(checkAndCallSafeTransfer(_from, _to, _tokenId, _data));
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we can move this require to the safeTransferFrom function and remove the safe param here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good one!

shrugs
shrugs previously approved these changes Mar 23, 2018
frangio
frangio previously approved these changes Mar 23, 2018
@spalladino spalladino dismissed stale reviews from frangio and shrugs via 3676b55 March 23, 2018 16:38
@frangio frangio merged commit e96164f into OpenZeppelin:master Mar 23, 2018
@mudgen
Copy link

mudgen commented Mar 23, 2018

I found a couple small issues in this function:

  function removeTokenFrom(address _from, uint256 _tokenId) internal {
    super.removeTokenFrom(_from, _tokenId);

    uint256 tokenIndex = ownedTokensIndex[_tokenId];
    uint256 lastTokenIndex = ownedTokens[_from].length.sub(1);
    uint256 lastToken = ownedTokens[_from][lastTokenIndex];

    ownedTokens[_from][tokenIndex] = lastToken;
    ownedTokens[_from][lastTokenIndex] = 0;
    // Note that this will handle single-element arrays. In that case, both tokenIndex and lastTokenIndex are going to
    // be zero. Then we can make sure that we will remove _tokenId from the ownedTokens list since we are first swapping
    // the lastToken to the first position, and then dropping the element placed in the last position of the list

    ownedTokens[_from].length--;
    ownedTokensIndex[_tokenId] = 0;
    ownedTokensIndex[lastToken] = tokenIndex;
  }

First issue: If there is only one element in ownedTokens[_from] then _tokenId and lastToken will be the same. Therefore the last two lines of the function should be switched so that the removed token is not assigned an index in ownedTokensIndex.

It should be like this:

    ownedTokensIndex[lastToken] = tokenIndex;
    ownedTokensIndex[_tokenId] = 0;

Second issue: I think it is clearer, more explicit to use the delete keyword when deleting things. Like this:

delete ownedTokensIndex[_tokenId];

Third issue: I think "lastToken" should be renamed to "lastTokenId" to be consistent with using "_tokenId".

Cheers

@spalladino
Copy link
Contributor Author

Thanks for the review @mudgen! Regarding the first issue:

First issue: If there is only one element in ownedTokens[_from] then _tokenId and lastToken will be the same. Therefore the last two lines of the function should be switched so that the removed token is not assigned an index in ownedTokensIndex.

If there is only one element in ownedTokens[_from], then not only _tokenId and lastToken will be equal, but also tokenIndex will be zero. So the last two lines are doing the same thing, and switching them should change anything. Let me know if I'm missing something though.

@mudgen
Copy link

mudgen commented Mar 23, 2018

@spalladino I looked at it and you are correct and you are not missing anything. tokenIndex will be zero so my earlier statement was incorrect and the current implementation is correct as is. Thanks for pointing that out.

@mudgen
Copy link

mudgen commented Mar 23, 2018

@spalladino what if ownedTokens[_from] is greater than one and lastTokenId and _tokenId are the same because _tokenId is the last one in ownedTokens[_from]. In that case tokenIndex will be greater than 0.

@spalladino
Copy link
Contributor Author

@mudgen let's see a run in that scenario:

// Initial state
_tokenId = 30;
ownedTokens[_from] = [10, 20, 30];
ownedTokensIndex = { 10: 0, 20: 1, 30: 2};

uint256 tokenIndex = ownedTokensIndex[_tokenId]; // tokenIndex = 2
uint256 lastTokenIndex = ownedTokens[_from].length.sub(1); // lastTokenIndex = 2
uint256 lastToken = ownedTokens[_from][lastTokenIndex]; // lastToken = 30

ownedTokens[_from][tokenIndex] = lastToken; // ownedTokens[_from] = [10, 20, 30]
ownedTokens[_from][lastTokenIndex] = 0; // ownedTokens[_from] = [10, 20, 0]

ownedTokens[_from].length--; // ownedTokens[_from] = [10, 20]
ownedTokensIndex[_tokenId] = 0; // ownedTokensIndex = { 10: 0, 20: 1 };
ownedTokensIndex[lastToken] = tokenIndex; // ownedTokensIndex = { 10: 0, 20: 1, 30: 2}

It seems that ownedTokensIndex is not properly cleared. This should not be a major issue, since the token is currently "removed" from its owner, and the index will be corrected on the next "add" call; however, it's not very clean. I'll set up an issue to fix this and add the corresponding tests. Thanks!

@falox
Copy link

falox commented Mar 26, 2018

@spalladino In https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md the approve/transfer functions are external payable, while here are public. Is there an easy way to extend this implementation to get payable functions? Thanks

@spalladino
Copy link
Contributor Author

@falox I'm afraid that if you extend and change it to be payable you'll get a Overriding function changes state mutability from "nonpayable" to "payable" error. I'm reluctant to make the transfer functions payable in the implementation though.

@falox
Copy link

falox commented Mar 27, 2018

@spalladino as I guessed, thanks. May I ask you why you are preferring "nonpayable" to "payable" functions?

I'm asking this because I'm thinking at the use case where a token takes a fee on all transfers, including over-the-counter trades (i.e. in generic ERC721 exchanges and not via specific contract "buy" functions).

@spalladino
Copy link
Contributor Author

@falox it's a tradeoff: though on one hand we are making things more difficult for people who want to add payable functionality to their ERC721s, on the other we are securing all the people who use regular ERC721s, to make sure they don't accidentally send ETH to the token contracts, which would otherwise be lost.

@frangio
Copy link
Contributor

frangio commented Mar 27, 2018

Locking this conversation. Please open new issues for any further doubts or problems!

@OpenZeppelin OpenZeppelin locked as resolved and limited conversation to collaborators Mar 27, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet