diff --git a/contracts/mocks/ERC721BasicMock.sol b/contracts/mocks/ERC721BasicMock.sol deleted file mode 100644 index fdb464be421..00000000000 --- a/contracts/mocks/ERC721BasicMock.sol +++ /dev/null @@ -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); - } -} diff --git a/contracts/mocks/ERC721FullMock.sol b/contracts/mocks/ERC721FullMock.sol new file mode 100644 index 00000000000..e3f79b08f84 --- /dev/null +++ b/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); + } +} diff --git a/contracts/mocks/ERC721MintableBurnableImpl.sol b/contracts/mocks/ERC721MintableBurnableImpl.sol index d0eae1fb353..4d3c962d89e 100644 --- a/contracts/mocks/ERC721MintableBurnableImpl.sol +++ b/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 { } diff --git a/contracts/mocks/ERC721Mock.sol b/contracts/mocks/ERC721Mock.sol index a1eb59e04af..607ceed135f 100644 --- a/contracts/mocks/ERC721Mock.sol +++ b/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); } } diff --git a/contracts/token/ERC721/ERC721.sol b/contracts/token/ERC721/ERC721.sol index adfbc5f3771..7501248d7a1 100644 --- a/contracts/token/ERC721/ERC721.sol +++ b/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); + } } diff --git a/contracts/token/ERC721/ERC721Basic.sol b/contracts/token/ERC721/ERC721Basic.sol deleted file mode 100644 index ea15012a168..00000000000 --- a/contracts/token/ERC721/ERC721Basic.sol +++ /dev/null @@ -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); - } -} diff --git a/contracts/token/ERC721/ERC721Enumerable.sol b/contracts/token/ERC721/ERC721Enumerable.sol new file mode 100644 index 00000000000..a4cceed2049 --- /dev/null +++ b/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; + } +} diff --git a/contracts/token/ERC721/ERC721Full.sol b/contracts/token/ERC721/ERC721Full.sol new file mode 100644 index 00000000000..6492a8ca1f8 --- /dev/null +++ b/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 + { + } +} diff --git a/contracts/token/ERC721/ERC721Metadata.sol b/contracts/token/ERC721/ERC721Metadata.sol new file mode 100644 index 00000000000..b4f1da3abfa --- /dev/null +++ b/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]; + } + } +} diff --git a/contracts/token/ERC721/ERC721Mintable.sol b/contracts/token/ERC721/ERC721Mintable.sol index 81b6f8ea6cb..39fb9761df9 100644 --- a/contracts/token/ERC721/ERC721Mintable.sol +++ b/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; diff --git a/contracts/token/ERC721/ERC721Pausable.sol b/contracts/token/ERC721/ERC721Pausable.sol index 0378373a957..c0cca9dcfc7 100644 --- a/contracts/token/ERC721/ERC721Pausable.sol +++ b/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 diff --git a/contracts/token/ERC721/IERC721.sol b/contracts/token/ERC721/IERC721.sol index ebf7060a199..1d4ac1de473 100644 --- a/contracts/token/ERC721/IERC721.sol +++ b/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; } diff --git a/contracts/token/ERC721/IERC721Basic.sol b/contracts/token/ERC721/IERC721Basic.sol deleted file mode 100644 index 9dc6d946885..00000000000 --- a/contracts/token/ERC721/IERC721Basic.sol +++ /dev/null @@ -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; -} diff --git a/contracts/token/ERC721/IERC721Enumerable.sol b/contracts/token/ERC721/IERC721Enumerable.sol new file mode 100644 index 00000000000..b848b7b695b --- /dev/null +++ b/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); +} diff --git a/contracts/token/ERC721/IERC721Full.sol b/contracts/token/ERC721/IERC721Full.sol new file mode 100644 index 00000000000..10914dc23d5 --- /dev/null +++ b/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 { +} diff --git a/contracts/token/ERC721/IERC721Metadata.sol b/contracts/token/ERC721/IERC721Metadata.sol new file mode 100644 index 00000000000..b3cdede1325 --- /dev/null +++ b/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); +} diff --git a/test/token/ERC721/ERC721Basic.behavior.js b/test/token/ERC721/ERC721.behavior.js similarity index 99% rename from test/token/ERC721/ERC721Basic.behavior.js rename to test/token/ERC721/ERC721.behavior.js index b3be95ed020..03e83fabe24 100644 --- a/test/token/ERC721/ERC721Basic.behavior.js +++ b/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, }; diff --git a/test/token/ERC721/ERC721.test.js b/test/token/ERC721/ERC721.test.js index 081d8e86f9e..4c40e397237 100644 --- a/test/token/ERC721/ERC721.test.js +++ b/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); }); diff --git a/test/token/ERC721/ERC721Basic.test.js b/test/token/ERC721/ERC721Basic.test.js deleted file mode 100644 index 708523ed228..00000000000 --- a/test/token/ERC721/ERC721Basic.test.js +++ /dev/null @@ -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); -}); diff --git a/test/token/ERC721/ERC721Burnable.test.js b/test/token/ERC721/ERC721Burnable.test.js index 34605028ddb..a8b011b9070 100644 --- a/test/token/ERC721/ERC721Burnable.test.js +++ b/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); }); diff --git a/test/token/ERC721/ERC721Full.test.js b/test/token/ERC721/ERC721Full.test.js new file mode 100644 index 00000000000..ce8e915fa00 --- /dev/null +++ b/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', + ]); +}); diff --git a/test/token/ERC721/ERC721Mintable.test.js b/test/token/ERC721/ERC721Mintable.test.js index b53ddb59301..65c56864306 100644 --- a/test/token/ERC721/ERC721Mintable.test.js +++ b/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); }); diff --git a/test/token/ERC721/ERC721Pausable.test.js b/test/token/ERC721/ERC721Pausable.test.js index 240e478e8a3..3724655e05c 100644 --- a/test/token/ERC721/ERC721Pausable.test.js +++ b/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); }); });