-
Notifications
You must be signed in to change notification settings - Fork 11.8k
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
Conversation
55113cf
to
c03b84a
Compare
require(_from != address(0)); | ||
require(_to != address(0)); | ||
require(_to != ownerOf(_tokenId)); | ||
require(ownerOf(_tokenId) == _from); |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
I am giving a cursory review and everything looks good so far. |
Congrats on the awesome job! The implementation and tests seem correct. I have only two comments on it.
|
There was a problem hiding this 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 { |
There was a problem hiding this comment.
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 totalTokens
count is always updated and that the corresponding event Transfer
is transmitted.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
@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! |
@spalladino Thanks for the prompt response. I see. It does make sense to stay agnostic on that matter. |
- 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
279c487
to
6fbe771
Compare
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).
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? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
contracts/AddressUtils.sol
Outdated
* @return whether there is code in the target address | ||
*/ | ||
function isContract(address addr) internal view returns (bool) { | ||
uint size; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)); | ||
} |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good one!
Also rename addToken and removeToken for added clarity
I found a couple small issues in this function:
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:
Second issue: I think it is clearer, more explicit to use the delete keyword when deleting things. Like this:
Third issue: I think "lastToken" should be renamed to "lastTokenId" to be consistent with using "_tokenId". Cheers |
Thanks for the review @mudgen! Regarding the first issue:
If there is only one element in |
@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. |
@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. |
@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 |
@spalladino In https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md the approve/transfer functions are |
@falox I'm afraid that if you extend and change it to be payable you'll get a |
@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). |
@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. |
Locking this conversation. Please open new issues for any further doubts or problems! |
Fixes #640
Fixes #809
Continues the work of @facuspagnuolo in #682
Current status:
Pending discussions:
exists
was added to the interface here, though will not be part of the standardonERC721Received
call was removed (see here)