Browse Source

Improved ERC721 granularity (#1304)

* Split enumerable and metadata implementations.

* Renamed ERC721Basic to ERC721, and ERC721 to ERC721Full.

* Fixed linter errors.
Nicolás Venturo 7 years ago
parent
commit
4b33eaefa2

+ 0 - 18
contracts/mocks/ERC721BasicMock.sol

@@ -1,18 +0,0 @@
-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);
-  }
-}

+ 30 - 0
contracts/mocks/ERC721FullMock.sol

@@ -0,0 +1,30 @@
+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);
+  }
+}

+ 5 - 3
contracts/mocks/ERC721MintableBurnableImpl.sol

@@ -1,6 +1,6 @@
 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
   {
   }

+ 6 - 18
contracts/mocks/ERC721Mock.sol

@@ -1,30 +1,18 @@
 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);
   }
 }

+ 248 - 139
contracts/token/ERC721/ERC721.sol

@@ -1,217 +1,326 @@
 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;
-
-  // Optional mapping for token URIs
-  mapping(uint256 => string) private _tokenURIs;
-
-  bytes4 private constant _InterfaceId_ERC721Enumerable = 0x780e9d63;
-  /**
-   * 0x780e9d63 ===
-   *   bytes4(keccak256('totalSupply()')) ^
-   *   bytes4(keccak256('tokenOfOwnerByIndex(address,uint256)')) ^
-   *   bytes4(keccak256('tokenByIndex(uint256)'))
+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)'))
    */
 
-  bytes4 private constant _InterfaceId_ERC721Metadata = 0x5b5e139f;
+  constructor()
+    public
+  {
+    // register the supported interfaces to conform to ERC721 via ERC165
+    _registerInterface(_InterfaceId_ERC721);
+  }
+
   /**
-   * 0x5b5e139f ===
-   *   bytes4(keccak256('name()')) ^
-   *   bytes4(keccak256('symbol()')) ^
-   *   bytes4(keccak256('tokenURI(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];
+  }
 
   /**
-   * @dev Constructor function
+   * @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
    */
-  constructor(string name, string symbol) public {
-    _name = name;
-    _symbol = symbol;
-
-    // register the supported interfaces to conform to ERC721 via ERC165
-    _registerInterface(_InterfaceId_ERC721Enumerable);
-    _registerInterface(_InterfaceId_ERC721Metadata);
+  function ownerOf(uint256 tokenId) public view returns (address) {
+    address owner = _tokenOwner[tokenId];
+    require(owner != address(0));
+    return owner;
   }
 
   /**
-   * @dev Gets the token name
-   * @return string representing the token name
+   * @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 name() external view returns (string) {
-    return _name;
+  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 token symbol
-   * @return string representing the token symbol
+   * @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 symbol() external view returns (string) {
-    return _symbol;
+  function getApproved(uint256 tokenId) public view returns (address) {
+    require(_exists(tokenId));
+    return _tokenApprovals[tokenId];
   }
 
   /**
-   * @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 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 tokenURI(uint256 tokenId) public view returns (string) {
-    require(_exists(tokenId));
-    return _tokenURIs[tokenId];
+  function setApprovalForAll(address to, bool approved) public {
+    require(to != msg.sender);
+    _operatorApprovals[msg.sender][to] = approved;
+    emit ApprovalForAll(msg.sender, to, approved);
   }
 
   /**
-   * @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
+   * @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 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
-   */
-  function totalSupply() public view returns (uint256) {
-    return _allTokens.length;
+   * @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 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 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 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);
+    _clearApproval(owner, tokenId);
+    _removeTokenFrom(owner, tokenId);
+    emit Transfer(owner, address(0), tokenId);
+  }
 
-    // Clear metadata (if any)
-    if (bytes(_tokenURIs[tokenId]).length != 0) {
-      delete _tokenURIs[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);
     }
+  }
 
-    // Reorg all tokens array
-    uint256 tokenIndex = _allTokensIndex[tokenId];
-    uint256 lastTokenIndex = _allTokens.length.sub(1);
-    uint256 lastToken = _allTokens[lastTokenIndex];
-
-    _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);
+  }
 }

+ 0 - 325
contracts/token/ERC721/ERC721Basic.sol

@@ -1,325 +0,0 @@
-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);
-  }
-}

+ 146 - 0
contracts/token/ERC721/ERC721Enumerable.sol

@@ -0,0 +1,146 @@
+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;
+  }
+}

+ 19 - 0
contracts/token/ERC721/ERC721Full.sol

@@ -0,0 +1,19 @@
+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
+  {
+  }
+}

+ 88 - 0
contracts/token/ERC721/ERC721Metadata.sol

@@ -0,0 +1,88 @@
+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];
+    }
+  }
+}

+ 2 - 2
contracts/token/ERC721/ERC721Mintable.sol

@@ -1,6 +1,6 @@
 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;

+ 3 - 3
contracts/token/ERC721/ERC721Pausable.sol

@@ -1,14 +1,14 @@
 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

+ 42 - 32
contracts/token/ERC721/IERC721.sol

@@ -1,40 +1,50 @@
 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;
 }

+ 0 - 50
contracts/token/ERC721/IERC721Basic.sol

@@ -1,50 +0,0 @@
-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;
-}

+ 21 - 0
contracts/token/ERC721/IERC721Enumerable.sol

@@ -0,0 +1,21 @@
+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);
+}

+ 13 - 0
contracts/token/ERC721/IERC721Full.sol

@@ -0,0 +1,13 @@
+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 {
+}

+ 14 - 0
contracts/token/ERC721/IERC721Metadata.sol

@@ -0,0 +1,14 @@
+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);
+}

+ 3 - 3
test/token/ERC721/ERC721Basic.behavior.js → test/token/ERC721/ERC721.behavior.js

@@ -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,
 };

+ 4 - 216
test/token/ERC721/ERC721.test.js

@@ -1,8 +1,4 @@
-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;
-
+contract('ERC721', function ([_, creator, ...accounts]) {
   beforeEach(async function () {
-    this.token = await ERC721.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);
-        });
-      });
-    });
+    this.token = await ERC721.new({ from: creator });
   });
 
-  shouldBehaveLikeERC721Basic(creator, minter, accounts);
-  shouldBehaveLikeMintAndBurnERC721(creator, minter, accounts);
-
-  shouldSupportInterfaces([
-    'ERC165',
-    'ERC721',
-    'ERC721Enumerable',
-    'ERC721Metadata',
-  ]);
+  shouldBehaveLikeERC721(creator, creator, accounts);
 });

+ 0 - 16
test/token/ERC721/ERC721Basic.test.js

@@ -1,16 +0,0 @@
-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);
-});

+ 2 - 2
test/token/ERC721/ERC721Burnable.test.js

@@ -1,4 +1,4 @@
-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);
 });

+ 228 - 0
test/token/ERC721/ERC721Full.test.js

@@ -0,0 +1,228 @@
+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',
+  ]);
+});

+ 2 - 2
test/token/ERC721/ERC721Mintable.test.js

@@ -1,4 +1,4 @@
-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);
 });

+ 3 - 3
test/token/ERC721/ERC721Pausable.test.js

@@ -1,5 +1,5 @@
 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);
   });
 });