Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
1192e68
Rename current ERC721 implementation to BaseERC721
facuspagnuolo Feb 26, 2018
ca163a8
Implement ERC721 optional & approveAll functionality
facuspagnuolo Feb 26, 2018
71cbc51
Support for new ERC721 interface
spalladino Mar 7, 2018
3745025
Add more tests for ERC721
spalladino Mar 8, 2018
559df81
Implement suggestions by @dekz
spalladino Mar 8, 2018
d726c79
Update comments in ERC721 contracts
spalladino Mar 8, 2018
54a1d2e
Implement tokensByIndex extension
spalladino Mar 9, 2018
851685c
Add default implementation for metadata URI
spalladino Mar 9, 2018
3cef880
Allow operators to call approve on a token
spalladino Mar 9, 2018
6f180a6
Remove gas stipend restriction in call to 721 receiver
spalladino Mar 9, 2018
6fbe771
Remove deprecated implementation
spalladino Mar 9, 2018
626742e
Add notice to isContract helper on constract constructors
spalladino Mar 20, 2018
95a1f9a
Change natspec delimiters for consistency
spalladino Mar 21, 2018
15f9556
Minor linting fixes
spalladino Mar 21, 2018
b332995
Add constant modifier to ERC721_RECEIVED magic value
spalladino Mar 21, 2018
f4748da
Use 4-params safeTransferFrom for implementing the 3-params overload
spalladino Mar 21, 2018
fb4f728
Minor text changes in natspec comments
spalladino Mar 21, 2018
6b98e4e
Use address(0) instead of 0 or 0x0
spalladino Mar 21, 2018
3f2ea8a
Use if-statements instead of boolean one-liners for clarity
spalladino Mar 21, 2018
74db03b
Keep ownedTokensCount state var in sync in full ERC721 implementation
spalladino Mar 21, 2018
981c6f7
Fix incorrect comparison when burning ERC721 tokens with metadata
spalladino Mar 21, 2018
73b77ae
Use address(0) instead of 0 in one more place in ERC721
spalladino Mar 21, 2018
eee5b0e
Throw when querying balance for the zero address
spalladino Mar 21, 2018
9deb637
Update links to approved version of EIP721
spalladino Mar 21, 2018
fe6e4ff
Use explicit size for uint
spalladino Mar 22, 2018
4836279
Remove unneeded internal function in ERC721
spalladino Mar 22, 2018
619ae84
Use underscore instead of 'do' prefix for internal methods in ERC721
spalladino Mar 22, 2018
2e593f2
Fix failing test due to events reordering in ERC721 safe transfer
spalladino Mar 22, 2018
6c09d20
Fix bug introduced in 74db03ba06
spalladino Mar 22, 2018
37929c8
Remove do prefix for internal setTokenUri method
spalladino Mar 22, 2018
3676b55
Allow transfers to self in ERC721
spalladino Mar 23, 2018
7815cc5
Merge branch 'master' into feature/full_erc721
frangio Mar 23, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Implement tokensByIndex extension
- Remove restrictions from mock mint and burn calls
  • Loading branch information
spalladino committed Mar 9, 2018
commit 54a1d2eaccfaaa191f6cf26b28fa590708dfd03f
2 changes: 1 addition & 1 deletion contracts/mocks/ERC721BasicTokenMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ contract ERC721BasicTokenMock is ERC721BasicToken {
}

function burn(uint256 _tokenId) public {
super.doBurn(_tokenId);
super.doBurn(ownerOf(_tokenId), _tokenId);
}
}
12 changes: 9 additions & 3 deletions contracts/mocks/ERC721TokenMock.sol
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
pragma solidity ^0.4.18;

import "./ERC721BasicTokenMock.sol";
import "../token/ERC721/ERC721Token.sol";

