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;
import "../token/ERC721/ERC721.sol";
import "../token/ERC721/ERC721Full.sol";
import "../token/ERC721/ERC721Mintable.sol";
import "../token/ERC721/ERC721Burnable.sol";
......@@ -8,10 +8,12 @@ import "../token/ERC721/ERC721Burnable.sol";
/**
* @title ERC721MintableBurnableImpl
*/
contract ERC721MintableBurnableImpl is ERC721, ERC721Mintable, ERC721Burnable {
contract ERC721MintableBurnableImpl
is ERC721Full, ERC721Mintable, ERC721Burnable {
constructor()
ERC721Mintable()
ERC721("Test", "TEST")
ERC721Full("Test", "TEST")
public
{
}
......
pragma solidity ^0.4.24;
import "../token/ERC721/ERC721.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
* This mock just provides a public mint and burn functions for testing purposes
*/
contract ERC721Mock is ERC721, ERC721Mintable, ERC721Burnable {
constructor(string name, string symbol) public
ERC721Mintable()
ERC721(name, symbol)
{}
function exists(uint256 tokenId) public view returns (bool) {
return _exists(tokenId);
}
function setTokenURI(uint256 tokenId, string uri) public {
_setTokenURI(tokenId, uri);
contract ERC721Mock is ERC721 {
function mint(address to, uint256 tokenId) public {
_mint(to, tokenId);
}
function removeTokenFrom(address from, uint256 tokenId) public {
_removeTokenFrom(from, tokenId);
function burn(uint256 tokenId) public {
_burn(ownerOf(tokenId), tokenId);
}
}
pragma solidity ^0.4.24;
import "./IERC721.sol";
import "./ERC721Basic.sol";
import "./IERC721Receiver.sol";
import "../../math/SafeMath.sol";
import "../../utils/Address.sol";
import "../../introspection/ERC165.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
* @title ERC721 Non-Fungible Token Standard basic implementation
* @dev see https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md
*/
contract ERC721 is ERC165, ERC721Basic, IERC721 {
// Token name
string internal _name;
// Token symbol
string internal _symbol;
// 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;
contract ERC721 is ERC165, IERC721 {
using SafeMath for uint256;
using Address for address;
// Equals to `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`
// which can be also obtained as `IERC721Receiver(0).onERC721Received.selector`
bytes4 private constant _ERC721_RECEIVED = 0x150b7a02;
// Mapping from token ID to owner
mapping (uint256 => address) private _tokenOwner;
// Mapping from token ID to approved address
mapping (uint256 => address) private _tokenApprovals;
// Mapping from owner to number of owned token
mapping (address => uint256) private _ownedTokensCount;
// Mapping from owner to operator approvals
mapping (address => mapping (address => bool)) private _operatorApprovals;
bytes4 private constant _InterfaceId_ERC721 = 0x80ac58cd;
/*
* 0x80ac58cd ===
* bytes4(keccak256('balanceOf(address)')) ^
* bytes4(keccak256('ownerOf(uint256)')) ^
* bytes4(keccak256('approve(address,uint256)')) ^
* bytes4(keccak256('getApproved(uint256)')) ^
* bytes4(keccak256('setApprovalForAll(address,bool)')) ^
* bytes4(keccak256('isApprovedForAll(address,address)')) ^
* bytes4(keccak256('transferFrom(address,address,uint256)')) ^
* bytes4(keccak256('safeTransferFrom(address,address,uint256)')) ^
* bytes4(keccak256('safeTransferFrom(address,address,uint256,bytes)'))
*/
// Optional mapping for token URIs
mapping(uint256 => string) private _tokenURIs;
constructor()
public
{
// register the supported interfaces to conform to ERC721 via ERC165
_registerInterface(_InterfaceId_ERC721);
}
bytes4 private constant _InterfaceId_ERC721Enumerable = 0x780e9d63;
/**
* 0x780e9d63 ===
* bytes4(keccak256('totalSupply()')) ^
* bytes4(keccak256('tokenOfOwnerByIndex(address,uint256)')) ^
* bytes4(keccak256('tokenByIndex(uint256)'))
* @dev Gets the balance of the specified address
* @param owner address to query the balance of
* @return uint256 representing the amount owned by the passed address
*/
function balanceOf(address owner) public view returns (uint256) {
require(owner != address(0));
return _ownedTokensCount[owner];
}
bytes4 private constant _InterfaceId_ERC721Metadata = 0x5b5e139f;
/**
* 0x5b5e139f ===
* bytes4(keccak256('name()')) ^
* bytes4(keccak256('symbol()')) ^
* bytes4(keccak256('tokenURI(uint256)'))
* @dev Gets the owner of the specified token ID
* @param tokenId uint256 ID of the token to query the owner of
* @return owner address currently marked as the owner of the given token ID
*/
function ownerOf(uint256 tokenId) public view returns (address) {
address owner = _tokenOwner[tokenId];
require(owner != address(0));
return owner;
}
/**
* @dev Constructor function
* @dev Approves another address to transfer the given token ID
* The zero address indicates there is no approved address.
* There can only be one approved address per token at a given time.
* Can only be called by the token owner or an approved operator.
* @param to address to be approved for the given token ID
* @param tokenId uint256 ID of the token to be approved
*/
constructor(string name, string symbol) public {
_name = name;
_symbol = symbol;
function approve(address to, uint256 tokenId) public {
address owner = ownerOf(tokenId);
require(to != owner);
require(msg.sender == owner || isApprovedForAll(owner, msg.sender));
// register the supported interfaces to conform to ERC721 via ERC165
_registerInterface(_InterfaceId_ERC721Enumerable);
_registerInterface(_InterfaceId_ERC721Metadata);
_tokenApprovals[tokenId] = to;
emit Approval(owner, to, tokenId);
}
/**
* @dev Gets the token name
* @return string representing the token name
* @dev Gets the approved address for a token ID, or zero if no address set
* Reverts if the token ID does not exist.
* @param tokenId uint256 ID of the token to query the approval of
* @return address currently approved for the given token ID
*/
function name() external view returns (string) {
return _name;
function getApproved(uint256 tokenId) public view returns (address) {
require(_exists(tokenId));
return _tokenApprovals[tokenId];
}
/**
* @dev Gets the token symbol
* @return string representing the token symbol
* @dev Sets or unsets the approval of a given operator
* An operator is allowed to transfer all tokens of the sender on their behalf
* @param to operator address to set the approval
* @param approved representing the status of the approval to be set
*/
function symbol() external view returns (string) {
return _symbol;
function setApprovalForAll(address to, bool approved) public {
require(to != msg.sender);
_operatorApprovals[msg.sender][to] = approved;
emit ApprovalForAll(msg.sender, to, approved);
}
/**
* @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
* @dev Tells whether an operator is approved by a given owner
* @param owner owner address which you want to query the approval of
* @param operator operator address which you want to query the approval of
* @return bool whether the given operator is approved by the given owner
*/
function tokenURI(uint256 tokenId) public view returns (string) {
require(_exists(tokenId));
return _tokenURIs[tokenId];
}
/**
* @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(
function isApprovedForAll(
address owner,
uint256 index
address operator
)
public
view
returns (uint256)
returns (bool)
{
require(index < balanceOf(owner));
return _ownedTokens[owner][index];
return _operatorApprovals[owner][operator];
}
/**
* @dev Gets the total amount of tokens stored by the contract
* @return uint256 representing the total amount of tokens
* @dev Transfers the ownership of a given token ID to another address
* Usage of this method is discouraged, use `safeTransferFrom` whenever possible
* Requires the msg sender to be the owner, approved, or operator
* @param from current owner of the token
* @param to address to receive the ownership of the given token ID
* @param tokenId uint256 ID of the token to be transferred
*/
function totalSupply() public view returns (uint256) {
return _allTokens.length;
function transferFrom(
address from,
address to,
uint256 tokenId
)
public
{
require(_isApprovedOrOwner(msg.sender, tokenId));
require(to != address(0));
_clearApproval(from, tokenId);
_removeTokenFrom(from, tokenId);
_addTokenTo(to, tokenId);
emit Transfer(from, to, tokenId);
}
/**
* @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
* @dev Safely transfers the ownership of a given token ID to another address
* If the target address is a contract, it must implement `onERC721Received`,
* which is called upon a safe transfer, and return the magic value
* `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`; otherwise,
* the transfer is reverted.
*
* Requires the msg sender to be the owner, approved, or operator
* @param from current owner of the token
* @param to address to receive the ownership of the given token ID
* @param tokenId uint256 ID of the token to be transferred
*/
function tokenByIndex(uint256 index) public view returns (uint256) {
require(index < totalSupply());
return _allTokens[index];
function safeTransferFrom(
address from,
address to,
uint256 tokenId
)
public
{
// solium-disable-next-line arg-overflow
safeTransferFrom(from, to, 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
* @dev Safely transfers the ownership of a given token ID to another address
* If the target address is a contract, it must implement `onERC721Received`,
* which is called upon a safe transfer, and return the magic value
* `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`; otherwise,
* the transfer is reverted.
* Requires the msg sender to be the owner, approved, or operator
* @param from current owner of the token
* @param to address to receive the ownership of the given token ID
* @param tokenId uint256 ID of the token to be transferred
* @param _data bytes data to send along with a safe transfer check
*/
function _setTokenURI(uint256 tokenId, string uri) internal {
require(_exists(tokenId));
_tokenURIs[tokenId] = uri;
function safeTransferFrom(
address from,
address to,
uint256 tokenId,
bytes _data
)
public
{
transferFrom(from, to, tokenId);
// solium-disable-next-line arg-overflow
require(_checkAndCallSafeTransfer(from, to, tokenId, _data));
}
/**
* @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
* @dev Returns whether the specified token exists
* @param tokenId uint256 ID of the token to query the existence of
* @return whether the token exists
*/
function _addTokenTo(address to, uint256 tokenId) internal {
super._addTokenTo(to, tokenId);
uint256 length = _ownedTokens[to].length;
_ownedTokens[to].push(tokenId);
_ownedTokensIndex[tokenId] = length;
function _exists(uint256 tokenId) internal view returns (bool) {
address owner = _tokenOwner[tokenId];
return owner != address(0);
}
/**
* @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
* @dev Returns whether the given spender can transfer a given token ID
* @param spender address of the spender to query
* @param tokenId uint256 ID of the token to be transferred
* @return bool whether the msg.sender is approved for the given token ID,
* is an operator of the owner, or is the owner of the token
*/
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;
function _isApprovedOrOwner(
address spender,
uint256 tokenId
)
internal
view
returns (bool)
{
address owner = ownerOf(tokenId);
// Disable solium check because of
// https://github.com/duaraghav8/Solium/issues/175
// solium-disable-next-line operator-whitespace
return (
spender == owner ||
getApproved(tokenId) == spender ||
isApprovedForAll(owner, spender)
);
}
/**
* @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 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 {
super._mint(to, tokenId);
_allTokensIndex[tokenId] = _allTokens.length;
_allTokens.push(tokenId);
require(to != address(0));
_addTokenTo(to, tokenId);
emit Transfer(address(0), to, 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);
// Clear metadata (if any)
if (bytes(_tokenURIs[tokenId]).length != 0) {
delete _tokenURIs[tokenId];
_clearApproval(owner, tokenId);
_removeTokenFrom(owner, tokenId);
emit Transfer(owner, address(0), tokenId);
}
// Reorg all tokens array
uint256 tokenIndex = _allTokensIndex[tokenId];
uint256 lastTokenIndex = _allTokens.length.sub(1);
uint256 lastToken = _allTokens[lastTokenIndex];
/**
* @dev Internal function to clear current approval of a given token ID
* Reverts if the given address is not indeed the owner of the token
* @param owner owner of the token
* @param tokenId uint256 ID of the token to be transferred
*/
function _clearApproval(address owner, uint256 tokenId) internal {
require(ownerOf(tokenId) == owner);
if (_tokenApprovals[tokenId] != address(0)) {
_tokenApprovals[tokenId] = address(0);
}
}
_allTokens[tokenIndex] = lastToken;
_allTokens[lastTokenIndex] = 0;
/**
* @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 {
require(_tokenOwner[tokenId] == address(0));
_tokenOwner[tokenId] = to;
_ownedTokensCount[to] = _ownedTokensCount[to].add(1);
}
_allTokens.length--;
_allTokensIndex[tokenId] = 0;
_allTokensIndex[lastToken] = tokenIndex;
/**
* @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 {
require(ownerOf(tokenId) == from);
_ownedTokensCount[from] = _ownedTokensCount[from].sub(1);
_tokenOwner[tokenId] = address(0);
}
/**
* @dev Internal function to invoke `onERC721Received` on a target address
* The call is not executed if the target address is not a contract
* @param from address representing the previous owner of the given token ID
* @param to target address that will receive the tokens
* @param tokenId uint256 ID of the token to be transferred
* @param _data bytes optional data to send along with the call
* @return whether the call correctly returned the expected magic value
*/
function _checkAndCallSafeTransfer(
address from,
address to,
uint256 tokenId,
bytes _data
)
internal
returns (bool)
{
if (!to.isContract()) {
return true;
}
bytes4 retval = IERC721Receiver(to).onERC721Received(
msg.sender, from, tokenId, _data);
return (retval == _ERC721_RECEIVED);
}
}
pragma solidity ^0.4.24;
import "./IERC721Basic.sol";
import "./IERC721Receiver.sol";
import "../../math/SafeMath.sol";
import "../../utils/Address.sol";
import "../../introspection/ERC165.sol";
/**
* @title ERC721 Non-Fungible Token Standard basic implementation
* @dev see https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md
*/
contract ERC721Basic is ERC165, IERC721Basic {
using SafeMath for uint256;
using Address for address;
// Equals to `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`
// which can be also obtained as `IERC721Receiver(0).onERC721Received.selector`
bytes4 private constant _ERC721_RECEIVED = 0x150b7a02;
// Mapping from token ID to owner
mapping (uint256 => address) private _tokenOwner;
// Mapping from token ID to approved address
mapping (uint256 => address) private _tokenApprovals;
// Mapping from owner to number of owned token
mapping (address => uint256) private _ownedTokensCount;
// Mapping from owner to operator approvals
mapping (address => mapping (address => bool)) private _operatorApprovals;
bytes4 private constant _InterfaceId_ERC721 = 0x80ac58cd;
/*
* 0x80ac58cd ===
* bytes4(keccak256('balanceOf(address)')) ^
* bytes4(keccak256('ownerOf(uint256)')) ^
* bytes4(keccak256('approve(address,uint256)')) ^
* bytes4(keccak256('getApproved(uint256)')) ^
* bytes4(keccak256('setApprovalForAll(address,bool)')) ^
* bytes4(keccak256('isApprovedForAll(address,address)')) ^
* bytes4(keccak256('transferFrom(address,address,uint256)')) ^
* bytes4(keccak256('safeTransferFrom(address,address,uint256)')) ^
* bytes4(keccak256('safeTransferFrom(address,address,uint256,bytes)'))
*/
constructor()
public
{
// register the supported interfaces to conform to ERC721 via ERC165
_registerInterface(_InterfaceId_ERC721);
}
/**
* @dev Gets the balance of the specified address
* @param owner address to query the balance of
* @return uint256 representing the amount owned by the passed address
*/
function balanceOf(address owner) public view returns (uint256) {
require(owner != address(0));
return _ownedTokensCount[owner];
}
/**
* @dev Gets the owner of the specified token ID
* @param tokenId uint256 ID of the token to query the owner of
* @return owner address currently marked as the owner of the given token ID
*/
function ownerOf(uint256 tokenId) public view returns (address) {
address owner = _tokenOwner[tokenId];
require(owner != address(0));
return owner;
}
/**
* @dev Approves another address to transfer the given token ID
* The zero address indicates there is no approved address.
* There can only be one approved address per token at a given time.
* Can only be called by the token owner or an approved operator.
* @param to address to be approved for the given token ID
* @param tokenId uint256 ID of the token to be approved
*/
function approve(address to, uint256 tokenId) public {
address owner = ownerOf(tokenId);
require(to != owner);
require(msg.sender == owner || isApprovedForAll(owner, msg.sender));
_tokenApprovals[tokenId] = to;
emit Approval(owner, to, tokenId);
}
/**
* @dev Gets the approved address for a token ID, or zero if no address set
* @param tokenId uint256 ID of the token to query the approval of
* @return address currently approved for the given token ID
*/
function getApproved(uint256 tokenId) public view returns (address) {
require(_exists(tokenId));
return _tokenApprovals[tokenId];
}
/**
* @dev Sets or unsets the approval of a given operator
* An operator is allowed to transfer all tokens of the sender on their behalf
* @param to operator address to set the approval
* @param approved representing the status of the approval to be set
*/
function setApprovalForAll(address to, bool approved) public {
require(to != msg.sender);
_operatorApprovals[msg.sender][to] = approved;
emit ApprovalForAll(msg.sender, to, approved);
}
/**
* @dev Tells whether an operator is approved by a given owner
* @param owner owner address which you want to query the approval of
* @param operator operator address which you want to query the approval of
* @return bool whether the given operator is approved by the given owner
*/
function isApprovedForAll(
address owner,
address operator
)
public
view
returns (bool)
{
return _operatorApprovals[owner][operator];
}
/**
* @dev Transfers the ownership of a given token ID to another address
* Usage of this method is discouraged, use `safeTransferFrom` whenever possible
* Requires the msg sender to be the owner, approved, or operator
* @param from current owner of the token
* @param to address to receive the ownership of the given token ID
* @param tokenId uint256 ID of the token to be transferred
*/
function transferFrom(
address from,
address to,
uint256 tokenId
)
public
{
require(_isApprovedOrOwner(msg.sender, tokenId));
require(to != address(0));
_clearApproval(from, tokenId);
_removeTokenFrom(from, tokenId);
_addTokenTo(to, tokenId);
emit Transfer(from, to, tokenId);
}
/**
* @dev Safely transfers the ownership of a given token ID to another address
* If the target address is a contract, it must implement `onERC721Received`,
* which is called upon a safe transfer, and return the magic value
* `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`; otherwise,
* the transfer is reverted.
*
* Requires the msg sender to be the owner, approved, or operator
* @param from current owner of the token
* @param to address to receive the ownership of the given token ID
* @param tokenId uint256 ID of the token to be transferred
*/
function safeTransferFrom(
address from,
address to,
uint256 tokenId
)
public
{
// solium-disable-next-line arg-overflow
safeTransferFrom(from, to, tokenId, "");
}
/**
* @dev Safely transfers the ownership of a given token ID to another address
* If the target address is a contract, it must implement `onERC721Received`,
* which is called upon a safe transfer, and return the magic value
* `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`; otherwise,
* the transfer is reverted.
* Requires the msg sender to be the owner, approved, or operator
* @param from current owner of the token
* @param to address to receive the ownership of the given token ID
* @param tokenId uint256 ID of the token to be transferred
* @param data bytes data to send along with a safe transfer check
*/
function safeTransferFrom(
address from,
address to,
uint256 tokenId,
bytes data
)
public
{
transferFrom(from, to, tokenId);
// solium-disable-next-line arg-overflow
require(_checkAndCallSafeTransfer(from, to, tokenId, data));
}
/**
* @dev Returns whether the specified token exists
* @param tokenId uint256 ID of the token to query the existence of
* @return whether the token exists
*/
function _exists(uint256 tokenId) internal view returns (bool) {
address owner = _tokenOwner[tokenId];
return owner != address(0);
}
/**
* @dev Returns whether the given spender can transfer a given token ID
* @param spender address of the spender to query
* @param tokenId uint256 ID of the token to be transferred
* @return bool whether the msg.sender is approved for the given token ID,
* is an operator of the owner, or is the owner of the token
*/
function _isApprovedOrOwner(
address spender,
uint256 tokenId
)
internal
view
returns (bool)
{
address owner = ownerOf(tokenId);
// Disable solium check because of
// https://github.com/duaraghav8/Solium/issues/175
// solium-disable-next-line operator-whitespace
return (
spender == owner ||
getApproved(tokenId) == spender ||
isApprovedForAll(owner, spender)
);
}
/**
* @dev Internal function to mint a new token
* Reverts if the given token ID already exists
* @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 {
require(to != address(0));
_addTokenTo(to, tokenId);
emit Transfer(address(0), to, tokenId);
}
/**
* @dev Internal function to burn a specific token
* Reverts if the token does not exist
* @param tokenId uint256 ID of the token being burned by the msg.sender
*/
function _burn(address owner, uint256 tokenId) internal {
_clearApproval(owner, tokenId);
_removeTokenFrom(owner, tokenId);
emit Transfer(owner, address(0), tokenId);
}
/**
* @dev Internal function to clear current approval of a given token ID
* Reverts if the given address is not indeed the owner of the token
* @param owner owner of the token
* @param tokenId uint256 ID of the token to be transferred
*/
function _clearApproval(address owner, uint256 tokenId) internal {
require(ownerOf(tokenId) == owner);
if (_tokenApprovals[tokenId] != address(0)) {
_tokenApprovals[tokenId] = address(0);
}
}
/**
* @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 {
require(_tokenOwner[tokenId] == address(0));
_tokenOwner[tokenId] = to;
_ownedTokensCount[to] = _ownedTokensCount[to].add(1);
}
/**
* @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 {
require(ownerOf(tokenId) == from);
_ownedTokensCount[from] = _ownedTokensCount[from].sub(1);
_tokenOwner[tokenId] = address(0);
}
/**
* @dev Internal function to invoke `onERC721Received` on a target address
* The call is not executed if the target address is not a contract
* @param from address representing the previous owner of the given token ID
* @param to target address that will receive the tokens
* @param tokenId uint256 ID of the token to be transferred
* @param data bytes optional data to send along with the call
* @return whether the call correctly returned the expected magic value
*/
function _checkAndCallSafeTransfer(
address from,
address to,
uint256 tokenId,
bytes data
)
internal
returns (bool)
{
if (!to.isContract()) {
return true;
}
bytes4 retval = IERC721Receiver(to).onERC721Received(
msg.sender, from, tokenId, data);
return (retval == _ERC721_RECEIVED);
}
}
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;
import "./ERC721.sol";
import "./ERC721Full.sol";
import "../../access/roles/MinterRole.sol";
......@@ -8,7 +8,7 @@ import "../../access/roles/MinterRole.sol";
* @title ERC721Mintable
* @dev ERC721 minting logic
*/
contract ERC721Mintable is ERC721, MinterRole {
contract ERC721Mintable is ERC721Full, MinterRole {
event MintingFinished();
bool private _mintingFinished = false;
......
pragma solidity ^0.4.24;
import "./ERC721Basic.sol";
import "./ERC721.sol";
import "../../lifecycle/Pausable.sol";
/**
* @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(
address to,
uint256 tokenId
......
pragma solidity ^0.4.24;
import "./IERC721Basic.sol";
import "../../introspection/IERC165.sol";
/**
* @title ERC-721 Non-Fungible Token Standard, optional enumeration extension
* @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md
* @title ERC721 Non-Fungible Token Standard basic interface
* @dev see https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md
*/
contract IERC721Enumerable is IERC721Basic {
function totalSupply() public view returns (uint256);
function tokenOfOwnerByIndex(
address owner,
uint256 index
contract IERC721 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
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 {
public;
}
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')
.use(require('chai-bignumber')(BigNumber))
.should();
function shouldBehaveLikeERC721Basic (
function shouldBehaveLikeERC721 (
creator,
minter,
[owner, approved, anotherApproved, operator, anyone]
......@@ -22,7 +22,7 @@ function shouldBehaveLikeERC721Basic (
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
const RECEIVER_MAGIC_VALUE = '0x150b7a02';
describe('like an ERC721Basic', function () {
describe('like an ERC721', function () {
beforeEach(async function () {
await this.token.mint(owner, firstTokenId, { from: minter });
await this.token.mint(owner, secondTokenId, { from: minter });
......@@ -520,5 +520,5 @@ function shouldBehaveLikeERC721Basic (
}
module.exports = {
shouldBehaveLikeERC721Basic,
shouldBehaveLikeERC721,
};
const { assertRevert } = require('../../helpers/assertRevert');
const { shouldBehaveLikeERC721Basic } = require('./ERC721Basic.behavior');
const { shouldBehaveLikeMintAndBurnERC721 } = require('./ERC721MintBurn.behavior');
const { shouldSupportInterfaces } = require('../../introspection/SupportsInterface.behavior');
const _ = require('lodash');
const { shouldBehaveLikeERC721 } = require('./ERC721.behavior');
const BigNumber = web3.BigNumber;
const ERC721 = artifacts.require('ERC721Mock.sol');
......@@ -11,218 +7,10 @@ require('chai')
.use(require('chai-bignumber')(BigNumber))
.should();
contract('ERC721', 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 ERC721.new(name, symbol, { from: creator });
});
describe('like a full ERC721', function () {
contract('ERC721', function ([_, creator, ...accounts]) {
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));
this.token = await ERC721.new({ from: creator });
});
});
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);
shouldBehaveLikeMintAndBurnERC721(creator, minter, accounts);
shouldSupportInterfaces([
'ERC165',
'ERC721',
'ERC721Enumerable',
'ERC721Metadata',
]);
shouldBehaveLikeERC721(creator, creator, accounts);
});
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 {
shouldBehaveLikeMintAndBurnERC721,
} = require('./ERC721MintBurn.behavior');
......@@ -17,6 +17,6 @@ contract('ERC721Burnable', function ([_, creator, ...accounts]) {
this.token = await ERC721Burnable.new({ from: creator });
});
shouldBehaveLikeERC721Basic(creator, minter, accounts);
shouldBehaveLikeERC721(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 {
shouldBehaveLikeMintAndBurnERC721,
} = require('./ERC721MintBurn.behavior');
......@@ -19,6 +19,6 @@ contract('ERC721Mintable', function ([_, creator, ...accounts]) {
});
});
shouldBehaveLikeERC721Basic(creator, minter, accounts);
shouldBehaveLikeERC721(creator, minter, accounts);
shouldBehaveLikeMintAndBurnERC721(creator, minter, accounts);
});
const { shouldBehaveLikeERC721PausedToken } = require('./ERC721PausedToken.behavior');
const { shouldBehaveLikeERC721Basic } = require('./ERC721Basic.behavior');
const { shouldBehaveLikeERC721 } = require('./ERC721.behavior');
const { shouldBehaveLikePublicRole } = require('../../access/roles/PublicRole.behavior');
const BigNumber = web3.BigNumber;
......@@ -39,7 +39,7 @@ contract('ERC721Pausable', 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 () {
......@@ -48,6 +48,6 @@ contract('ERC721Pausable', function ([
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