-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
EIP721 #130
base: staging
Are you sure you want to change the base?
EIP721 #130
Conversation
Not sure why merging into staging isn't appropriately matching the updates merged into master. That's where the conflict is coming from. Let me know if I should rebase or resubmit the PR. |
Fixed @simondlr |
test/eip721/eip721.js
Outdated
}); | ||
|
||
it('creation: create multiple tokens to one user', async () => { | ||
await nft.createToken(accounts[0], { from: accounts[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.
looks like the createToken
method signature only takes one, so maybe the second arg isn't needed?
contracts/eip721/EIP721.sol
Outdated
//2) delete last item (since it's now a duplicate) | ||
delete allTokens[allTokensLength-1]; | ||
//3) reduce length of array | ||
allTokens.length -= 1; |
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.
Is there a possible underflow here?
contracts/eip721/EIP721.sol
Outdated
//2) delete last item (since it's now a duplicate) | ||
delete ownedTokens[_from][ownerLength-1]; | ||
//3) reduce length of array | ||
ownedTokens[_from].length -= 1; |
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.
Is there a possible underflow here?
/// @param _to The new owner | ||
/// @param _tokenId The NFT to transfer | ||
/// @param data Additional data with no specified format, sent in call to `_to` | ||
function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable |
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.
Does this need to be payable
?
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 stated in the PR, I wondered about this too.
EIP721 by default sets transfer functions to add payable to transfer & approve functions. This does not seem the most secure to me. Should we perhaps think of forcing it to not have this by default?
I'm okay with following the standard and keeping payable.
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.
Ahh, sorry. Started the review on the plane, skipped the PR notes. Will review before I dig in again tomorrow.
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.
The standard is actually quite flexible on this item.
So payable is unnecessary to stick with the standard.
/// @dev Throws if `_tokenId` is not a valid NFT. URIs are defined in RFC | ||
/// 3986. The URI may point to a JSON file that conforms to the "ERC721 | ||
/// Metadata JSON Schema". | ||
function tokenURI(uint256 _tokenId) external view returns (string) { |
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 can't find a setter to actually populate the URI mapping?
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.
Yes. It's not in the standard.
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.
The standard also says this function is optional. I would remove 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.
Part of the goal for me was to implement all functionality in one contract only (subjective choice for readability). Removing this means the metadata interface is only partly implemented, which will also break the interface signature.
It's a subjective choice though.
function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable | ||
tokenExists(_tokenId) | ||
allowedToTransfer(_from, _to, _tokenId) { | ||
settleTransfer(_from, _to, _tokenId); |
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 don't think it's a big deal, but since you have a possible revert later in this function, I think settleTransfer()
would be best called at the end.
This would be in keeping with the "Checks, effects, interactions" pattern.
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 don't think that's the case here since if you call the callback before settling the transfer the callback would have no different state to handle
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.
Only problem is that it's a call, which shouldn't that be usually after internal state handling? either/or, not a big problem, but wondering what is the best practice here.
About the gas limit in the specs I actually think that limiting variance of the gas spent in different implementations is good! 👍 |
// Note: the following code is equivalent to: require(getApproved(_tokenId) != 0) || _approved != 0); | ||
// However: I found the following easier to read & understand. | ||
if (approvedOwnerOfToken[_tokenId] == 0 && _approved == 0) { | ||
revert(); // add reason for revert? |
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.
Add a comment such as "Revert on NOOP" for readability.
/// @param _to The new owner | ||
/// @param _tokenId The NFT to transfer | ||
/// @param data Additional data with no specified format, sent in call to `_to` | ||
function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable |
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.
Regarding naming: I believe data
can be _data
(or the other parameters can have no underscore), since the function ID only uses the types of the parameters, not their Solidity names.
contracts/eip721/EIP721.sol
Outdated
|
||
// all tokens | ||
uint256[] internal allTokens; | ||
mapping(uint256 => uint256) internal allTokensIndex; |
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.
Mapping of tokenID => position in allTokens
. Maybe a comment here?
contracts/eip721/EIP721.sol
Outdated
uint256 allTokensLength = allTokens.length; | ||
//1) Put last item into index of token to be removed. | ||
allTokens[allIndex] = allTokens[allTokensLength - 1]; | ||
allTokensIndex[allTokensLength - 1] = allIndex; |
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.
What is this line doing? Is it necessary? Did you mean to do something else? Here is a mental code execution.
At time zero, we have these data structures:
allTokens = [A, B, C]
allTokensIndex = { A : 0, B : 1, C: 2 }
Say we want to get rid of NFT B
. We will correspond line numbers in this code to our mental execution:
286: allIndex = 1
287: allTokensArrayLength = 3
289: allTokens = [A, C, C] // Replace B with the content of the last array item
290: allTokensIndex = { A : 0, B : 1, C: 2, 2 : 1 } // ????
Execution continues and successfully truncates the array:
292: allTokens = [A, C, -]
293: allTokens = [A, C]
But what is happening, or what was supposed to happen on line 290? In a mapping of tokenIds to array indexes, you added an array index as a key with an array index as a value.
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.
Same question on line 303
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 think you found a bug @skmgoldin. Here's what should happen.
Here's a mental exercise.
allTokens = [100, 123, 142, 161, 170] // using larger ids for example
allTokensIndex = { 100: 0, 123: 1, 142: 2, 161: 3, 170: 4 }
Remove Token 123.
286: allIndex = 1
287: allTokensArrayLength = 5
289: allTokens = [100, 170, 142, 161, 170]
Now, in line 290, allTokensIndex of id 170, should be changed to 1 (replacing 123's index). So the line should be:
allTokensIndex[allTokens[allTokensLength-1]] = allIndex;
This takes the id of the last token (170) and changes its index to the index of the token just removes (123).
Can we got a confirm on this @GNSPS @maurelian?
[variable naming is hard]
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 crept through because the tests have the same indexes as its IDs.
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 should basically change the TestImplementation to start ids at 10 [so that it doesn't match the indexes]. Fix incoming.
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.
Confirmed! ❤️ Eternal love to @skmgoldin & crew for doing this bug hunting!
contracts/eip721/EIP721.sol
Outdated
ownedTokensIndex[_tokenId] = ownedTokens[_to].length-1; | ||
} | ||
|
||
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.
_owner
would be a better name for this parameter, since it is used to get the owner of the token.
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.
Both remove/add can be used in context's where it's not the owner specifically that's removing it.
contracts/eip721/EIP721.sol
Outdated
ownerOfToken[_tokenId] = _to; | ||
// add that token to an array keeping track of tokens owned by that address | ||
ownedTokens[_to].push(_tokenId); | ||
// shorten length |
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 comment does not seem to correspond to what is happening. It's actually filling in the position of the just-added token in the owner's token list.
emit Transfer(_from, _to, _tokenId); | ||
} | ||
|
||
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.
_to
should probably be _owner
.
contracts/eip721/EIP721.sol
Outdated
assembly { size := extcodesize(_to) } // solhint-disable-line no-inline-assembly | ||
if (size > 0) { | ||
// call on onERC721Received. | ||
require(EIP721TokenReceiverInterface(_to).onERC721Received(_from, _tokenId, data) == 0xf0b9e5ba); |
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.
Why not add a constant for this signature like you have for others towards the top of the file?
contracts/eip721/EIP721.sol
Outdated
mapping(uint256 => address) internal approvedOwnerOfToken; | ||
|
||
// An operator is allowed to manage all assets of another owner. | ||
mapping(address => mapping (address => bool)) internal approvedOperators; |
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.
Could this just be called operators? It technically contains all operators, just with their boolean value flag set to false. Like, if I have some operator who I approve, and then I revoke the approval, we make the revocation assignment to the _approved_Operators mapping, which seems semantically questionable.
Thanks @skmgoldin for finding that bug. Thanks @maurelian @GNSPS @akuanti for inputs. I fixed the bug and added some stylistic changes. |
Seems there's some tiny changes in the standard coming in. Notably, 50k gas is removed (we didn't include it). ethereum/EIPs#721 (comment) |
There's been 3 changes to the spec. Minor changes.
|
Since the last call, some changes have been made. Notably: the on receive function added an "operator" so that the recipient knows who a token belongs to if it was transferred to them. fulldecent/EIPs@2778813#diff-a87271f8186c59c075e229cd7421f205 There's also small stylistic changes to documentation to improve the language and remove ambiguity. Will need to add this. |
Bumped it FINAL spec on ERC721 and tidied the PR some more. Ready for final review. |
Here is the updated reference implementation / https://github.com/0xcert/ethereum-erc721 |
Hey all.
Here's a WIP of EIP721 along with an extensive test suite. https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md.
EIP721 is non-fungible token standard of the Ethereum community. It contains several interfaces for implementing NFT:
The goal was to implement without too many dependencies into one specific EIP721 contract. It borrows from OpenZeppelin's implementation with various stylistic changes.
There's some things that I want to point out:
Please Double Check Conformity to EIP721 standard
Please help double check if I got all functions & interfaces & recommendations.
Naming
I spent a lot of time thinking about different naming of variables. Having to keep track of tokens using index arrays makes it complicated and sometimes cumbersome. Are the variable naming clear? If not, are there any other recommendations? I feel it could be even more clearer, but had trouble thinking up appropriate names.
EIP721 itself has inconsistent naming: eg usage of
approve
to denote approving one owner per token ID vssetApprovalForAll
which denotes setting a specific operator to be able to transfer & approve all tokens of a specific owner.Other inconsistencies in the standard include:
function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;
. Event logs also hate dangling underscores since eslint shouts about it.Even though there are inconsistencies in the standard, I'd err on not changing an this, but the things we can change, I want more advice on this.
No Function To Retrieve Token Arrays
EIP721 does not specify a way to retrieve arrays of tokens. This seems like a shortsight? In the enumerable interface, it would've been nice to retrieve batches of arrays. Is this an extension we want to include perhaps? eg, return all tokens or return an owner's tokens. Otherwise, currently, you'll have to enumerate yourself with several eth_calls vs one eth_call.
Default Payable?
EIP721 by default sets transfer functions to add payable to transfer & approve functions. This does not seem the most secure to me. Should we perhaps think of forcing it to not have this by default?
Blocking onERC721Received
The standard specifies that receiver should not consumer more than 50000 gas. I find this an odd restriction.
This function MUST use 50,000 gas or less
.What are thoughts on this?
Revert Reason
During the development of this EIP721, it became possible to add reasons for reverts (eg, using revert or require). Do we want to add this? I think it could help. > 0.4.22 of Solidity. I think for now, best to keep it simple, but in the future, perhaps upgrade to add in reasons. :)
Naming Implementation
The only unimplemented interface is naming & symbols. The reason being that's specified as
external pure
. This means you can't even read state, so the only way to implement naming is to hardcode it in the function, which I feel is stylistically awkward. egSo, I changed to public as visibility specifier. This however does not change the interface signatures.
This PR also contains another bump of Truffle 4.1.7.
For now that it is it. I will update if more comes to mind.
Here's a pic of a puppy. NOTE: This is not a collectible. Do not attempt to buy it. ;)