-
Notifications
You must be signed in to change notification settings - Fork 11.9k
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
Implement ERC721 standard #627
Implement ERC721 standard #627
Conversation
Looking good! Some comments:
|
.gitignore
Outdated
@@ -1,5 +1,6 @@ | |||
*.swp | |||
*.swo | |||
.idea |
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.
🙁
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'll move it to .git/info/exclude
contracts/token/AssetToken.sol
Outdated
*/ | ||
function removeToken(address _from, uint256 _tokenId) internal { | ||
uint256 length = balanceOf(_from); | ||
if(length <= 1) delete ownedTokens[_from][_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'd go with:
require(length > 0);
if (length == 1) {
delete ownedTokens[_from][_tokenId];
delete ownedTokens[_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.
good point, actually there's no need to handle any specific scenario for single element arrays
5958386
to
4173800
Compare
@eordano thanks for reviewing! I addressed many of your comments. Regarding the name that we should use for the implementation, I'm in favour of using the one that describes what the implementation does better. That said, I really like both On the other hand, we've been thinking about the scope of this implementation with @frangio and we decided not to cover all the optional functionality, but some of them. For example, we think it could be better to leave the approval-all or transfer-and-call functionalities for further implementations. Thoughts? We finally decided to add a basic mint and burn functionalities too. Please review :D |
I think cc: @frangio |
nit: maybe add the |
I read your last comment of the ERC721 thread, and it convinced me. I will work on an |
+1 on the additional functionality encapsulated in another contract |
a95016b
to
c099c97
Compare
c099c97
to
a4d3036
Compare
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.
Really incredible test coverage, I'm so impressed/happy about this.
Some questions:
- what's the argument for only a single address being able to be approved or eternallyApproved?
- do we need a
takeOwnershipFor(address _add, utin256 _tokenId)
function to allow contracts that are eternally approved to take ownership of a token on behalf of a user? - do we want to include a convenience "multi transfer" function to make n-of-n atomic swaps easier?
contracts/token/ERC721.sol
Outdated
@@ -0,0 +1,21 @@ | |||
pragma solidity ^0.4.11; |
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.
OZ is on 0.4.18 atm, here and below
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.
my bad!
contracts/token/NonFungibleToken.sol
Outdated
} | ||
|
||
/** | ||
* @dev Gets then token ID at a given index of the tokens list of the requested owner |
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.
nitpick: then
-> the
contracts/token/NonFungibleToken.sol
Outdated
* @param _tokenId uint256 ID of the token being burned by the msg.sender | ||
*/ | ||
function burn(uint256 _tokenId) onlyOwnerOf(_tokenId) public { | ||
if(approvedFor(_tokenId) != 0) clearApproval(msg.sender, _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'm personally very into requiring syntax like
if (condition) {
doThing()
}
regardless of its simplicity. does OZ have a stance on 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 think we don't but I agree
test/NonFungibleToken.test.js
Outdated
}); | ||
}); | ||
|
||
describe('when the given address owns some tokens', function () { |
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 not own some tokens"?
test/NonFungibleToken.test.js
Outdated
}); | ||
|
||
describe('when the given address is the zero address', function () { | ||
const to = 0x0; |
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 expect this works cause web3 casts it behind the scenes, but should we make a constant for the null address and use that here and around the test file instead of 0x0000000000000000000000000000000000000000
?
0x calls this NULL_ADDRESS
but we can use ZERO_ADDRESS
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.
like the idea!
contracts/token/NonFungibleToken.sol
Outdated
*/ | ||
function removeToken(address _from, uint256 _tokenId) internal { | ||
require(balanceOf(_from) > 0); | ||
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.
these requires are implied by all of the code paths that reach removeToken
, so I think we can technically remove them. that said, being explicit about it is 👍 - just making sure that was the intention
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 placed those requires there because I wanted to give an internal interface to handle the list of tokens. If someone extends our implementation, given that the state variables are private, the only way they will be able to change the contract's state will be through our interface. This is just to ensure our users that they will be handling this correctly reducing the attack surface. What do you think?
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.
Great point, let's definitely keep them
test/NonFungibleToken.test.js
Outdated
describe('approveAll', function () { | ||
const sender = _creator; | ||
|
||
describe('when the address willing to approve is the 0 address', function () { |
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 it worth including this test case? the code doesn't react differently based on _to
being the 0-address or not
(up to you)
1572511
to
0d9efcf
Compare
@frangio please review |
Tomorrow we'll be introducing the ERC 821: DARs (Distinguishable Asset Registries). We're writing a blogpost and the final text of the standard, would like you to take a look at it and hear your feedback I'd go against (2), cementing this implementation. I want a great standard, not a good enough one... |
By the way, tomorrow is the https://raredigitalartfestival.splashthat.com/ conference! |
contracts/token/ERC721.sol
Outdated
event Transfer(address indexed _from, address indexed _to, uint256 _tokenId); | ||
event Approval(address indexed _owner, address indexed _approved, uint256 _tokenId); | ||
|
||
function name() public view returns (string _name); |
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.
Since name
and symbol
are optional, they shouldn't be part of the ERC721
interface.
contracts/token/NonFungibleToken.sol
Outdated
using SafeMath for uint256; | ||
|
||
// Token name | ||
string internal _name; |
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.
Since name
and symbol
are optional, I thought we were going to include them in a different contract in #640.
contracts/token/NonFungibleToken.sol
Outdated
* @return owner address currently marked as the owner of the given token ID | ||
*/ | ||
function ownerOf(uint256 _tokenId) public view returns (address owner) { | ||
owner = tokenOwner[_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.
We don't use this style of returning values in the rest of OpenZeppelin, and I'd use an explicit return here to be consistent.
contracts/token/NonFungibleToken.sol
Outdated
* @return uint256 token ID at the given index of the tokens list owned by the requested address | ||
*/ | ||
function tokenOfOwnerByIndex(address _owner, uint256 _index) public view returns (uint256) { | ||
require(_index < balanceOf(_owner)); |
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 check isn't necessary because Solidity does bounds checking on array accesses.
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.
Solidity throws when checking array bounds, and we are reverting instead
contracts/token/NonFungibleToken.sol
Outdated
*/ | ||
function transfer(address _to, uint _tokenId, bytes _data) public onlyOwnerOf(_tokenId) { | ||
clearApprovalAndTransfer(msg.sender, _to, _tokenId); | ||
require(_to.call.value(0)(_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.
The .value(0)
part isn't necessary, because it's the default. We might want to keep it for explicitness, but I'd rather remove it because like this a single additional character can make ether be sent. 😰
contracts/token/NonFungibleToken.sol
Outdated
* @param _approved representing the status of the approval | ||
*/ | ||
function approve(address _to, bool _approved) public { | ||
require(_to != msg.sender); // we are reverting here following the same behaviour as approve does |
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 comment is confusing because this function is called approve
as well. I'm not comfortable with that... Have we considered other names for this one?
contracts/token/NonFungibleToken.sol
Outdated
* @param _to address which you want to transfer the token to | ||
* @param _tokenId uint256 ID of the token being claimed by the msg.sender | ||
*/ | ||
function takeOwnershipFor(address _to, uint256 _tokenId) public onlyApprovedFor(_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.
Hmmm, this name... But I see that this is the least agreed upon part in the ERC.
Did we make the name up or steal it from somewhere else?
83fe097
to
4ccf668
Compare
4ccf668
to
5b50e99
Compare
Based on what we discussed, I'm rollbacking this implementation to hold just the required functionality of the ERC721 standard. Detailed implementation and approveAll/transfer&call functionalities will be provided in another PRs |
contracts/token/ERC721Token.sol
Outdated
* @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 mint(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.
Child contracts are not allowed to change the visibility of inherited functions, so a child contract here won't be able to define public functions called mint
or burn
. Since those are the names of the public functions in Mintable
and Burnable
ERC20 tokens, I think I would rename these to avoid the potential clash.
Perhaps _mint
and _burn
, or doMint
and doBurn
...
contracts/token/ERC721Token.sol
Outdated
* @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.
I'm thinking that addToken
and removeToken
should be private instead of internal, given that we already expose mint
and burn
as internal, which guarantee the correct emision of Transfer
events as required by the spec.
contracts/token/ERC721Token.sol
Outdated
* @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 { | ||
require(balanceOf(_from) > 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.
I would remove this require
because it should be implied by the second one. If _from
owns _tokenId
it will have a balance of at least 1, as guaranteed by addToken
.
|
||
tokenOwner[_tokenId] = 0; | ||
ownedTokens[_from][tokenIndex] = lastToken; | ||
ownedTokens[_from][lastTokenIndex] = 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.
Since it's kind of magical that the algorithm works when there is only one item in the list, what do you think about adding a brief note explaining it?
In particular, it feels worthy of mention that the order of lines 193 and 194 is important, because if _tokenId
is the only item in the list, in this way it is properly deleted from storage.
contracts/token/ERC721Token.sol
Outdated
* @param _tokenId uint256 ID of the token to query the approval of | ||
* @return bool whether the msg.sender is approved for the given token ID or not | ||
*/ | ||
function isApprovedFor(uint256 _tokenId) internal view 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.
I'm not comfortable with helpers not being pure. In this case it uses the global msg.sender
property of the transaction. I would add an explicit address
parameter to make it pure.
I was going to say that the helper should be private or that we should consider removing it, but it may be an internal helper with the purpose of being overriden, such as in the Operatable subcontract. If so, should we make that explicit in the comments?
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.
Sorry, but I don't see how this function can be pure since it is reading the contract state. Would you mind giving further details?
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.
My bad! I didn't use "pure" in a precise way.
Agreed offline to add an additional address parameter.
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.
done!
65e6fb2
to
23afc74
Compare
@facuspagnuolo Shall this be treated as an intermediate reference implementation? As there is an effort still going on about finalisation of ERC721 here |
…ent_erc721_standard Implement ERC721 standard
This PR tackles #615, it should be merged after #628
@eordano was working on this issue too, but unfortunately, I heard about it once this solution was almost done. I'm pushing this PR just to see how we can merge both jobs.
fixes #615