├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── contracts ├── Core │ ├── Card.sol │ └── ERC721X │ │ ├── ERC721XToken.sol │ │ └── ERC721XTokenNFT.sol ├── Interfaces │ ├── ERC721X.sol │ └── ERC721XReceiver.sol ├── Libraries │ └── ObjectsLib.sol └── Migrations.sol ├── migrations ├── 1_initial_migration.js └── 2_erc721x.js ├── package-lock.json ├── package.json ├── scripts └── test.sh ├── test ├── ERC721X.test.js ├── constants.js └── helpers.js └── truffle-config.js /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2018, Loom Network 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ERC721x — A Smarter Token for the Future of Crypto Collectibles 2 | ERC721x is an extension of ERC721 that adds support for multi-fungible tokens and batch transfers, while being fully backward-compatible. 3 | 4 | **Quick Links:** 5 | 6 | - [ERC721x Interface](contracts/Interfaces/ERC721X.sol) 7 | 8 | - [ERC721x Receiver](contracts/Interfaces/ERC721XReceiver.sol) 9 | 10 | - [ERC721x Reference Implementation](contracts/Core/ERC721X/ERC721XToken.sol) 11 | 12 | - [ERC721x Backwards Compatibility Layer](contracts/Core/ERC721X/ERC721XTokenNFT.sol) 13 | 14 | - [Open source under BSD-3](LICENSE) 15 | --- 16 | 17 | **The ERC721x Interface:** 18 | 19 | ```sol 20 | contract ERC721X { 21 | function implementsERC721X() public pure returns (bool); 22 | function ownerOf(uint256 _tokenId) public view returns (address _owner); 23 | function balanceOf(address owner) public view returns (uint256); 24 | function balanceOf(address owner, uint256 tokenId) public view returns (uint256); 25 | function tokensOwned(address owner) public view returns (uint256[], uint256[]); 26 | 27 | function transfer(address to, uint256 tokenId, uint256 quantity) public; 28 | function transferFrom(address from, address to, uint256 tokenId, uint256 quantity) public; 29 | 30 | // Fungible Safe Transfer From 31 | function safeTransferFrom(address from, address to, uint256 tokenId, uint256 _amount) public; 32 | function safeTransferFrom(address from, address to, uint256 tokenId, uint256 _amount, bytes data) public; 33 | 34 | // Batch Safe Transfer From 35 | function safeBatchTransferFrom(address _from, address _to, uint256[] tokenIds, uint256[] _amounts, bytes _data) public; 36 | 37 | function name() external view returns (string); 38 | function symbol() external view returns (string); 39 | 40 | // Required Events 41 | event TransferWithQuantity(address indexed from, address indexed to, uint256 indexed tokenId, uint256 quantity); 42 | event TransferToken(address indexed from, address indexed to, uint256 indexed tokenId, uint256 quantity); 43 | event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved); 44 | event BatchTransfer(address indexed from, address indexed to, uint256[] tokenTypes, uint256[] amounts); 45 | } 46 | ``` 47 | 48 | ---- 49 | 50 | **Quick Start:** 51 | 52 | ```bash 53 | 54 | yarn add erc721x 55 | 56 | ``` 57 | 58 | 59 | ```bash 60 | 61 | npm install erc721x 62 | 63 | ``` 64 | 65 | To run the tests in this repo, simply clone it and run `truffle test` 66 | 67 | ---- 68 | 69 | ### Background 70 | Here at Loom Network, we’ve been working on Zombie Battleground, a 100% on-chain collectible card game that’s targeted at the mainstream audience. Recently, we finished a Kickstarter campaign, and as part of the early backer packages, we will be delivering almost 2 million cards to these backers. We started with a normal ERC721 smart contract, but quickly realized that we needed some adjustments to make it mainstream-friendly. Here are the criteria we’re working with: 71 | 72 | Transfers should cost very little gas, even if the player is transferring a large quantity of items. For example, someone might want to transfer a few hundred very cheap cards that are worth little individually, but quite valuable in bulk. 73 | One contract should contain multiple “classes” of items. For example, under the broad category of Zombie Battleground cards, we want to have 100 different kinds of cards, each having many copies. 74 | Compatibility with marketplaces, wallets, and existing infrastructure (e.g. Etherscan). Wallet and marketplace makers provide a valuable service to the community, and it makes sense to leverage their existing work. 75 | 76 | ### The Current Landscape 77 | 78 | | ERC # | Cheap Bulk Transfers | Multiple Classes of NFT/FT | Works as a Collectible | Wallet/Marketplace Compatibility | 79 | |---|---|---|---|---| 80 | | ERC721 | NO | NO | YES | YES | 81 | | ERC20 | YES | NO | NO | YES | 82 | | ERC1155 | YES | YES | YES | NO | 83 | | ERC1178 | YES | YES | NO | NO | 84 | 85 | We are not the first ones to need something like this, and there have been a few brilliant proposals on github. But every single instance sacrifices compatibility with existing wallets and marketplaces by creating an entirely new specification. While we wholeheartedly support new breakthroughs, it seemed to us that the more pragmatic path — the one we can use NOW instead of months later — would be to extend ERC721 somehow, rather than abandoning it altogether. 86 | 87 | Our Approach: Extending ERC721 with ERC1178 88 | Out of all the existing solutions to this problem, the one that best suited our needs was ERC1178 (https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1178.md). 89 | 90 | It is extremely easy to read and understand because of its similarity to ERC20 — easy enough that any curious user can audit the smart contract and see what the developer put in it. (If they need a little help, doing a lesson on CryptoZombies.io should be enough 😉) 91 | It has very little bloat — just the bare minimum to implement the necessary features. The fewer things added, the better the chances are that it’s secure, because it deviates less from battle-tested code. 92 | It’s really useful for things beyond just games — for example, creating a token that can represent preferred, common, or restricted shares of a company. 93 | 94 | ![image copied on 2018-09-07 at 19 14 21 pm](https://user-images.githubusercontent.com/1289797/45216191-45e03d00-b2d2-11e8-8fa8-88bc761a3584.png) 95 | 96 | 97 | Using ERC1178 as the base, we added a very thin optional layer of features to support crypto-collectibles, then wrapped everything with an ERC721 compatibility layer. 98 | 99 | ### Real World Usage 100 | 101 | ERC721x is immediately usable with any ERC721-compatible wallet, marketplace, or service. For example, you can browse for a card in Trust Wallet and easily transfer it to your friend. That person can check the status of the transfer on Etherscan, and then resell it by sending it to OpenSea or Rarebits. 102 | 103 | Then, on a service that supports the enhanced features, such as cheap batch transfers, you get all the improved benefits, without the end user needing to know about any of the details. For example, on the Loom Trading Post, you can send hundreds of cards for the price of sending one, and you can enjoy transactions that are completely free by storing the cards on PlasmaChain 😎 104 | 105 | ### Conclusion 106 | 107 | Beyond the technical bits that make up blockchains, the spirit of blockchain tech is equally (if not more) important. Services should be interoperable, open, and compatible. It doesn’t matter if you add a million features when the end user has no wallet that can open them and no service like Etherscan that can view them. 108 | 109 | At the same time, any improvements made to a technology should aim to be as seamless as possible. We can see a wonderful example of this with our USB devices. There’s absolutely no need for us to stop and think, “Is this USB 1.0, 2.0, or 3.0?” We are spared from this mental overhead because, even if not all the new features are supported, we will still be able to use the device the exact same way. 110 | 111 | It’s these two principles that led us to create the new ERC721x, specifically for crypto-collectibles — and it’s completely open source. 112 | -------------------------------------------------------------------------------- /contracts/Core/Card.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.6; 2 | 3 | import "./ERC721X/ERC721XToken.sol"; 4 | 5 | // Example 6 | 7 | contract Card is ERC721XToken { 8 | 9 | constructor(string memory _baseTokenURI) public ERC721XToken(_baseTokenURI) {} 10 | 11 | function name() external view returns (string memory) { 12 | return "Card"; 13 | } 14 | 15 | function symbol() external view returns (string memory) { 16 | return "CRD"; 17 | } 18 | 19 | // fungible mint 20 | function mint(uint256 _tokenId, address _to, uint256 _supply) external { 21 | _mint(_tokenId, _to, _supply); 22 | } 23 | 24 | // nft mint 25 | function mint(uint256 _tokenId, address _to) external { 26 | _mint(_tokenId, _to); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /contracts/Core/ERC721X/ERC721XToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.6; 2 | 3 | import "./../../Interfaces/ERC721X.sol"; 4 | 5 | import "./../../Interfaces/ERC721XReceiver.sol"; 6 | import "./ERC721XTokenNFT.sol"; 7 | 8 | import "openzeppelin-solidity/contracts/utils/Address.sol"; 9 | import "./../../Libraries/ObjectsLib.sol"; 10 | 11 | 12 | // Additional features over NFT token that is compatible with batch transfers 13 | contract ERC721XToken is ERC721X, ERC721XTokenNFT { 14 | 15 | using ObjectLib for ObjectLib.Operations; 16 | using Address for address; 17 | 18 | bytes4 internal constant ERC721X_RECEIVED = 0x660b3370; 19 | bytes4 internal constant ERC721X_BATCH_RECEIVE_SIG = 0xe9e5be6a; 20 | 21 | event BatchTransfer(address from, address to, uint256[] tokenTypes, uint256[] amounts); 22 | 23 | constructor(string memory _baseTokenURI) public ERC721XTokenNFT(_baseTokenURI) {} 24 | 25 | 26 | modifier isOperatorOrOwner(address _from) { 27 | require((msg.sender == _from) || operators[_from][msg.sender], "msg.sender is neither _from nor operator"); 28 | _; 29 | } 30 | 31 | function implementsERC721X() public pure returns (bool) { 32 | return true; 33 | } 34 | 35 | /** 36 | * @dev transfer objects from different tokenIds to specified address 37 | * @param _from The address to BatchTransfer objects from. 38 | * @param _to The address to batchTransfer objects to. 39 | * @param _tokenIds Array of tokenIds to update balance of 40 | * @param _amounts Array of amount of object per type to be transferred. 41 | * Note: Arrays should be sorted so that all tokenIds in a same bin are adjacent (more efficient). 42 | */ 43 | function _batchTransferFrom(address _from, address _to, uint256[] memory _tokenIds, uint256[] memory _amounts) 44 | internal 45 | isOperatorOrOwner(_from) 46 | { 47 | 48 | // Requirements 49 | require(_tokenIds.length == _amounts.length, "Inconsistent array length between args"); 50 | require(_to != address(0), "Invalid recipient"); 51 | 52 | if (tokenType[_tokenIds[0]] == NFT) { 53 | tokenOwner[_tokenIds[0]] = _to; 54 | emit Transfer(_from, _to, _tokenIds[0]); 55 | } 56 | 57 | // Load first bin and index where the object balance exists 58 | (uint256 bin, uint256 index) = ObjectLib.getTokenBinIndex(_tokenIds[0]); 59 | 60 | // Balance for current bin in memory (initialized with first transfer) 61 | // Written with bad library syntax instead of as below to bypass stack limit error 62 | uint256 balFrom = ObjectLib.updateTokenBalance( 63 | packedTokenBalance[_from][bin], index, _amounts[0], ObjectLib.Operations.SUB 64 | ); 65 | uint256 balTo = ObjectLib.updateTokenBalance( 66 | packedTokenBalance[_to][bin], index, _amounts[0], ObjectLib.Operations.ADD 67 | ); 68 | 69 | // Number of transfers to execute 70 | uint256 nTransfer = _tokenIds.length; 71 | 72 | // Last bin updated 73 | uint256 lastBin = bin; 74 | 75 | for (uint256 i = 1; i < nTransfer; i++) { 76 | // If we're transferring an NFT we additionally should update the tokenOwner and emit the corresponding event 77 | if (tokenType[_tokenIds[i]] == NFT) { 78 | tokenOwner[_tokenIds[i]] = _to; 79 | emit Transfer(_from, _to, _tokenIds[i]); 80 | } 81 | (bin, index) = _tokenIds[i].getTokenBinIndex(); 82 | 83 | // If new bin 84 | if (bin != lastBin) { 85 | // Update storage balance of previous bin 86 | packedTokenBalance[_from][lastBin] = balFrom; 87 | packedTokenBalance[_to][lastBin] = balTo; 88 | 89 | // Load current bin balance in memory 90 | balFrom = packedTokenBalance[_from][bin]; 91 | balTo = packedTokenBalance[_to][bin]; 92 | 93 | // Bin will be the most recent bin 94 | lastBin = bin; 95 | } 96 | 97 | // Update memory balance 98 | balFrom = balFrom.updateTokenBalance(index, _amounts[i], ObjectLib.Operations.SUB); 99 | balTo = balTo.updateTokenBalance(index, _amounts[i], ObjectLib.Operations.ADD); 100 | } 101 | 102 | // Update storage of the last bin visited 103 | packedTokenBalance[_from][bin] = balFrom; 104 | packedTokenBalance[_to][bin] = balTo; 105 | 106 | // Emit batchTransfer event 107 | emit BatchTransfer(_from, _to, _tokenIds, _amounts); 108 | } 109 | 110 | function batchTransferFrom(address _from, address _to, uint256[] memory _tokenIds, uint256[] memory _amounts) public { 111 | // Batch Transfering 112 | _batchTransferFrom(_from, _to, _tokenIds, _amounts); 113 | } 114 | 115 | /** 116 | * @dev transfer objects from different tokenIds to specified address 117 | * @param _from The address to BatchTransfer objects from. 118 | * @param _to The address to batchTransfer objects to. 119 | * @param _tokenIds Array of tokenIds to update balance of 120 | * @param _amounts Array of amount of object per type to be transferred. 121 | * @param _data Data to pass to onERC721XReceived() function if recipient is contract 122 | * Note: Arrays should be sorted so that all tokenIds in a same bin are adjacent (more efficient). 123 | */ 124 | function safeBatchTransferFrom( 125 | address _from, 126 | address _to, 127 | uint256[] memory _tokenIds, 128 | uint256[] memory _amounts, 129 | bytes memory _data 130 | ) 131 | public 132 | { 133 | 134 | // Batch Transfering 135 | _batchTransferFrom(_from, _to, _tokenIds, _amounts); 136 | 137 | // Pass data if recipient is contract 138 | if (_to.isContract()) { 139 | bytes4 retval = ERC721XReceiver(_to).onERC721XBatchReceived( 140 | msg.sender, _from, _tokenIds, _amounts, _data 141 | ); 142 | require(retval == ERC721X_BATCH_RECEIVE_SIG); 143 | } 144 | } 145 | 146 | function transfer(address _to, uint256 _tokenId, uint256 _amount) public { 147 | _transferFrom(msg.sender, _to, _tokenId, _amount); 148 | } 149 | 150 | function transferFrom(address _from, address _to, uint256 _tokenId, uint256 _amount) public { 151 | _transferFrom(_from, _to, _tokenId, _amount); 152 | } 153 | 154 | function _transferFrom(address _from, address _to, uint256 _tokenId, uint256 _amount) 155 | internal 156 | isOperatorOrOwner(_from) 157 | { 158 | require(tokenType[_tokenId] == FT); 159 | require(_amount <= balanceOf(_from, _tokenId), "Quantity greater than from balance"); 160 | require(_to != address(0), "Invalid to address"); 161 | 162 | _updateTokenBalance(_from, _tokenId, _amount, ObjectLib.Operations.SUB); 163 | _updateTokenBalance(_to, _tokenId, _amount, ObjectLib.Operations.ADD); 164 | emit TransferWithQuantity(_from, _to, _tokenId, _amount); 165 | } 166 | 167 | function safeTransferFrom(address _from, address _to, uint256 _tokenId, uint256 _amount) public { 168 | safeTransferFrom(_from, _to, _tokenId, _amount, ""); 169 | } 170 | 171 | function safeTransferFrom(address _from, address _to, uint256 _tokenId, uint256 _amount, bytes memory _data) public { 172 | _transferFrom(_from, _to, _tokenId, _amount); 173 | require( 174 | checkAndCallSafeTransfer(_from, _to, _tokenId, _amount, _data), 175 | "Sent to a contract which is not an ERC721X receiver" 176 | ); 177 | } 178 | 179 | function _mint(uint256 _tokenId, address _to, uint256 _supply) internal { 180 | // If the token doesn't exist, add it to the tokens array 181 | if (!exists(_tokenId)) { 182 | tokenType[_tokenId] = FT; 183 | allTokens.push(_tokenId); 184 | } else { 185 | // if the token exists, it must be a FT 186 | require(tokenType[_tokenId] == FT, "Not a FT"); 187 | } 188 | 189 | _updateTokenBalance(_to, _tokenId, _supply, ObjectLib.Operations.ADD); 190 | emit TransferWithQuantity(address(this), _to, _tokenId, _supply); 191 | } 192 | 193 | 194 | function checkAndCallSafeTransfer( 195 | address _from, 196 | address _to, 197 | uint256 _tokenId, 198 | uint256 _amount, 199 | bytes memory _data 200 | ) 201 | internal 202 | returns (bool) 203 | { 204 | if (!_to.isContract()) { 205 | return true; 206 | } 207 | 208 | bytes4 retval = ERC721XReceiver(_to).onERC721XReceived( 209 | msg.sender, _from, _tokenId, _amount, _data); 210 | return(retval == ERC721X_RECEIVED); 211 | } 212 | 213 | } 214 | -------------------------------------------------------------------------------- /contracts/Core/ERC721X/ERC721XTokenNFT.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.6; 2 | 3 | import "openzeppelin-solidity/contracts/token/ERC721/ERC721.sol"; 4 | import "openzeppelin-solidity/contracts/token/ERC721/IERC721Receiver.sol"; 5 | import "openzeppelin-solidity/contracts/utils/Address.sol"; 6 | import "../../Libraries/ObjectsLib.sol"; 7 | 8 | 9 | // Packed NFT that has storage which is batch transfer compatible 10 | contract ERC721XTokenNFT is ERC721 { 11 | 12 | using ObjectLib for ObjectLib.Operations; 13 | using ObjectLib for uint256; 14 | using Address for address; 15 | 16 | // bytes4 internal constant InterfaceId_ERC721Enumerable = 0x780e9d63; 17 | bytes4 internal constant ERC721_RECEIVED = 0x150b7a02; 18 | bytes4 internal constant InterfaceId_ERC721Metadata = 0x5b5e139f; 19 | 20 | uint256[] internal allTokens; 21 | mapping(address => mapping(uint256 => uint256)) packedTokenBalance; 22 | mapping(uint256 => address) internal tokenOwner; 23 | mapping(address => mapping(address => bool)) operators; 24 | mapping (uint256 => address) internal tokenApprovals; 25 | mapping(uint256 => uint256) tokenType; 26 | 27 | uint256 constant NFT = 1; 28 | uint256 constant FT = 2; 29 | 30 | string baseTokenURI; 31 | 32 | constructor(string memory _baseTokenURI) public { 33 | baseTokenURI = _baseTokenURI; 34 | _registerInterface(InterfaceId_ERC721Metadata); 35 | } 36 | 37 | function name() external view returns (string memory) { 38 | return "ERC721XTokenNFT"; 39 | } 40 | 41 | function symbol() external view returns (string memory) { 42 | return "ERC721X"; 43 | } 44 | 45 | /** 46 | * @dev Returns whether the specified token exists 47 | * @param _tokenId uint256 ID of the token to query the existence of 48 | * @return whether the token exists 49 | */ 50 | function exists(uint256 _tokenId) public view returns (bool) { 51 | return tokenType[_tokenId] != 0; 52 | } 53 | 54 | function implementsERC721() public pure returns (bool) { 55 | return true; 56 | } 57 | 58 | /** 59 | * @dev Gets the total amount of tokens stored by the contract 60 | * @return uint256 representing the total amount of tokens 61 | */ 62 | function totalSupply() public view returns (uint256) { 63 | return allTokens.length; 64 | } 65 | 66 | /** 67 | * @dev Gets the token ID at a given index of all the tokens in this contract 68 | * Reverts if the index is greater or equal to the total number of tokens 69 | * @param _index uint256 representing the index to be accessed of the tokens list 70 | * @return uint256 token ID at the given index of the tokens list 71 | */ 72 | function tokenByIndex(uint256 _index) public view returns (uint256) { 73 | require(_index < totalSupply()); 74 | return allTokens[_index]; 75 | } 76 | 77 | /** 78 | * @dev Gets the owner of a given NFT 79 | * @param _tokenId uint256 representing the unique token identifier 80 | * @return address the owner of the token 81 | */ 82 | function ownerOf(uint256 _tokenId) public view returns (address) { 83 | require(tokenOwner[_tokenId] != address(0), "Coin does not exist"); 84 | return tokenOwner[_tokenId]; 85 | } 86 | 87 | /** 88 | * @dev Gets Iterate through the list of existing tokens and return the indexes 89 | * and balances of the tokens owner by the user 90 | * @param _owner The adddress we are checking 91 | * @return indexes The tokenIds 92 | * @return balances The balances of each token 93 | */ 94 | function tokensOwned(address _owner) public view returns (uint256[] memory indexes, uint256[] memory balances) { 95 | uint256 numTokens = totalSupply(); 96 | uint256[] memory tokenIndexes = new uint256[](numTokens); 97 | uint256[] memory tempTokens = new uint256[](numTokens); 98 | 99 | uint256 count; 100 | for (uint256 i = 0; i < numTokens; i++) { 101 | uint256 tokenId = allTokens[i]; 102 | if (balanceOf(_owner, tokenId) > 0) { 103 | tempTokens[count] = balanceOf(_owner, tokenId); 104 | tokenIndexes[count] = tokenId; 105 | count++; 106 | } 107 | } 108 | 109 | // copy over the data to a correct size array 110 | uint256[] memory _ownedTokens = new uint256[](count); 111 | uint256[] memory _ownedTokensIndexes = new uint256[](count); 112 | 113 | for (uint256 i = 0; i < count; i++) { 114 | _ownedTokens[i] = tempTokens[i]; 115 | _ownedTokensIndexes[i] = tokenIndexes[i]; 116 | } 117 | 118 | return (_ownedTokensIndexes, _ownedTokens); 119 | } 120 | 121 | /** 122 | * @dev Gets the number of tokens owned by the address we are checking 123 | * @param _owner The adddress we are checking 124 | * @return balance The unique amount of tokens owned 125 | */ 126 | function balanceOf(address _owner) public view returns (uint256 balance) { 127 | (,uint256[] memory tokens) = tokensOwned(_owner); 128 | return tokens.length; 129 | } 130 | 131 | /** 132 | * @dev return the _tokenId type' balance of _address 133 | * @param _address Address to query balance of 134 | * @param _tokenId type to query balance of 135 | * @return Amount of objects of a given type ID 136 | */ 137 | function balanceOf(address _address, uint256 _tokenId) public view returns (uint256) { 138 | (uint256 bin, uint256 index) = _tokenId.getTokenBinIndex(); 139 | return packedTokenBalance[_address][bin].getValueInBin(index); 140 | } 141 | 142 | function safeTransferFrom( 143 | address _from, 144 | address _to, 145 | uint256 _tokenId 146 | ) 147 | public 148 | { 149 | safeTransferFrom(_from, _to, _tokenId, ""); 150 | } 151 | 152 | function safeTransferFrom( 153 | address _from, 154 | address _to, 155 | uint256 _tokenId, 156 | bytes memory _data 157 | ) 158 | public 159 | { 160 | _transferFrom(_from, _to, _tokenId); 161 | require( 162 | checkAndCallSafeTransfer(_from, _to, _tokenId, _data), 163 | "Sent to a contract which is not an ERC721 receiver" 164 | ); 165 | } 166 | 167 | function transferFrom(address _from, address _to, uint256 _tokenId) public { 168 | _transferFrom(_from, _to, _tokenId); 169 | } 170 | 171 | function _transferFrom(address _from, address _to, uint256 _tokenId) 172 | internal 173 | { 174 | require(tokenType[_tokenId] == NFT); 175 | require(isApprovedOrOwner(_from, ownerOf(_tokenId), _tokenId)); 176 | require(_to != address(0), "Invalid to address"); 177 | 178 | _updateTokenBalance(_from, _tokenId, 0, ObjectLib.Operations.REPLACE); 179 | _updateTokenBalance(_to, _tokenId, 1, ObjectLib.Operations.REPLACE); 180 | 181 | tokenOwner[_tokenId] = _to; 182 | emit Transfer(_from, _to, _tokenId); 183 | } 184 | 185 | function tokenURI(uint256 _tokenId) public view returns (string memory) { 186 | require(exists(_tokenId), "Token doesn't exist"); 187 | return string(abi.encodePacked( 188 | baseTokenURI, 189 | uint2str(_tokenId), 190 | ".json" 191 | )); 192 | } 193 | 194 | function uint2str(uint _i) private pure returns (string memory _uintAsString) { 195 | if (_i == 0) { 196 | return "0"; 197 | } 198 | 199 | uint j = _i; 200 | uint len; 201 | while (j != 0) { 202 | len++; 203 | j /= 10; 204 | } 205 | 206 | bytes memory bstr = new bytes(len); 207 | uint k = len - 1; 208 | while (_i != 0) { 209 | bstr[k--] = byte(uint8(48 + _i % 10)); 210 | _i /= 10; 211 | } 212 | 213 | return string(bstr); 214 | } 215 | 216 | /** 217 | * @dev Internal function to invoke `onERC721Received` on a target address 218 | * The call is not executed if the target address is not a contract 219 | * @param _from address representing the previous owner of the given token ID 220 | * @param _to target address that will receive the tokens 221 | * @param _tokenId uint256 ID of the token to be transferred 222 | * @param _data bytes optional data to send along with the call 223 | * @return whether the call correctly returned the expected magic value 224 | */ 225 | function checkAndCallSafeTransfer( 226 | address _from, 227 | address _to, 228 | uint256 _tokenId, 229 | bytes memory _data 230 | ) 231 | internal 232 | returns (bool) 233 | { 234 | if (!_to.isContract()) { 235 | return true; 236 | } 237 | bytes4 retval = IERC721Receiver(_to).onERC721Received( 238 | msg.sender, _from, _tokenId, _data 239 | ); 240 | return (retval == ERC721_RECEIVED); 241 | } 242 | 243 | /** 244 | * @dev Will set _operator operator status to true or false 245 | * @param _operator Address to changes operator status. 246 | * @param _approved _operator's new operator status (true or false) 247 | */ 248 | function setApprovalForAll(address _operator, bool _approved) public { 249 | // Update operator status 250 | operators[msg.sender][_operator] = _approved; 251 | emit ApprovalForAll(msg.sender, _operator, _approved); 252 | } 253 | 254 | /** 255 | * @dev Approves another address to transfer the given token ID 256 | * The zero address indicates there is no approved address. 257 | * There can only be one approved address per token at a given time. 258 | * Can only be called by the token owner or an approved operator. 259 | * @param _to address to be approved for the given token ID 260 | * @param _tokenId uint256 ID of the token to be approved 261 | */ 262 | function approve(address _to, uint256 _tokenId) public { 263 | address owner = ownerOf(_tokenId); 264 | require(_to != owner); 265 | require(msg.sender == owner || isApprovedForAll(owner, msg.sender)); 266 | 267 | tokenApprovals[_tokenId] = _to; 268 | emit Approval(owner, _to, _tokenId); 269 | } 270 | 271 | function _mint(uint256 _tokenId, address _to) internal { 272 | require(!exists(_tokenId), "Error: Tried to mint duplicate token id"); 273 | _updateTokenBalance(_to, _tokenId, 1, ObjectLib.Operations.REPLACE); 274 | tokenOwner[_tokenId] = _to; 275 | tokenType[_tokenId] = NFT; 276 | allTokens.push(_tokenId); 277 | emit Transfer(address(this), _to, _tokenId); 278 | } 279 | 280 | function _updateTokenBalance( 281 | address _from, 282 | uint256 _tokenId, 283 | uint256 _amount, 284 | ObjectLib.Operations op 285 | ) 286 | internal 287 | { 288 | (uint256 bin, uint256 index) = _tokenId.getTokenBinIndex(); 289 | packedTokenBalance[_from][bin] = 290 | packedTokenBalance[_from][bin].updateTokenBalance( 291 | index, _amount, op 292 | ); 293 | } 294 | 295 | 296 | /** 297 | * @dev Gets the approved address for a token ID, or zero if no address set 298 | * @param _tokenId uint256 ID of the token to query the approval of 299 | * @return address currently approved for the given token ID 300 | */ 301 | function getApproved(uint256 _tokenId) public view returns (address) { 302 | return tokenApprovals[_tokenId]; 303 | } 304 | 305 | /** 306 | * @dev Function that verifies whether _operator is an authorized operator of _tokenHolder. 307 | * @param _operator The address of the operator to query status of 308 | * @param _owner Address of the tokenHolder 309 | * @return A uint256 specifying the amount of tokens still available for the spender. 310 | */ 311 | function isApprovedForAll(address _owner, address _operator) public view returns (bool isOperator) { 312 | return operators[_owner][_operator]; 313 | } 314 | 315 | function isApprovedOrOwner(address _spender, address _owner, uint256 _tokenId) 316 | internal 317 | view 318 | returns (bool) 319 | { 320 | return ( 321 | _spender == _owner || 322 | getApproved(_tokenId) == _spender || 323 | isApprovedForAll(_owner, _spender) 324 | ); 325 | } 326 | 327 | // FOR COMPATIBILITY WITH ERC721 Standard, UNUSED. 328 | function tokenOfOwnerByIndex(address _owner, uint256 _index) public pure returns (uint256 _tokenId) {_owner; _index; return 0;} 329 | } 330 | -------------------------------------------------------------------------------- /contracts/Interfaces/ERC721X.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.6; 2 | 3 | 4 | contract ERC721X { 5 | function implementsERC721X() public pure returns (bool); 6 | function ownerOf(uint256 _tokenId) public view returns (address _owner); 7 | function balanceOf(address owner) public view returns (uint256); 8 | function balanceOf(address owner, uint256 tokenId) public view returns (uint256); 9 | function tokensOwned(address owner) public view returns (uint256[] memory, uint256[] memory); 10 | 11 | function transfer(address to, uint256 tokenId, uint256 quantity) public; 12 | function transferFrom(address from, address to, uint256 tokenId, uint256 quantity) public; 13 | 14 | // Fungible Safe Transfer From 15 | function safeTransferFrom(address from, address to, uint256 tokenId, uint256 _amount) public; 16 | function safeTransferFrom(address from, address to, uint256 tokenId, uint256 _amount, bytes memory data) public; 17 | 18 | // Batch Safe Transfer From 19 | function safeBatchTransferFrom(address _from, address _to, uint256[] memory tokenIds, uint256[] memory _amounts, bytes memory _data) public; 20 | 21 | function name() external view returns (string memory); 22 | function symbol() external view returns (string memory); 23 | 24 | // Required Events 25 | event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); 26 | event TransferWithQuantity(address indexed from, address indexed to, uint256 indexed tokenId, uint256 quantity); 27 | event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved); 28 | event BatchTransfer(address indexed from, address indexed to, uint256[] tokenTypes, uint256[] amounts); 29 | } 30 | -------------------------------------------------------------------------------- /contracts/Interfaces/ERC721XReceiver.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.6; 2 | 3 | 4 | /** 5 | * @title ERC721X token receiver interface 6 | * @dev Interface for any contract that wants to support safeTransfers 7 | * from ERC721X contracts. 8 | */ 9 | contract ERC721XReceiver { 10 | /** 11 | * @dev Magic value to be returned upon successful reception of an amount of ERC721X tokens 12 | * Equals to `bytes4(keccak256("onERC721XReceived(address,uint256,bytes)"))`, 13 | * which can be also obtained as `ERC721XReceiver(0).onERC721XReceived.selector` 14 | */ 15 | bytes4 constant ERC721X_RECEIVED = 0x660b3370; 16 | bytes4 constant ERC721X_BATCH_RECEIVE_SIG = 0xe9e5be6a; 17 | 18 | function onERC721XReceived(address _operator, address _from, uint256 tokenId, uint256 amount, bytes memory data) public returns(bytes4); 19 | 20 | /** 21 | * @dev Handle the receipt of multiple fungible tokens from an MFT contract. The ERC721X smart contract calls 22 | * this function on the recipient after a `batchTransfer`. This function MAY throw to revert and reject the 23 | * transfer. Return of other than the magic value MUST result in the transaction being reverted. 24 | * Returns `bytes4(keccak256("onERC721XBatchReceived(address,address,uint256[],uint256[],bytes)"))` unless throwing. 25 | * @notice The contract address is always the message sender. A wallet/broker/auction application 26 | * MUST implement the wallet interface if it will accept safe transfers. 27 | * @param _operator The address which called `safeTransferFrom` function. 28 | * @param _from The address from which the token was transfered from. 29 | * @param _types Array of types of token being transferred (where each type is represented as an ID) 30 | * @param _amounts Array of amount of object per type to be transferred. 31 | * @param _data Additional data with no specified format. 32 | */ 33 | function onERC721XBatchReceived( 34 | address _operator, 35 | address _from, 36 | uint256[] memory _types, 37 | uint256[] memory _amounts, 38 | bytes memory _data 39 | ) 40 | public 41 | returns(bytes4); 42 | } 43 | -------------------------------------------------------------------------------- /contracts/Libraries/ObjectsLib.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.6; 2 | 3 | import "openzeppelin-solidity/contracts/math/SafeMath.sol"; 4 | 5 | library ObjectLib { 6 | 7 | using SafeMath for uint256; 8 | enum Operations { ADD, SUB, REPLACE } 9 | // Constants regarding bin or chunk sizes for balance packing 10 | uint256 constant TYPES_BITS_SIZE = 16; // Max size of each object 11 | uint256 constant TYPES_PER_UINT256 = 256 / TYPES_BITS_SIZE; // Number of types per uint256 12 | 13 | 14 | // 15 | // Objects and Tokens Functions 16 | // 17 | 18 | /** 19 | * @dev Return the bin number and index within that bin where ID is 20 | * @param _tokenId Object type 21 | * @return (Bin number, ID's index within that bin) 22 | */ 23 | function getTokenBinIndex(uint256 _tokenId) internal pure returns (uint256 bin, uint256 index) { 24 | bin = _tokenId * TYPES_BITS_SIZE / 256; 25 | index = _tokenId % TYPES_PER_UINT256; 26 | return (bin, index); 27 | } 28 | 29 | 30 | /** 31 | * @dev update the balance of a type provided in _binBalances 32 | * @param _binBalances Uint256 containing the balances of objects 33 | * @param _index Index of the object in the provided bin 34 | * @param _amount Value to update the type balance 35 | * @param _operation Which operation to conduct : 36 | * Operations.REPLACE : Replace type balance with _amount 37 | * Operations.ADD : ADD _amount to type balance 38 | * Operations.SUB : Substract _amount from type balance 39 | */ 40 | function updateTokenBalance( 41 | uint256 _binBalances, 42 | uint256 _index, 43 | uint256 _amount, 44 | Operations _operation) internal pure returns (uint256 newBinBalance) 45 | { 46 | uint256 objectBalance; 47 | if (_operation == Operations.ADD) { 48 | 49 | objectBalance = getValueInBin(_binBalances, _index); 50 | newBinBalance = writeValueInBin(_binBalances, _index, objectBalance.add(_amount)); 51 | 52 | } else if (_operation == Operations.SUB) { 53 | 54 | objectBalance = getValueInBin(_binBalances, _index); 55 | newBinBalance = writeValueInBin(_binBalances, _index, objectBalance.sub(_amount)); 56 | 57 | } else if (_operation == Operations.REPLACE) { 58 | 59 | newBinBalance = writeValueInBin(_binBalances, _index, _amount); 60 | 61 | } else { 62 | revert("Invalid operation"); // Bad operation 63 | } 64 | 65 | return newBinBalance; 66 | } 67 | /* 68 | * @dev return value in _binValue at position _index 69 | * @param _binValue uint256 containing the balances of TYPES_PER_UINT256 types 70 | * @param _index index at which to retrieve value 71 | * @return Value at given _index in _bin 72 | */ 73 | function getValueInBin(uint256 _binValue, uint256 _index) internal pure returns (uint256) { 74 | 75 | // Mask to retrieve data for a given binData 76 | uint256 mask = (uint256(1) << TYPES_BITS_SIZE) - 1; 77 | 78 | // Shift amount 79 | uint256 rightShift = 256 - TYPES_BITS_SIZE * (_index + 1); 80 | return (_binValue >> rightShift) & mask; 81 | } 82 | 83 | /** 84 | * @dev return the updated _binValue after writing _amount at _index 85 | * @param _binValue uint256 containing the balances of TYPES_PER_UINT256 types 86 | * @param _index Index at which to retrieve value 87 | * @param _amount Value to store at _index in _bin 88 | * @return Value at given _index in _bin 89 | */ 90 | function writeValueInBin(uint256 _binValue, uint256 _index, uint256 _amount) internal pure returns (uint256) { 91 | require(_amount < 2**TYPES_BITS_SIZE, "Amount to write in bin is too large"); 92 | 93 | // Mask to retrieve data for a given binData 94 | uint256 mask = (uint256(1) << TYPES_BITS_SIZE) - 1; 95 | 96 | // Shift amount 97 | uint256 leftShift = 256 - TYPES_BITS_SIZE * (_index + 1); 98 | return (_binValue & ~(mask << leftShift) ) | (_amount << leftShift); 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity <0.6.0; 2 | 3 | 4 | contract Migrations { 5 | address public owner; 6 | 7 | // solhint-disable-next-line 8 | uint public last_completed_migration; 9 | 10 | constructor () public { 11 | owner = msg.sender; 12 | } 13 | 14 | modifier restricted() { 15 | if (msg.sender == owner) { 16 | _; 17 | } 18 | } 19 | 20 | function setCompleted(uint completed) public restricted { 21 | last_completed_migration = completed; 22 | } 23 | 24 | // solhint-disable-next-line 25 | function upgrade(address new_address) public restricted { 26 | Migrations upgraded = Migrations(new_address); 27 | upgraded.setCompleted(last_completed_migration); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | const Migrations = artifacts.require('./Migrations.sol') 2 | 3 | module.exports = (deployer) => { 4 | deployer.deploy(Migrations) 5 | }; 6 | -------------------------------------------------------------------------------- /migrations/2_erc721x.js: -------------------------------------------------------------------------------- 1 | const Card = artifacts.require('./Card.sol') 2 | 3 | module.exports = (deployer) => { 4 | const baseTokenURI = "https://rinkeby.loom.games/erc721/zmb/" 5 | deployer.deploy(Card, baseTokenURI) 6 | }; 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "erc721x", 3 | "version": "1.0.1", 4 | "description": "ERC721X Solidity contracts", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "rm -rf build && bash scripts/test.sh", 8 | "test:gas": "GAS_REPORTER=true npm test", 9 | "compile": "truffle compile", 10 | "lint": "solium --dir ./contracts", 11 | "deploy:ganache": "rm -rf build && truffle deploy --reset --network ganache", 12 | "deploy:rinkeby": "rm -rf build && truffle deploy --reset --network rinkeby" 13 | }, 14 | "keywords": [ 15 | "blockchain" 16 | ], 17 | "author": { 18 | "name": "Loom Network", 19 | "url": "https://loomx.io" 20 | }, 21 | "license": "BSD-3-Clause", 22 | "devDependencies": { 23 | "bignumber.js": "^7.2.1", 24 | "bn-chai": "^1.0.1", 25 | "bn.js": "^4.11.8", 26 | "chai": "^4.1.2", 27 | "chai-as-promised": "^7.1.1", 28 | "chai-bignumber": "^2.0.2", 29 | "eth-gas-reporter": "^0.1.10", 30 | "ethereumjs-util": "^5.2.0", 31 | "ganache-cli": "^6.1.8", 32 | "openzeppelin-solidity": "^2.2.0", 33 | "truffle": "^5.0.9", 34 | "truffle-hdwallet-provider": "git+https://github.com/loomnetwork/truffle-hdwallet-provider.git#web3-one", 35 | "web3-utils": "1.0.0-beta.34" 36 | }, 37 | "dependencies": { 38 | "solium": "^1.1.8" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ganache_port=7545 4 | 5 | function ganache_running() { 6 | nc -z localhost "$ganache_port" 7 | } 8 | 9 | function cleanup { 10 | echo "Exiting ganache-cli with pid $ganache_pid" 11 | kill -9 $ganache_pid 12 | } 13 | 14 | function start_ganache { 15 | ganache-cli -p $ganache_port -e 1000000 -l 7503668 > /dev/null & 16 | ganache_pid=$! 17 | echo "Started ganache-cli with pid $ganache_pid" 18 | trap cleanup EXIT 19 | } 20 | 21 | if ganache_running; then 22 | echo "Using existing ganache instance at port $ganache_port" 23 | else 24 | echo "Starting our own ganache instance at port $ganache_port" 25 | start_ganache 26 | fi 27 | 28 | truffle test --network development 29 | -------------------------------------------------------------------------------- /test/ERC721X.test.js: -------------------------------------------------------------------------------- 1 | const { assertEventVar, 2 | expectThrow, 3 | } = require('./helpers') 4 | const { BN } = web3.utils 5 | const bnChai = require('bn-chai') 6 | 7 | require('chai') 8 | .use(require('chai-as-promised')) 9 | .use(bnChai(BN)) 10 | .should() 11 | 12 | const Card = artifacts.require('Card') 13 | 14 | const safeTransferFromNoDataFT = async function(token, from, to, uid, amount, opts) { 15 | return token.methods['safeTransferFrom(address,address,uint256,uint256)'](from, to, uid, amount, opts) 16 | } 17 | 18 | const safeTransferFromNoDataNFT = async function(token, from, to, uid, opts) { 19 | return token.methods['safeTransferFrom(address,address,uint256)'](from, to, uid, opts) 20 | } 21 | 22 | const baseTokenURI = "https://rinkeby.loom.games/erc721/zmb/" 23 | 24 | Number.prototype.pad = function(size) { 25 | var s = String(this); 26 | while (s.length < (size || 2)) {s = "0" + s;} 27 | return s; 28 | } 29 | 30 | contract('Card', accounts => { 31 | let card 32 | const [ alice, bob, carlos ] = accounts; 33 | 34 | beforeEach(async () => { 35 | card = await Card.new(baseTokenURI) 36 | }); 37 | 38 | it('Should ZBGCard be deployed', async () => { 39 | card.address.should.not.be.null 40 | 41 | const name = await card.name.call() 42 | name.should.be.equal('Card') 43 | 44 | const symbol = await card.symbol.call() 45 | symbol.should.be.equal('CRD') 46 | }) 47 | 48 | it('Should get the correct supply when minting both NFTs and FTs', async () => { 49 | // Supply is the total amount of UNIQUE cards. 50 | for (let i = 0; i < 10; i+=2) { 51 | await card.mint(i, accounts[0], 2) 52 | await card.mint(i+1, accounts[0]) 53 | } 54 | const supply = await card.totalSupply.call() 55 | assert.equal(supply, 10) 56 | 57 | }) 58 | 59 | 60 | it('Should return correct token uri for multiple FT', async () => { 61 | for (let i = 0; i< 100; i++) { 62 | await card.mint(i, accounts[0], 2) 63 | const cardUri = await card.tokenURI.call(i) 64 | assert.equal(cardUri, `${baseTokenURI}${i}.json`) 65 | } 66 | }) 67 | 68 | it('Should return correct token uri for multiple NFT', async () => { 69 | for (let i = 0; i< 100; i++) { 70 | await card.mint(i, accounts[0]) 71 | const cardUri = await card.tokenURI.call(i) 72 | assert.equal(cardUri, `${baseTokenURI}${i}.json`) 73 | } 74 | }) 75 | 76 | it('Should return correct token uri for 6-digit NFT', async () => { 77 | const uid = 987145 78 | await card.mint(uid, accounts[0]) 79 | const cardUri = await card.tokenURI.call(uid) 80 | assert.equal(cardUri, `${baseTokenURI}${uid}.json`) 81 | }) 82 | 83 | it('Should be able to mint a fungible token', async () => { 84 | const uid = 0 85 | const amount = 5; 86 | await card.mint(uid, accounts[0], amount) 87 | 88 | const balanceOf1 = await card.balanceOf.call(accounts[0], uid) 89 | balanceOf1.should.be.eq.BN(new BN(5)) 90 | 91 | const balanceOf2 = await card.balanceOf.call(accounts[0]) 92 | balanceOf2.should.be.eq.BN(new BN(1)) 93 | 94 | await card.mint(uid, accounts[0], amount) 95 | const newBalanceOf1 = await card.balanceOf.call(accounts[0], uid) 96 | newBalanceOf1.should.be.eq.BN(new BN(10)) 97 | 98 | const newBalanceOf2 = await card.balanceOf.call(accounts[0]) 99 | newBalanceOf2.should.be.eq.BN(balanceOf2) 100 | }) 101 | 102 | it('Should be able to mint a non-fungible token', async () => { 103 | const uid = 0 104 | await card.mint(uid, accounts[0]) 105 | 106 | const balanceOf1 = await card.balanceOf.call(accounts[0], uid) 107 | balanceOf1.should.be.eq.BN(new BN(1)) 108 | 109 | const balanceOf2 = await card.balanceOf.call(accounts[0]) 110 | balanceOf2.should.be.eq.BN(new BN(1)) 111 | 112 | const ownerOf = await card.ownerOf.call(uid) 113 | ownerOf.should.be.eq.BN(accounts[0]) 114 | }) 115 | 116 | it('Should be impossible to mint NFT tokens with duplicate tokenId', async () => { 117 | const uid = 0; 118 | await card.mint(uid, alice); 119 | const supplyPostMint = await card.totalSupply() 120 | await expectThrow(card.mint(uid, alice)) 121 | const supplyPostSecondMint = await card.totalSupply() 122 | supplyPostMint.should.be.eq.BN(supplyPostSecondMint) 123 | }) 124 | 125 | it('Should be impossible to mint NFT tokens with the same tokenId as an existing FT tokenId', async () => { 126 | const uid = 0; 127 | await card.mint(uid, alice, 5); 128 | const supplyPostMint = await card.totalSupply() 129 | await expectThrow(card.mint(uid, alice)) 130 | const supplyPostSecondMint = await card.totalSupply() 131 | supplyPostMint.should.be.eq.BN(supplyPostSecondMint) 132 | }) 133 | 134 | it('Should be impossible to mint FT tokens with the same tokenId as an existing NFT tokenId', async () => { 135 | const uid = 0; 136 | await card.mint(uid, alice); 137 | const supplyPostMint = await card.totalSupply() 138 | await expectThrow(card.mint(uid, alice, 5)) 139 | const supplyPostSecondMint = await card.totalSupply() 140 | supplyPostMint.should.be.eq.BN(supplyPostSecondMint) 141 | }) 142 | 143 | it('Should be impossible to mint NFT tokens more than once even when owner is the contract itself', async () => { 144 | const uid = 0; 145 | await card.mint(uid, card.address); 146 | const supplyPostMint = await card.totalSupply() 147 | await expectThrow(card.mint(uid, card.address, 3)) 148 | const supplyPostSecondMint = await card.totalSupply() 149 | supplyPostMint.should.be.eq.BN(supplyPostSecondMint) 150 | }) 151 | 152 | it('a FT token should not have an owner', async () => { 153 | const uid = 0; 154 | await card.mint(uid, alice, 10); 155 | await expectThrow(card.ownerOf(uid)); 156 | }) 157 | 158 | it('Should be impossible for a FT token to be transferred with NFT transfer', async () => { 159 | const uid = 0; 160 | await card.mint(uid, alice, 10); 161 | await expectThrow(card.transferFrom(alice, bob, uid)); 162 | await expectThrow(card.ownerOf(uid)); 163 | }) 164 | 165 | it('Should be able to transfer a non fungible token', async () => { 166 | const uid = 0 167 | await card.mint(uid, alice) 168 | 169 | const balanceOf1 = await card.balanceOf.call(alice, uid) 170 | balanceOf1.should.be.eq.BN(new BN(1)) 171 | 172 | const balanceOf2 = await card.balanceOf.call(alice) 173 | balanceOf2.should.be.eq.BN(new BN(1)) 174 | 175 | const tx2 = await safeTransferFromNoDataNFT(card, alice, bob, uid, {from: alice}) 176 | 177 | const ownerOf2 = await card.ownerOf(uid); 178 | assert.equal(ownerOf2, bob) 179 | 180 | assertEventVar(tx2, 'Transfer', 'from', alice) 181 | assertEventVar(tx2, 'Transfer', 'to', bob) 182 | assertEventVar(tx2, 'Transfer', 'tokenId', uid) 183 | 184 | const balanceOf3 = await card.balanceOf.call(bob) 185 | balanceOf3.should.be.eq.BN(new BN(1)) 186 | }) 187 | 188 | it('Should Alice transfer a fungible token', async () => { 189 | const uid = 0 190 | const amount = 3 191 | await card.mint(uid, alice, amount) 192 | 193 | const aliceCardsBefore = await card.balanceOf(alice) 194 | assert.equal(aliceCardsBefore, 1) 195 | 196 | const bobCardsBefore = await card.balanceOf(bob) 197 | assert.equal(bobCardsBefore, 0) 198 | 199 | const tx = await safeTransferFromNoDataFT(card, alice, bob, uid, amount, {from: alice}) 200 | 201 | assertEventVar(tx, 'TransferWithQuantity', 'from', alice) 202 | assertEventVar(tx, 'TransferWithQuantity', 'to', bob) 203 | assertEventVar(tx, 'TransferWithQuantity', 'tokenId', uid) 204 | assertEventVar(tx, 'TransferWithQuantity', 'quantity', amount) 205 | 206 | const aliceCardsAfter = await card.balanceOf(alice) 207 | assert.equal(aliceCardsAfter, 0) 208 | const bobCardsAfter = await card.balanceOf(bob) 209 | assert.equal(bobCardsAfter, 1) 210 | }) 211 | 212 | it('Should Alice authorize transfer from Bob', async () => { 213 | const uid = 0; 214 | const amount = 5 215 | await card.mint(uid, alice, amount) 216 | let tx = await card.setApprovalForAll(bob, true, {from: alice}) 217 | 218 | assertEventVar(tx, 'ApprovalForAll', 'owner', alice) 219 | assertEventVar(tx, 'ApprovalForAll', 'operator', bob) 220 | assertEventVar(tx, 'ApprovalForAll', 'approved', true) 221 | 222 | tx = await safeTransferFromNoDataFT(card, alice, bob, uid, amount, {from: bob}) 223 | 224 | assertEventVar(tx, 'TransferWithQuantity', 'from', alice) 225 | assertEventVar(tx, 'TransferWithQuantity', 'to', bob) 226 | assertEventVar(tx, 'TransferWithQuantity', 'tokenId', uid) 227 | assertEventVar(tx, 'TransferWithQuantity', 'quantity', amount) 228 | }) 229 | 230 | it('Should Carlos not be authorized to spend', async () => { 231 | const uid = 0; 232 | const amount = 5 233 | let tx = await card.setApprovalForAll(bob, true, {from: alice}) 234 | 235 | assertEventVar(tx, 'ApprovalForAll', 'owner', alice) 236 | assertEventVar(tx, 'ApprovalForAll', 'operator', bob) 237 | assertEventVar(tx, 'ApprovalForAll', 'approved', true) 238 | 239 | await expectThrow(safeTransferFromNoDataFT(card, alice, bob, uid, amount, {from: carlos})) 240 | }) 241 | 242 | it('Should get the correct number of coins owned by a user', async () => { 243 | let numTokens = await card.totalSupply(); 244 | let balanceOf = await card.balanceOf(alice); 245 | balanceOf.should.be.eq.BN(new BN(0)); 246 | 247 | await card.mint(1000, alice, 100); 248 | let numTokens1 = await card.totalSupply(); 249 | 250 | numTokens1.should.be.eq.BN(numTokens.add(new BN(1))); 251 | 252 | await card.mint(11, bob, 5); 253 | let numTokens2 = await card.totalSupply(); 254 | numTokens2.should.be.eq.BN(numTokens1.add(new BN(1))); 255 | 256 | await card.mint(12, alice, 2); 257 | let numTokens3 = await card.totalSupply(); 258 | numTokens3.should.be.eq.BN(numTokens2.add(new BN(1))); 259 | 260 | await card.mint(13, alice); 261 | let numTokens4 = await card.totalSupply(); 262 | numTokens4.should.be.eq.BN(numTokens3.add(new BN(1))); 263 | balanceOf = await card.balanceOf(alice); 264 | balanceOf.should.be.eq.BN(new BN(3)); 265 | 266 | const tokensOwned = await card.tokensOwned(alice); 267 | const indexes = tokensOwned[0]; 268 | const balances = tokensOwned[1]; 269 | 270 | indexes[0].should.be.eq.BN(new BN(1000)); 271 | indexes[1].should.be.eq.BN(new BN(12)); 272 | indexes[2].should.be.eq.BN(new BN(13)); 273 | 274 | balances[0].should.be.eq.BN(new BN(100)); 275 | balances[1].should.be.eq.BN(new BN(2)); 276 | balances[2].should.be.eq.BN(new BN(1)); 277 | }); 278 | 279 | it('Should fail to mint quantity of coins larger than packed bin can represent', async () => { 280 | // each bin can only store numbers < 2^16 281 | await expectThrow(card.mint(0, alice, 150000)); 282 | }) 283 | 284 | it('Should update balances of sender and receiver and ownerOf for NFTs', async () => { 285 | // bins : -- 0 -- ---- 1 ---- ---- 2 ---- ---- 3 ---- 286 | let cards = []; //[0,1,2,3, 16,17,18,19, 32,33,34,35, 48,49,50,51]; 287 | let copies = []; //[0,1,2,3, 12,13,14,15, 11,12,13,14, 11,12,13,14]; 288 | 289 | let nCards = 100; 290 | 291 | //Minting enough copies for transfer for each cards 292 | for (let i = 300; i < nCards + 300; i++){ 293 | await card.mint(i, alice); 294 | cards.push(i); 295 | copies.push(1); 296 | } 297 | 298 | const tx = await card.batchTransferFrom(alice, bob, cards, copies, {from: alice}); 299 | 300 | let balanceFrom; 301 | let balanceTo; 302 | let ownerOf; 303 | 304 | for (let i = 0; i < cards.length; i++){ 305 | balanceFrom = await card.balanceOf(alice, cards[i]); 306 | balanceTo = await card.balanceOf(bob, cards[i]); 307 | ownerOf = await card.ownerOf(cards[i]); 308 | 309 | balanceFrom.should.be.eq.BN(0); 310 | balanceTo.should.be.eq.BN(1); 311 | assert.equal(ownerOf, bob); 312 | } 313 | 314 | assertEventVar(tx, 'BatchTransfer', 'from', alice) 315 | assertEventVar(tx, 'BatchTransfer', 'to', bob) 316 | }) 317 | 318 | it('Should update balances of sender and receiver', async () => { 319 | // bins : -- 0 -- ---- 1 ---- ---- 2 ---- ---- 3 ---- 320 | let cards = []; //[0,1,2,3, 16,17,18,19, 32,33,34,35, 48,49,50,51]; 321 | let copies = []; //[0,1,2,3, 12,13,14,15, 11,12,13,14, 11,12,13,14]; 322 | 323 | let nCards = 100; 324 | let nCopiesPerCard = 10; 325 | 326 | //Minting enough copies for transfer for each cards 327 | for (let i = 300; i < nCards + 300; i++){ 328 | await card.mint(i, alice, nCopiesPerCard); 329 | cards.push(i); 330 | copies.push(nCopiesPerCard); 331 | } 332 | 333 | const tx = await card.batchTransferFrom(alice, bob, cards, copies, {from: alice}); 334 | 335 | let balanceFrom; 336 | let balanceTo; 337 | 338 | for (let i = 0; i < cards.length; i++){ 339 | balanceFrom = await card.balanceOf(alice, cards[i]); 340 | balanceTo = await card.balanceOf(bob, cards[i]); 341 | 342 | balanceFrom.should.be.eq.BN(0); 343 | balanceTo.should.be.eq.BN(copies[i]); 344 | } 345 | 346 | assertEventVar(tx, 'BatchTransfer', 'from', alice) 347 | assertEventVar(tx, 'BatchTransfer', 'to', bob) 348 | }) 349 | }) 350 | -------------------------------------------------------------------------------- /test/constants.js: -------------------------------------------------------------------------------- 1 | const day = 60 * 60 * 24 * 1000; 2 | const dayInSecond = 60 * 60 * 24; 3 | const second = 1000; 4 | const gasPriceMax = 50000000000; 5 | 6 | module.exports = { 7 | day, 8 | dayInSecond, 9 | second, 10 | gasPriceMax 11 | } 12 | -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | const ethutil = require('ethereumjs-util') 2 | const { soliditySha3 } = require('web3-utils') 3 | 4 | const { 5 | dayInSecond 6 | } = require('./constants'); 7 | 8 | // helper functions ---------------------------------------------------------- 9 | const addsDayOnEVM = async (days) => { 10 | await web3.currentProvider.send({ 11 | jsonrpc: "2.0", 12 | method: "evm_increaseTime", 13 | params: [dayInSecond * days], 14 | id: 0 15 | }); 16 | 17 | await web3.currentProvider.send({ 18 | jsonrpc: "2.0", 19 | method: "evm_mine", 20 | params: [], 21 | id: 0 22 | }); 23 | } 24 | 25 | const expectThrow = async (promise) => { 26 | try { 27 | await promise; 28 | } catch (error) { 29 | const invalidOpcode = error.message.search('invalid opcode') >= 0; 30 | const invalidJump = error.message.search('invalid JUMP') >= 0; 31 | const outOfGas = error.message.search('out of gas') >= 0; 32 | const revert = error.message.search('revert') >= 0; 33 | 34 | assert( 35 | invalidOpcode || invalidJump || outOfGas || revert, 36 | "Expected throw, got '" + error + "' instead", 37 | ); 38 | return; 39 | } 40 | 41 | assert.fail('Expected throw not received'); 42 | }; 43 | 44 | const assertEventVar = (transaction, eventName, eventVar, equalVar) => { 45 | const event = transaction.logs.find(log => log.event === eventName); 46 | assert.equal(event.args[eventVar], equalVar, `Event ${event.args[eventVar]} didn't happen`); 47 | }; 48 | 49 | const getGUID = () => { 50 | function s4() { 51 | return Math.floor((1 + Math.random()) * 0x10000) 52 | .toString(16) 53 | .substring(1) 54 | } 55 | return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4() 56 | } 57 | 58 | const Promisify = (inner) => 59 | new Promise((resolve, reject) => 60 | inner((err, res) => { 61 | if (err) { 62 | reject(err) 63 | } else { 64 | resolve(res) 65 | } 66 | }) 67 | ) 68 | 69 | async function signHash(from, hash) { 70 | let sig = (await web3.eth.sign(hash, from)).slice(2) 71 | let r = ethutil.toBuffer('0x' + sig.substring(0, 64)) 72 | let s = ethutil.toBuffer('0x' + sig.substring(64, 128)) 73 | let v = ethutil.toBuffer(parseInt(sig.substring(128, 130), 16) + 27) 74 | let mode = ethutil.toBuffer(1) // mode = geth 75 | let signature = '0x' + Buffer.concat([mode, r, s, v]).toString('hex') 76 | return signature 77 | } 78 | 79 | /** 80 | * 81 | * @param Number serialNumber 82 | * @param Number seriesTotal 83 | * @param Number mouldId 84 | * @param Number cosmeticType 85 | */ 86 | const cardCreatorHelper = ( 87 | serialNumber, 88 | seriesTotal, 89 | mouldId, 90 | cosmeticType, 91 | ) => { 92 | const padLeft = (n, str) => { 93 | return (nr) => { 94 | return Array(n-String(nr).length+1).join(str||'0')+nr 95 | } 96 | } 97 | 98 | const padLefter = padLeft(4) 99 | 100 | const cardSkel = [ 101 | padLefter(serialNumber.toString(16)), 102 | padLefter(seriesTotal.toString(16)), 103 | padLefter(mouldId.toString(16)), 104 | padLefter(cosmeticType.toString(16)), 105 | ] 106 | return `0x${cardSkel.join('')}` 107 | } 108 | 109 | module.exports = { 110 | addsDayOnEVM, 111 | expectThrow, 112 | assertEventVar, 113 | getGUID, 114 | Promisify, 115 | signHash, 116 | cardCreatorHelper 117 | } 118 | -------------------------------------------------------------------------------- /truffle-config.js: -------------------------------------------------------------------------------- 1 | const HDWalletProvider = require('truffle-hdwallet-provider') 2 | const fs = require("fs") 3 | const { join } = require('path') 4 | 5 | const mochaGasSettings = { 6 | reporter: 'eth-gas-reporter', 7 | reporterOptions: { 8 | currency: 'USD', 9 | gasPrice: 3 10 | } 11 | } 12 | 13 | // First read in the secrets.json to get our mnemonic 14 | let secrets 15 | let mnemonic 16 | 17 | let filename = process.env.SECRET_FILE 18 | if (filename == "") { 19 | filename = "secrets.json" 20 | } 21 | if(fs.existsSync(filename)) { 22 | secrets = JSON.parse(fs.readFileSync(filename, "utf8")) 23 | mnemonic = secrets.mnemonic 24 | } else { 25 | console.log("No secrets.json found. If you are trying to publish EPM " + 26 | "this will fail. Otherwise, you can ignore this message!") 27 | mnemonic = "" 28 | } 29 | 30 | const mocha = process.env.GAS_REPORTER ? mochaGasSettings : {} 31 | 32 | 33 | module.exports = { 34 | networks: { 35 | development: { 36 | host: '127.0.0.1', 37 | port: 7545, 38 | network_id: '*', 39 | gasPrice: 1 40 | }, 41 | ganache: { 42 | host: '127.0.0.1', 43 | port: 8545, 44 | network_id: '*', 45 | gasPrice: 1 46 | }, 47 | rinkeby: { 48 | network_id: 4, 49 | provider: function() { 50 | return new HDWalletProvider(mnemonic, 'https://rinkeby.infura.io/', 0, 10) 51 | }, 52 | gasPrice: 15000000001, 53 | skipDryRun: true 54 | }, 55 | mainnet: { 56 | network_id: 1, 57 | provider: function() { 58 | return new HDWalletProvider(mnemonic, 'https://mainnet.infura.io/', 0, 10) 59 | }, 60 | gasPrice: 15000000001, 61 | skipDryRun: true 62 | } 63 | }, 64 | mocha, 65 | solc: { 66 | optimizer: { 67 | enabled: true, 68 | runs: 200 69 | } 70 | }, 71 | compilers : { 72 | solc: { 73 | version: "0.5.7" 74 | } 75 | } 76 | } 77 | --------------------------------------------------------------------------------