/**
* @title ERC721TokenMock
* This mock just provides a public mint and burn functions for testing purposes,
* and a mock metadata URI implementation
*/
contract ERC721TokenMock is ERC721Token, ERC721BasicTokenMock {
contract ERC721TokenMock is ERC721Token {
function ERC721TokenMock(string name, string symbol)
ERC721BasicTokenMock()
ERC721Token(name, symbol)
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure if we have a convention for this, but I think modifiers should be indented.

public
{ }

function mint(address _to, uint256 _tokenId) public {
doMint(_to, _tokenId);
}

function burn(uint256 _tokenId) public {
doBurn(ownerOf(_tokenId), _tokenId);
}

// Mock implementation for testing.
// Do not use this code in production!
function tokenURI(uint256 _tokenId) public view returns (string) {
Expand Down
2 changes: 1 addition & 1 deletion contracts/token/ERC721/ERC721.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import "./ERC721Basic.sol";
contract ERC721Enumerable is ERC721Basic {
function totalSupply() public view returns (uint256);
function tokenOfOwnerByIndex(address _owner, uint256 _index) public view returns (uint256 _tokenId);
// function tokenByIndex(uint256 _index) public view returns (uint256);
function tokenByIndex(uint256 _index) public view returns (uint256);
}

/// @title ERC-721 Non-Fungible Token Standard, optional metadata extension
Expand Down
13 changes: 4 additions & 9 deletions contracts/token/ERC721/ERC721BasicToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@ contract ERC721BasicToken is ERC721Basic {
// Equals to bytes4(keccak256("onERC721Received(address,uint256,bytes)"))
bytes4 ERC721_RECEIVED = 0xf0b9e5ba;
Copy link
Contributor

Choose a reason for hiding this comment

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

We can replace this constant with the less magic ERC721Receiver(0).onERC721Received.selector. However, this is not considered a compile time constant (as of Solidity 0.4.21) so it cannot be saved in a constant variable without a warning. Since it is less magic it could be inlined.

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.

TBH, if it is not considered a constant at compile time, I'd rather keep the code warns-free. I'm adding a constant modifier, and the way to obtain the value you suggested as a comment.


// Total amount of tokens
uint256 internal totalTokens;

// Mapping from token ID to owner
mapping (uint256 => address) internal tokenOwner;

Expand Down Expand Up @@ -194,7 +191,6 @@ contract ERC721BasicToken is ERC721Basic {
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.

require(_to != address(0));
addToken(_to, _tokenId);
totalTokens = totalTokens.add(1);
Transfer(0x0, _to, _tokenId);
}

Expand All @@ -203,11 +199,10 @@ contract ERC721BasicToken is ERC721Basic {
* @dev Reverts if the token does not exist
* @param _tokenId uint256 ID of the token being burned by the msg.sender
*/
function doBurn(uint256 _tokenId) onlyOwnerOf(_tokenId) internal {
clearApproval(msg.sender, _tokenId);
removeToken(msg.sender, _tokenId);
totalTokens = totalTokens.sub(1);
Transfer(msg.sender, 0x0, _tokenId);
function doBurn(address _owner, uint256 _tokenId) internal {
clearApproval(_owner, _tokenId);
removeToken(_owner, _tokenId);
Transfer(_owner, 0x0, _tokenId);
}

/**
Expand Down
53 changes: 52 additions & 1 deletion contracts/token/ERC721/ERC721Token.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ contract ERC721Token is ERC721, ERC721BasicToken {
// Mapping from token ID to index of the owner tokens list
mapping(uint256 => uint256) internal ownedTokensIndex;

// Array with all token ids, used for enumeration
uint256[] internal allTokens;

// Mapping from token id to position in the allTokens array
mapping(uint256 => uint256) internal allTokensIndex;

Choose a reason for hiding this comment

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

I may be missing something, but why is this mapping needed? As per the mint function, isn't the token's identifier the same thing as its index in the allTokens array?

allTokensIndex[_tokenId] = allTokens.length;

The only other place this gets read is during burn, where you could just use the _tokenId to infer location instead of this mapping.

Copy link
Contributor

@frangio frangio Mar 21, 2018

Choose a reason for hiding this comment

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

When a token is burnt, the one with the highest id is put in the place of the burnt one in the allTokens array.

https://github.com/spalladino/zeppelin-solidity/blob/6fbe771c276f83e734256908877c8f9aaa2e1bfc/contracts/token/ERC721/ERC721Token.sol#L189

In this way the index ⇔ id equivalence is invalidated.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As per the mint function, isn't the token's identifier the same thing as its index in the allTokens array?

Yes, but only because mint generates autoincremental token IDs. I didn't want to rely on this for implementing the enumeration, as anyone should be able to override this behaviour if needed.

Choose a reason for hiding this comment

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

Thanks guys for the clarification!


/**
* @dev Constructor function
*/
Expand Down Expand Up @@ -73,7 +79,18 @@ contract ERC721Token is ERC721, ERC721BasicToken {
* @return uint256 representing the total amount of tokens
*/
function totalSupply() public view returns (uint256) {
return totalTokens;
return allTokens.length;
}

/**
* @dev Gets the token ID at a given index of all the tokens in this contract
* @dev Reverts if the index is greater or equal to the total number of tokens
* @param _index uint256 representing the index to be accessed of the tokens list
* @return uint256 token ID at the given index of the tokens list
*/
function tokenByIndex(uint256 _index) public view returns (uint256) {
require(_index < totalSupply());
return allTokens[_index];
}

/**
Expand Down Expand Up @@ -113,4 +130,38 @@ contract ERC721Token is ERC721, ERC721BasicToken {
ownedTokensIndex[lastToken] = tokenIndex;
}

/**
* @dev Internal function to mint a new token
* @dev Reverts if the given token ID already exists
* @param _to address the beneficiary 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 {
super.doMint(_to, _tokenId);

allTokensIndex[_tokenId] = allTokens.length;
allTokens.push(_tokenId);
}

/**
* @dev Internal function to burn a specific token
* @dev Reverts if the token does not exist
* @param _owner owner of the token to burn
* @param _tokenId uint256 ID of the token being burned by the msg.sender
*/
function doBurn(address _owner, uint256 _tokenId) internal {
super.doBurn(_owner, _tokenId);

uint256 tokenIndex = allTokensIndex[_tokenId];
uint256 lastTokenIndex = allTokens.length.sub(1);
uint256 lastToken = allTokens[lastTokenIndex];

allTokens[tokenIndex] = lastToken;
allTokens[lastTokenIndex] = 0;

allTokens.length--;
allTokensIndex[_tokenId] = 0;
allTokensIndex[lastToken] = tokenIndex;
}

}
10 changes: 5 additions & 5 deletions test/token/ERC721/ERC721BasicToken.behaviour.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,13 +138,13 @@ export default function shouldBehaveLikeERC721BasicToken (accounts) {
});

it('adjusts owners tokens by index', async function () {
if (!this.token.tokensOfOwnerByIndex) return;
if (!this.token.tokenOfOwnerByIndex) return;

const newOwnerToken = await this.tokensOfOwnerByIndex(this.to, 0);
newOwnerToken.should.be.equal(tokenId);
const newOwnerToken = await this.token.tokenOfOwnerByIndex(this.to, 0);
newOwnerToken.toNumber().should.be.equal(tokenId);

const previousOwnerToken = await this.tokensOfOwnerByIndex(owner, 0);
previousOwnerToken.should.not.be.equal(tokenId);
const previousOwnerToken = await this.token.tokenOfOwnerByIndex(owner, 0);
previousOwnerToken.toNumber().should.not.be.equal(tokenId);
});
};

Expand Down
20 changes: 0 additions & 20 deletions test/token/ERC721/ERC721MintBurn.behaviour.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,6 @@ export default function shouldMintAndBurnERC721Token (accounts) {
balance.should.be.bignumber.equal(1);
});

it('adjusts owner tokens by index', async function () {
if (!this.token.tokensOfOwnerByIndex) return;

const token = await this.tokensOfOwnerByIndex(to, 0);
token.should.be.equal(tokenId);
});

it('emits a transfer event', async function () {
logs.length.should.be.equal(1);
logs[0].event.should.be.eq('Transfer');
Expand Down Expand Up @@ -86,13 +79,6 @@ export default function shouldMintAndBurnERC721Token (accounts) {
balance.should.be.bignumber.equal(1);
});

it('removes that token from the token list of the owner', async function () {
if (!this.token.tokensOfOwnerByIndex) return;

const token = await this.tokensOfOwnerByIndex(sender, 0);
token.should.not.be.equal(tokenId);
});

it('emits a burn event', async function () {
logs.length.should.be.equal(1);
logs[0].event.should.be.eq('Transfer');
Expand Down Expand Up @@ -126,12 +112,6 @@ export default function shouldMintAndBurnERC721Token (accounts) {
});
});

describe('when the msg.sender does not own given token', function () {
it('reverts', async function () {
await assertRevert(this.token.burn(tokenId, { from: accounts[1] }));
});
});

describe('when the given token ID was not tracked by this contract', function () {
it('reverts', async function () {
await assertRevert(this.token.burn(unknownTokenId, { from: creator }));
Expand Down
130 changes: 111 additions & 19 deletions test/token/ERC721/ERC721Token.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import assertRevert from '../../helpers/assertRevert';
import shouldBehaveLikeERC721BasicToken from './ERC721BasicToken.behaviour';
import shouldMintAndBurnERC721Token from './ERC721MintBurn.behaviour';
import _ from 'lodash';

const BigNumber = web3.BigNumber;
const ERC721Token = artifacts.require('ERC721TokenMock.sol');
Expand All @@ -13,8 +14,8 @@ require('chai')
contract('ERC721Token', function (accounts) {
const name = 'Non Fungible Token';
const symbol = 'NFT';
const firstTokenId = 1;
const secondTokenId = 2;
const firstTokenId = 100;
const secondTokenId = 200;
const creator = accounts[0];

beforeEach(async function () {
Expand All @@ -30,6 +31,51 @@ contract('ERC721Token', function (accounts) {
await this.token.mint(creator, secondTokenId, { from: creator });
});

describe('mint', function () {
const to = accounts[1];
const tokenId = 3;

beforeEach(async function () {
await this.token.mint(to, tokenId);
});

it('adjusts owner tokens by index', async function () {
const token = await this.token.tokenOfOwnerByIndex(to, 0);
token.toNumber().should.be.equal(tokenId);
});

it('adjusts all tokens list', async function () {
const newToken = await this.token.tokenByIndex(2);
newToken.toNumber().should.be.equal(tokenId);
});
});

describe('burn', function () {
const tokenId = firstTokenId;
const sender = creator;

beforeEach(async function () {
await this.token.burn(tokenId, { from: sender });
});

it('removes that token from the token list of the owner', async function () {
const token = await this.token.tokenOfOwnerByIndex(sender, 0);
token.toNumber().should.be.equal(secondTokenId);
});

it('adjusts all tokens list', async function () {
const token = await this.token.tokenByIndex(0);
token.toNumber().should.be.equal(secondTokenId);
});

it('burns all tokens', async function () {
await this.token.burn(secondTokenId, { from: sender });
const total = await this.token.totalSupply();
total.toNumber().should.be.equal(0);
await assertRevert(this.token.tokenByIndex(0));
});
});

describe('metadata', function () {
it('has a name', async function () {
const name = await this.token.name();
Expand All @@ -56,32 +102,78 @@ contract('ERC721Token', function (accounts) {
});

describe('tokenOfOwnerByIndex', function () {
describe('when the given address owns some tokens', function () {
const owner = creator;
const owner = creator;
const another = accounts[1];

describe('when the given index is lower than the amount of tokens owned by the given address', function () {
it('returns the token ID placed at the given index', async function () {
const tokenId = await this.token.tokenOfOwnerByIndex(owner, 0);
tokenId.should.be.bignumber.equal(firstTokenId);
});
});

describe('when the index is greater than or equal to the total tokens owned by the given address', function () {
it('reverts', async function () {
await assertRevert(this.token.tokenOfOwnerByIndex(owner, 2));
});
});

describe('when the given index is lower than the amount of tokens owned by the given address', function () {
const index = 0;
describe('when the given address does not own any token', function () {
it('reverts', async function () {
await assertRevert(this.token.tokenOfOwnerByIndex(another, 0));
});
});

it('returns the token ID placed at the given index', async function () {
const tokenId = await this.token.tokenOfOwnerByIndex(owner, index);
tokenId.should.be.bignumber.equal(firstTokenId);
});
describe('after transferring all tokens to another user', function () {
beforeEach(async function () {
await this.token.transferFrom(owner, another, firstTokenId, { from: owner });
await this.token.transferFrom(owner, another, secondTokenId, { from: owner });
});

describe('when the index is greater than or equal to the total tokens owned by the given address', function () {
const index = 2;
it('returns correct token IDs for target', async function () {
const count = await this.token.balanceOf(another);
count.toNumber().should.be.equal(2);
const tokensListed = await Promise.all(_.range(2).map(i => this.token.tokenOfOwnerByIndex(another, i)));
tokensListed.map(t => t.toNumber()).should.have.members([firstTokenId, secondTokenId]);
});

it('reverts', async function () {
await assertRevert(this.token.tokenOfOwnerByIndex(owner, index));
});
it('returns empty collection for original owner', async function () {
const count = await this.token.balanceOf(owner);
count.toNumber().should.be.equal(0);
await assertRevert(this.token.tokenOfOwnerByIndex(owner, 0));
});
});
});

describe('when the given address does not own any token', function () {
const owner = accounts[1];
describe('tokenByIndex', function () {
it('should return all tokens', async function () {
const tokensListed = await Promise.all(_.range(2).map(i => this.token.tokenByIndex(i)));
tokensListed.map(t => t.toNumber()).should.have.members([firstTokenId, secondTokenId]);
});

it('reverts', async function () {
await assertRevert(this.token.tokenOfOwnerByIndex(owner, 0));
it('should revert if index is greater than supply', async function () {
await assertRevert(this.token.tokenByIndex(2));
});

[firstTokenId, secondTokenId].forEach(function (tokenId) {
it(`should return all tokens after burning token ${tokenId} and minting new tokens`, async function () {
const owner = accounts[0];
const newTokenId = 300;
const anotherNewTokenId = 400;

await this.token.burn(tokenId, { from: owner });
await this.token.mint(owner, newTokenId, { from: owner });
await this.token.mint(owner, anotherNewTokenId, { from: owner });

const count = await this.token.totalSupply();
count.toNumber().should.be.equal(3);

const tokensListed = await Promise.all(_.range(3).map(i => this.token.tokenByIndex(i)));
const expectedTokens = _.filter(
[firstTokenId, secondTokenId, newTokenId, anotherNewTokenId],
x => (x !== tokenId)
);
tokensListed.map(t => t.toNumber()).should.have.members(expectedTokens);
});
});
});
Expand Down