Commit 4b33eaef by Nicolás Venturo Committed by Francisco Giordano

Improved ERC721 granularity (#1304)

* Split enumerable and metadata implementations.

* Renamed ERC721Basic to ERC721, and ERC721 to ERC721Full.

* Fixed linter errors.
parent bafdcf07
pragma solidity ^0.4.24;
import "../token/ERC721/ERC721Basic.sol";
/**
* @title ERC721BasicMock
* This mock just provides a public mint and burn functions for testing purposes
*/
contract ERC721BasicMock is ERC721Basic {
function mint(address to, uint256 tokenId) public {
_mint(to, tokenId);
}
function burn(uint256 tokenId) public {
_burn(ownerOf(tokenId), tokenId);
}
}
pragma solidity ^0.4.24;
import "../token/ERC721/ERC721Full.sol";
import "../token/ERC721/ERC721Mintable.sol";
import "../token/ERC721/ERC721Burnable.sol";
/**
* @title ERC721Mock
* This mock just provides a public mint and burn functions for testing purposes,
* and a public setter for metadata URI
*/
contract ERC721FullMock is ERC721Full, ERC721Mintable, ERC721Burnable {
constructor(string name, string symbol) public
ERC721Mintable()
ERC721Full(name, symbol)
{}
function exists(uint256 tokenId) public view returns (bool) {
return _exists(tokenId);
}
function setTokenURI(uint256 tokenId, string uri) public {
_setTokenURI(tokenId, uri);
}
function removeTokenFrom(address from, uint256 tokenId) public {
_removeTokenFrom(from, tokenId);
}
}
pragma solidity ^0.4.24; pragma solidity ^0.4.24;
import "../token/ERC721/ERC721.sol"; import "../token/ERC721/ERC721Full.sol";
import "../token/ERC721/ERC721Mintable.sol"; import "../token/ERC721/ERC721Mintable.sol";
import "../token/ERC721/ERC721Burnable.sol"; import "../token/ERC721/ERC721Burnable.sol";
...@@ -8,10 +8,12 @@ import "../token/ERC721/ERC721Burnable.sol"; ...@@ -8,10 +8,12 @@ import "../token/ERC721/ERC721Burnable.sol";
/** /**
* @title ERC721MintableBurnableImpl * @title ERC721MintableBurnableImpl
*/ */
contract ERC721MintableBurnableImpl is ERC721, ERC721Mintable, ERC721Burnable { contract ERC721MintableBurnableImpl
is ERC721Full, ERC721Mintable, ERC721Burnable {
constructor() constructor()
ERC721Mintable() ERC721Mintable()
ERC721("Test", "TEST") ERC721Full("Test", "TEST")
public public
{ {
} }
......
pragma solidity ^0.4.24; pragma solidity ^0.4.24;
import "../token/ERC721/ERC721.sol"; import "../token/ERC721/ERC721.sol";
import "../token/ERC721/ERC721Mintable.sol";
import "../token/ERC721/ERC721Burnable.sol";
/** /**
* @title ERC721Mock * @title ERC721Mock
* This mock just provides a public mint and burn functions for testing purposes, * This mock just provides a public mint and burn functions for testing purposes
* and a public setter for metadata URI
*/ */
contract ERC721Mock is ERC721, ERC721Mintable, ERC721Burnable { contract ERC721Mock is ERC721 {
constructor(string name, string symbol) public function mint(address to, uint256 tokenId) public {
ERC721Mintable() _mint(to, tokenId);
ERC721(name, symbol)
{}
function exists(uint256 tokenId) public view returns (bool) {
return _exists(tokenId);
}
function setTokenURI(uint256 tokenId, string uri) public {
_setTokenURI(tokenId, uri);
} }
function removeTokenFrom(address from, uint256 tokenId) public { function burn(uint256 tokenId) public {
_removeTokenFrom(from, tokenId); _burn(ownerOf(tokenId), tokenId);
} }
} }
pragma solidity ^0.4.24;
import "./IERC721Enumerable.sol";
import "./ERC721.sol";
import "../../introspection/ERC165.sol";
contract ERC721Enumerable is ERC165, ERC721, IERC721Enumerable {
// Mapping from owner to list of owned token IDs
mapping(address => uint256[]) private _ownedTokens;
// Mapping from token ID to index of the owner tokens list
mapping(uint256 => uint256) private _ownedTokensIndex;
// Array with all token ids, used for enumeration
uint256[] private _allTokens;
// Mapping from token id to position in the allTokens array
mapping(uint256 => uint256) private _allTokensIndex;
bytes4 private constant _InterfaceId_ERC721Enumerable = 0x780e9d63;
/**
* 0x780e9d63 ===
* bytes4(keccak256('totalSupply()')) ^
* bytes4(keccak256('tokenOfOwnerByIndex(address,uint256)')) ^
* bytes4(keccak256('tokenByIndex(uint256)'))
*/
/**
* @dev Constructor function
*/
constructor() public {
// register the supported interface to conform to ERC721 via ERC165
_registerInterface(_InterfaceId_ERC721Enumerable);
}
/**
* @dev Gets the token ID at a given index of the tokens list of the requested owner
* @param owner address owning the tokens list to be accessed
* @param index uint256 representing the index to be accessed of the requested tokens list
* @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));
return _ownedTokens[owner][index];
}
/**
* @dev Gets the total amount of tokens stored by the contract
* @return uint256 representing the total amount of tokens
*/
function totalSupply() public view returns (uint256) {
return _allTokens.length;
}
/**
* @dev Gets the token ID at a given index of all the tokens in this contract
* 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];
}
/**
* @dev Internal function to add a token ID to the list of a given address
* @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 _addTokenTo(address to, uint256 tokenId) internal {
super._addTokenTo(to, tokenId);
uint256 length = _ownedTokens[to].length;
_ownedTokens[to].push(tokenId);
_ownedTokensIndex[tokenId] = length;
}
/**
* @dev Internal function to remove a token ID from the list of a given address
* @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 _removeTokenFrom(address from, uint256 tokenId) internal {
super._removeTokenFrom(from, tokenId);
// To prevent a gap in the array, we store the last token in the index of the token to delete, and
// then delete the last slot.
uint256 tokenIndex = _ownedTokensIndex[tokenId];
uint256 lastTokenIndex = _ownedTokens[from].length.sub(1);
uint256 lastToken = _ownedTokens[from][lastTokenIndex];
_ownedTokens[from][tokenIndex] = lastToken;
// This also deletes the contents at the last position of the array
_ownedTokens[from].length--;
// 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
_ownedTokensIndex[tokenId] = 0;
_ownedTokensIndex[lastToken] = tokenIndex;
}
/**
* @dev Internal function to mint a new token
* 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 _mint(address to, uint256 tokenId) internal {
super._mint(to, tokenId);
_allTokensIndex[tokenId] = _allTokens.length;
_allTokens.push(tokenId);
}
/**
* @dev Internal function to burn a specific token
* 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 _burn(address owner, uint256 tokenId) internal {
super._burn(owner, tokenId);
// Reorg all tokens array
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;
}
}
pragma solidity ^0.4.24;
import "./ERC721.sol";
import "./ERC721Enumerable.sol";
import "./ERC721Metadata.sol";
/**
* @title Full ERC721 Token
* This implementation includes all the required and some optional functionality of the ERC721 standard
* Moreover, it includes approve all functionality using operator terminology
* @dev see https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md
*/
contract ERC721Full is ERC721, ERC721Enumerable, ERC721Metadata {
constructor(string name, string symbol) ERC721Metadata(name, symbol)
public
{
}
}
pragma solidity ^0.4.24;
import "./ERC721.sol";
import "./IERC721Metadata.sol";
import "../../introspection/ERC165.sol";
contract ERC721Metadata is ERC165, ERC721, IERC721Metadata {
// Token name
string internal _name;
// Token symbol
string internal _symbol;
// Optional mapping for token URIs
mapping(uint256 => string) private _tokenURIs;
bytes4 private constant InterfaceId_ERC721Metadata = 0x5b5e139f;
/**
* 0x5b5e139f ===
* bytes4(keccak256('name()')) ^
* bytes4(keccak256('symbol()')) ^
* bytes4(keccak256('tokenURI(uint256)'))
*/
/**
* @dev Constructor function
*/
constructor(string name, string symbol) public {
_name = name;
_symbol = symbol;
// register the supported interfaces to conform to ERC721 via ERC165
_registerInterface(InterfaceId_ERC721Metadata);
}
/**
* @dev Gets the token name
* @return string representing the token name
*/
function name() external view returns (string) {
return _name;
}
/**
* @dev Gets the token symbol
* @return string representing the token symbol
*/
function symbol() external view returns (string) {
return _symbol;
}
/**
* @dev Returns an URI for a given token ID
* Throws if the token ID does not exist. May return an empty string.
* @param tokenId uint256 ID of the token to query
*/
function tokenURI(uint256 tokenId) public view returns (string) {
require(_exists(tokenId));
return _tokenURIs[tokenId];
}
/**
* @dev Internal function to set the token URI for a given token
* Reverts if the token ID does not exist
* @param tokenId uint256 ID of the token to set its URI
* @param uri string URI to assign
*/
function _setTokenURI(uint256 tokenId, string uri) internal {
require(_exists(tokenId));
_tokenURIs[tokenId] = uri;
}
/**
* @dev Internal function to burn a specific token
* 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 _burn(address owner, uint256 tokenId) internal {
super._burn(owner, tokenId);
// Clear metadata (if any)
if (bytes(_tokenURIs[tokenId]).length != 0) {
delete _tokenURIs[tokenId];
}
}
}
pragma solidity ^0.4.24; pragma solidity ^0.4.24;
import "./ERC721.sol"; import "./ERC721Full.sol";
import "../../access/roles/MinterRole.sol"; import "../../access/roles/MinterRole.sol";
...@@ -8,7 +8,7 @@ import "../../access/roles/MinterRole.sol"; ...@@ -8,7 +8,7 @@ import "../../access/roles/MinterRole.sol";
* @title ERC721Mintable * @title ERC721Mintable
* @dev ERC721 minting logic * @dev ERC721 minting logic
*/ */
contract ERC721Mintable is ERC721, MinterRole { contract ERC721Mintable is ERC721Full, MinterRole {
event MintingFinished(); event MintingFinished();
bool private _mintingFinished = false; bool private _mintingFinished = false;
......
pragma solidity ^0.4.24; pragma solidity ^0.4.24;
import "./ERC721Basic.sol"; import "./ERC721.sol";
import "../../lifecycle/Pausable.sol"; import "../../lifecycle/Pausable.sol";
/** /**
* @title ERC721 Non-Fungible Pausable token * @title ERC721 Non-Fungible Pausable token
* @dev ERC721Basic modified with pausable transfers. * @dev ERC721 modified with pausable transfers.
**/ **/
contract ERC721Pausable is ERC721Basic, Pausable { contract ERC721Pausable is ERC721, Pausable {
function approve( function approve(
address to, address to,
uint256 tokenId uint256 tokenId
......
pragma solidity ^0.4.24; pragma solidity ^0.4.24;
import "./IERC721Basic.sol"; import "../../introspection/IERC165.sol";
/** /**
* @title ERC-721 Non-Fungible Token Standard, optional enumeration extension * @title ERC721 Non-Fungible Token Standard basic interface
* @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md * @dev see https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md
*/ */
contract IERC721Enumerable is IERC721Basic { contract IERC721 is IERC165 {
function totalSupply() public view returns (uint256);
function tokenOfOwnerByIndex( event Transfer(
address owner, address indexed from,
uint256 index address indexed to,
uint256 indexed tokenId
);
event Approval(
address indexed owner,
address indexed approved,
uint256 indexed tokenId
);
event ApprovalForAll(
address indexed owner,
address indexed operator,
bool approved
);
function balanceOf(address owner) public view returns (uint256 balance);
function ownerOf(uint256 tokenId) public view returns (address owner);
function approve(address to, uint256 tokenId) public;
function getApproved(uint256 tokenId)
public view returns (address operator);
function setApprovalForAll(address operator, bool _approved) public;
function isApprovedForAll(address owner, address operator)
public view returns (bool);
function transferFrom(address from, address to, uint256 tokenId) public;
function safeTransferFrom(address from, address to, uint256 tokenId)
public;
function safeTransferFrom(
address from,
address to,
uint256 tokenId,
bytes data
) )
public public;
view
returns (uint256 tokenId);
function tokenByIndex(uint256 index) public view returns (uint256);
}
/**
* @title ERC-721 Non-Fungible Token Standard, optional metadata extension
* @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md
*/
contract IERC721Metadata is IERC721Basic {
function name() external view returns (string);
function symbol() external view returns (string);
function tokenURI(uint256 tokenId) public view returns (string);
}
/**
* @title ERC-721 Non-Fungible Token Standard, full implementation interface
* @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md
*/
contract IERC721 is IERC721Basic, IERC721Enumerable, IERC721Metadata {
} }
pragma solidity ^0.4.24;
import "../../introspection/IERC165.sol";
/**
* @title ERC721 Non-Fungible Token Standard basic interface
* @dev see https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md
*/
contract IERC721Basic is IERC165 {
event Transfer(
address indexed from,
address indexed to,
uint256 indexed tokenId
);
event Approval(
address indexed owner,
address indexed approved,
uint256 indexed tokenId
);
event ApprovalForAll(
address indexed owner,
address indexed operator,
bool approved
);
function balanceOf(address owner) public view returns (uint256 balance);
function ownerOf(uint256 tokenId) public view returns (address owner);
function approve(address to, uint256 tokenId) public;
function getApproved(uint256 tokenId)
public view returns (address operator);
function setApprovalForAll(address operator, bool approved) public;
function isApprovedForAll(address owner, address operator)
public view returns (bool);
function transferFrom(address from, address to, uint256 tokenId) public;
function safeTransferFrom(address from, address to, uint256 tokenId)
public;
function safeTransferFrom(
address from,
address to,
uint256 tokenId,
bytes data
)
public;
}
pragma solidity ^0.4.24;
import "./IERC721.sol";
/**
* @title ERC-721 Non-Fungible Token Standard, optional enumeration extension
* @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md
*/
contract IERC721Enumerable is IERC721 {
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);
}
pragma solidity ^0.4.24;
import "./IERC721.sol";
import "./IERC721Enumerable.sol";
import "./IERC721Metadata.sol";
/**
* @title ERC-721 Non-Fungible Token Standard, full implementation interface
* @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md
*/
contract IERC721Full is IERC721, IERC721Enumerable, IERC721Metadata {
}
pragma solidity ^0.4.24;
import "./IERC721.sol";
/**
* @title ERC-721 Non-Fungible Token Standard, optional metadata extension
* @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md
*/
contract IERC721Metadata is IERC721 {
function name() external view returns (string name);
function symbol() external view returns (string symbol);
function tokenURI(uint256 tokenId) public view returns (string);
}
...@@ -11,7 +11,7 @@ require('chai') ...@@ -11,7 +11,7 @@ require('chai')
.use(require('chai-bignumber')(BigNumber)) .use(require('chai-bignumber')(BigNumber))
.should(); .should();
function shouldBehaveLikeERC721Basic ( function shouldBehaveLikeERC721 (
creator, creator,
minter, minter,
[owner, approved, anotherApproved, operator, anyone] [owner, approved, anotherApproved, operator, anyone]
...@@ -22,7 +22,7 @@ function shouldBehaveLikeERC721Basic ( ...@@ -22,7 +22,7 @@ function shouldBehaveLikeERC721Basic (
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
const RECEIVER_MAGIC_VALUE = '0x150b7a02'; const RECEIVER_MAGIC_VALUE = '0x150b7a02';
describe('like an ERC721Basic', function () { describe('like an ERC721', function () {
beforeEach(async function () { beforeEach(async function () {
await this.token.mint(owner, firstTokenId, { from: minter }); await this.token.mint(owner, firstTokenId, { from: minter });
await this.token.mint(owner, secondTokenId, { from: minter }); await this.token.mint(owner, secondTokenId, { from: minter });
...@@ -520,5 +520,5 @@ function shouldBehaveLikeERC721Basic ( ...@@ -520,5 +520,5 @@ function shouldBehaveLikeERC721Basic (
} }
module.exports = { module.exports = {
shouldBehaveLikeERC721Basic, shouldBehaveLikeERC721,
}; };
const { assertRevert } = require('../../helpers/assertRevert'); const { shouldBehaveLikeERC721 } = require('./ERC721.behavior');
const { shouldBehaveLikeERC721Basic } = require('./ERC721Basic.behavior');
const { shouldBehaveLikeMintAndBurnERC721 } = require('./ERC721MintBurn.behavior');
const { shouldSupportInterfaces } = require('../../introspection/SupportsInterface.behavior');
const _ = require('lodash');
const BigNumber = web3.BigNumber; const BigNumber = web3.BigNumber;
const ERC721 = artifacts.require('ERC721Mock.sol'); const ERC721 = artifacts.require('ERC721Mock.sol');
...@@ -11,218 +7,10 @@ require('chai') ...@@ -11,218 +7,10 @@ require('chai')
.use(require('chai-bignumber')(BigNumber)) .use(require('chai-bignumber')(BigNumber))
.should(); .should();
contract('ERC721', function ([ contract('ERC721', function ([_, creator, ...accounts]) {
creator,
...accounts
]) {
const name = 'Non Fungible Token';
const symbol = 'NFT';
const firstTokenId = 100;
const secondTokenId = 200;
const thirdTokenId = 300;
const nonExistentTokenId = 999;
const minter = creator;
const [
owner,
newOwner,
another,
anyone,
] = accounts;
beforeEach(async function () { beforeEach(async function () {
this.token = await ERC721.new(name, symbol, { from: creator }); this.token = await ERC721.new({ from: creator });
});
describe('like a full ERC721', function () {
beforeEach(async function () {
await this.token.mint(owner, firstTokenId, { from: minter });
await this.token.mint(owner, secondTokenId, { from: minter });
});
describe('mint', function () {
beforeEach(async function () {
await this.token.mint(newOwner, thirdTokenId, { from: minter });
});
it('adjusts owner tokens by index', async function () {
(await this.token.tokenOfOwnerByIndex(newOwner, 0)).toNumber().should.be.equal(thirdTokenId);
});
it('adjusts all tokens list', async function () {
(await this.token.tokenByIndex(2)).toNumber().should.be.equal(thirdTokenId);
});
});
describe('burn', function () {
beforeEach(async function () {
await this.token.burn(firstTokenId, { from: owner });
});
it('removes that token from the token list of the owner', async function () {
(await this.token.tokenOfOwnerByIndex(owner, 0)).toNumber().should.be.equal(secondTokenId);
});
it('adjusts all tokens list', async function () {
(await this.token.tokenByIndex(0)).toNumber().should.be.equal(secondTokenId);
});
it('burns all tokens', async function () {
await this.token.burn(secondTokenId, { from: owner });
(await this.token.totalSupply()).toNumber().should.be.equal(0);
await assertRevert(this.token.tokenByIndex(0));
});
});
describe('removeTokenFrom', function () {
it('reverts if the correct owner is not passed', async function () {
await assertRevert(
this.token.removeTokenFrom(anyone, firstTokenId, { from: owner })
);
});
context('once removed', function () {
beforeEach(async function () {
await this.token.removeTokenFrom(owner, firstTokenId, { from: owner });
});
it('has been removed', async function () {
await assertRevert(this.token.tokenOfOwnerByIndex(owner, 1));
});
it('adjusts token list', async function () {
(await this.token.tokenOfOwnerByIndex(owner, 0)).toNumber().should.be.equal(secondTokenId);
});
it('adjusts owner count', async function () {
(await this.token.balanceOf(owner)).toNumber().should.be.equal(1);
});
it('does not adjust supply', async function () {
(await this.token.totalSupply()).toNumber().should.be.equal(2);
});
});
});
describe('metadata', function () {
const sampleUri = 'mock://mytoken';
it('has a name', async function () {
(await this.token.name()).should.be.equal(name);
});
it('has a symbol', async function () {
(await this.token.symbol()).should.be.equal(symbol);
});
it('sets and returns metadata for a token id', async function () {
await this.token.setTokenURI(firstTokenId, sampleUri);
(await this.token.tokenURI(firstTokenId)).should.be.equal(sampleUri);
});
it('reverts when setting metadata for non existent token id', async function () {
await assertRevert(this.token.setTokenURI(nonExistentTokenId, sampleUri));
});
it('can burn token with metadata', async function () {
await this.token.setTokenURI(firstTokenId, sampleUri);
await this.token.burn(firstTokenId, { from: owner });
(await this.token.exists(firstTokenId)).should.equal(false);
});
it('returns empty metadata for token', async function () {
(await this.token.tokenURI(firstTokenId)).should.be.equal('');
});
it('reverts when querying metadata for non existent token id', async function () {
await assertRevert(this.token.tokenURI(nonExistentTokenId));
});
});
describe('totalSupply', function () {
it('returns total token supply', async function () {
(await this.token.totalSupply()).should.be.bignumber.equal(2);
});
});
describe('tokenOfOwnerByIndex', function () {
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 () {
(await this.token.tokenOfOwnerByIndex(owner, 0)).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 address does not own any token', function () {
it('reverts', async function () {
await assertRevert(this.token.tokenOfOwnerByIndex(another, 0));
});
});
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 });
});
it('returns correct token IDs for target', async function () {
(await this.token.balanceOf(another)).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('returns empty collection for original owner', async function () {
(await this.token.balanceOf(owner)).toNumber().should.be.equal(0);
await assertRevert(this.token.tokenOfOwnerByIndex(owner, 0));
});
});
});
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('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 newTokenId = 300;
const anotherNewTokenId = 400;
await this.token.burn(tokenId, { from: owner });
await this.token.mint(newOwner, newTokenId, { from: minter });
await this.token.mint(newOwner, anotherNewTokenId, { from: minter });
(await this.token.totalSupply()).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);
});
});
});
}); });
shouldBehaveLikeERC721Basic(creator, minter, accounts); shouldBehaveLikeERC721(creator, creator, accounts);
shouldBehaveLikeMintAndBurnERC721(creator, minter, accounts);
shouldSupportInterfaces([
'ERC165',
'ERC721',
'ERC721Enumerable',
'ERC721Metadata',
]);
}); });
const { shouldBehaveLikeERC721Basic } = require('./ERC721Basic.behavior');
const BigNumber = web3.BigNumber;
const ERC721Basic = artifacts.require('ERC721BasicMock.sol');
require('chai')
.use(require('chai-bignumber')(BigNumber))
.should();
contract('ERC721Basic', function ([_, creator, ...accounts]) {
beforeEach(async function () {
this.token = await ERC721Basic.new({ from: creator });
});
shouldBehaveLikeERC721Basic(creator, creator, accounts);
});
const { shouldBehaveLikeERC721Basic } = require('./ERC721Basic.behavior'); const { shouldBehaveLikeERC721 } = require('./ERC721.behavior');
const { const {
shouldBehaveLikeMintAndBurnERC721, shouldBehaveLikeMintAndBurnERC721,
} = require('./ERC721MintBurn.behavior'); } = require('./ERC721MintBurn.behavior');
...@@ -17,6 +17,6 @@ contract('ERC721Burnable', function ([_, creator, ...accounts]) { ...@@ -17,6 +17,6 @@ contract('ERC721Burnable', function ([_, creator, ...accounts]) {
this.token = await ERC721Burnable.new({ from: creator }); this.token = await ERC721Burnable.new({ from: creator });
}); });
shouldBehaveLikeERC721Basic(creator, minter, accounts); shouldBehaveLikeERC721(creator, minter, accounts);
shouldBehaveLikeMintAndBurnERC721(creator, minter, accounts); shouldBehaveLikeMintAndBurnERC721(creator, minter, accounts);
}); });
const { assertRevert } = require('../../helpers/assertRevert');
const { shouldBehaveLikeERC721 } = require('./ERC721.behavior');
const { shouldBehaveLikeMintAndBurnERC721 } = require('./ERC721MintBurn.behavior');
const { shouldSupportInterfaces } = require('../../introspection/SupportsInterface.behavior');
const _ = require('lodash');
const BigNumber = web3.BigNumber;
const ERC721FullMock = artifacts.require('ERC721FullMock.sol');
require('chai')
.use(require('chai-bignumber')(BigNumber))
.should();
contract('ERC721Full', function ([
creator,
...accounts
]) {
const name = 'Non Fungible Token';
const symbol = 'NFT';
const firstTokenId = 100;
const secondTokenId = 200;
const thirdTokenId = 300;
const nonExistentTokenId = 999;
const minter = creator;
const [
owner,
newOwner,
another,
anyone,
] = accounts;
beforeEach(async function () {
this.token = await ERC721FullMock.new(name, symbol, { from: creator });
});
describe('like a full ERC721', function () {
beforeEach(async function () {
await this.token.mint(owner, firstTokenId, { from: minter });
await this.token.mint(owner, secondTokenId, { from: minter });
});
describe('mint', function () {
beforeEach(async function () {
await this.token.mint(newOwner, thirdTokenId, { from: minter });
});
it('adjusts owner tokens by index', async function () {
(await this.token.tokenOfOwnerByIndex(newOwner, 0)).toNumber().should.be.equal(thirdTokenId);
});
it('adjusts all tokens list', async function () {
(await this.token.tokenByIndex(2)).toNumber().should.be.equal(thirdTokenId);
});
});
describe('burn', function () {
beforeEach(async function () {
await this.token.burn(firstTokenId, { from: owner });
});
it('removes that token from the token list of the owner', async function () {
(await this.token.tokenOfOwnerByIndex(owner, 0)).toNumber().should.be.equal(secondTokenId);
});
it('adjusts all tokens list', async function () {
(await this.token.tokenByIndex(0)).toNumber().should.be.equal(secondTokenId);
});
it('burns all tokens', async function () {
await this.token.burn(secondTokenId, { from: owner });
(await this.token.totalSupply()).toNumber().should.be.equal(0);
await assertRevert(this.token.tokenByIndex(0));
});
});
describe('removeTokenFrom', function () {
it('reverts if the correct owner is not passed', async function () {
await assertRevert(
this.token.removeTokenFrom(anyone, firstTokenId, { from: owner })
);
});
context('once removed', function () {
beforeEach(async function () {
await this.token.removeTokenFrom(owner, firstTokenId, { from: owner });
});
it('has been removed', async function () {
await assertRevert(this.token.tokenOfOwnerByIndex(owner, 1));
});
it('adjusts token list', async function () {
(await this.token.tokenOfOwnerByIndex(owner, 0)).toNumber().should.be.equal(secondTokenId);
});
it('adjusts owner count', async function () {
(await this.token.balanceOf(owner)).toNumber().should.be.equal(1);
});
it('does not adjust supply', async function () {
(await this.token.totalSupply()).toNumber().should.be.equal(2);
});
});
});
describe('metadata', function () {
const sampleUri = 'mock://mytoken';
it('has a name', async function () {
(await this.token.name()).should.be.equal(name);
});
it('has a symbol', async function () {
(await this.token.symbol()).should.be.equal(symbol);
});
it('sets and returns metadata for a token id', async function () {
await this.token.setTokenURI(firstTokenId, sampleUri);
(await this.token.tokenURI(firstTokenId)).should.be.equal(sampleUri);
});
it('reverts when setting metadata for non existent token id', async function () {
await assertRevert(this.token.setTokenURI(nonExistentTokenId, sampleUri));
});
it('can burn token with metadata', async function () {
await this.token.setTokenURI(firstTokenId, sampleUri);
await this.token.burn(firstTokenId, { from: owner });
(await this.token.exists(firstTokenId)).should.equal(false);
});
it('returns empty metadata for token', async function () {
(await this.token.tokenURI(firstTokenId)).should.be.equal('');
});
it('reverts when querying metadata for non existent token id', async function () {
await assertRevert(this.token.tokenURI(nonExistentTokenId));
});
});
describe('totalSupply', function () {
it('returns total token supply', async function () {
(await this.token.totalSupply()).should.be.bignumber.equal(2);
});
});
describe('tokenOfOwnerByIndex', function () {
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 () {
(await this.token.tokenOfOwnerByIndex(owner, 0)).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 address does not own any token', function () {
it('reverts', async function () {
await assertRevert(this.token.tokenOfOwnerByIndex(another, 0));
});
});
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 });
});
it('returns correct token IDs for target', async function () {
(await this.token.balanceOf(another)).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('returns empty collection for original owner', async function () {
(await this.token.balanceOf(owner)).toNumber().should.be.equal(0);
await assertRevert(this.token.tokenOfOwnerByIndex(owner, 0));
});
});
});
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('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 newTokenId = 300;
const anotherNewTokenId = 400;
await this.token.burn(tokenId, { from: owner });
await this.token.mint(newOwner, newTokenId, { from: minter });
await this.token.mint(newOwner, anotherNewTokenId, { from: minter });
(await this.token.totalSupply()).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);
});
});
});
});
shouldBehaveLikeERC721(creator, minter, accounts);
shouldBehaveLikeMintAndBurnERC721(creator, minter, accounts);
shouldSupportInterfaces([
'ERC165',
'ERC721',
'ERC721Enumerable',
'ERC721Metadata',
]);
});
const { shouldBehaveLikeERC721Basic } = require('./ERC721Basic.behavior'); const { shouldBehaveLikeERC721 } = require('./ERC721.behavior');
const { const {
shouldBehaveLikeMintAndBurnERC721, shouldBehaveLikeMintAndBurnERC721,
} = require('./ERC721MintBurn.behavior'); } = require('./ERC721MintBurn.behavior');
...@@ -19,6 +19,6 @@ contract('ERC721Mintable', function ([_, creator, ...accounts]) { ...@@ -19,6 +19,6 @@ contract('ERC721Mintable', function ([_, creator, ...accounts]) {
}); });
}); });
shouldBehaveLikeERC721Basic(creator, minter, accounts); shouldBehaveLikeERC721(creator, minter, accounts);
shouldBehaveLikeMintAndBurnERC721(creator, minter, accounts); shouldBehaveLikeMintAndBurnERC721(creator, minter, accounts);
}); });
const { shouldBehaveLikeERC721PausedToken } = require('./ERC721PausedToken.behavior'); const { shouldBehaveLikeERC721PausedToken } = require('./ERC721PausedToken.behavior');
const { shouldBehaveLikeERC721Basic } = require('./ERC721Basic.behavior'); const { shouldBehaveLikeERC721 } = require('./ERC721.behavior');
const { shouldBehaveLikePublicRole } = require('../../access/roles/PublicRole.behavior'); const { shouldBehaveLikePublicRole } = require('../../access/roles/PublicRole.behavior');
const BigNumber = web3.BigNumber; const BigNumber = web3.BigNumber;
...@@ -39,7 +39,7 @@ contract('ERC721Pausable', function ([ ...@@ -39,7 +39,7 @@ contract('ERC721Pausable', function ([
}); });
context('when token is not paused yet', function () { context('when token is not paused yet', function () {
shouldBehaveLikeERC721Basic(creator, creator, accounts); shouldBehaveLikeERC721(creator, creator, accounts);
}); });
context('when token is paused and then unpaused', function () { context('when token is paused and then unpaused', function () {
...@@ -48,6 +48,6 @@ contract('ERC721Pausable', function ([ ...@@ -48,6 +48,6 @@ contract('ERC721Pausable', function ([
await this.token.unpause({ from: creator }); await this.token.unpause({ from: creator });
}); });
shouldBehaveLikeERC721Basic(creator, creator, accounts); shouldBehaveLikeERC721(creator, creator, accounts);
}); });
}); });
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment