├── .gitignore ├── .nvmrc ├── LICENSE ├── README.md ├── contracts ├── ERC1155Tradable.sol ├── IFactory.sol ├── ILootBox.sol ├── Migrations.sol ├── MyCollectible.sol ├── MyFactory.sol ├── MyLootBox.sol ├── SafeMath.sol ├── Strings.sol └── test │ ├── MockProxyRegistry.sol │ └── TestForReentrancyAttack.sol ├── flatten.sh ├── lib └── testValuesCommon.js ├── metadata-api ├── .gitignore ├── Procfile ├── app.py ├── images │ ├── bases │ │ ├── base-crab.png │ │ ├── base-goldfish.png │ │ ├── base-jellyfish.png │ │ ├── base-narwhal.png │ │ ├── base-starfish.png │ │ └── base-tealfish.png │ ├── box │ │ ├── lootbox.png │ │ └── multiple-eggs.png │ ├── eyes │ │ ├── eyes-big.png │ │ ├── eyes-content.png │ │ ├── eyes-joy.png │ │ ├── eyes-sleepy.png │ │ └── eyes-wink.png │ ├── factory │ │ ├── egg.png │ │ └── four-eggs.png │ ├── mouths │ │ ├── mouth-cute.png │ │ ├── mouth-happy.png │ │ ├── mouth-pleased.png │ │ ├── mouth-smirk.png │ │ └── mouth-surprised.png │ └── output │ │ ├── 1.png │ │ ├── 3.png │ │ ├── 7.png │ │ └── temp.txt ├── requirements.txt └── src │ └── pip-delete-this-directory.txt ├── migrations ├── 1_initial_migration.js └── 2_deploy_contracts.js ├── package.json ├── scripts ├── advanced │ └── mint.js ├── initial_sale.js └── sell.js ├── test ├── ERC1155Tradable.js ├── MyCollectible.js ├── MyFactory.js └── MyLootBox.js └── truffle-config.js /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | flattened/ 3 | node_modules/ 4 | .env* 5 | *.DS_Store 6 | *-error.log 7 | .idea/ 8 | yarn.lock 9 | package-lock.json 10 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 8.11.2 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 OpenSea 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## OpenSea ERC-1155 Starter Contracts 2 | 3 | - [About these contracts](#about-these-contracts) 4 | - [Configuring the Lootbox](#configuring-the-lootbox) 5 | - [Why are some standard methods overridden?](#why-are-some-standard-methods-overridden) 6 | - [Requirements](#requirements) 7 | - [Node version](#node-version) 8 | - [Installation](#installation) 9 | - [Deploying](#deploying) 10 | - [Deploying to the Rinkeby network.](#deploying-to-the-rinkeby-network) 11 | - [Deploying to the mainnet Ethereum network.](#deploying-to-the-mainnet-ethereum-network) 12 | - [Viewing your items on OpenSea](#viewing-your-items-on-opensea) 13 | - [Troubleshooting](#troubleshooting) 14 | - [It doesn't compile!](#it-doesnt-compile) 15 | - [It doesn't deploy anything!](#it-doesnt-deploy-anything) 16 | - [Minting tokens.](#minting-tokens) 17 | - [License](#license) 18 | - [ERC1155 Implementation](#erc1155-implementation) 19 | 20 | # About these contracts 21 | 22 | This is a sample ERC-1155 contract for the purposes of demonstrating integration with the [OpenSea](https://opensea.io) marketplace for crypto collectibles. We also include: 23 | - A script for minting items. 24 | - A factory contract for making sell orders for unminted items (allowing for **gas-free and mint-free presales**). 25 | - A configurable lootbox contract for selling randomized collections of ERC-1155 items. 26 | 27 | On top of the features from the [OpenSea ERC721 sample contracts](https://github.com/ProjectOpenSea/opensea-creatures), ERC1155 28 | - supports multiple creators per contract, where only the creator is able to mint more copies 29 | - supports pre-minted items for the lootbox to choose from 30 | 31 | ## Configuring the Lootbox 32 | 33 | Open MyLootbox.sol 34 | 35 | 1. Change `Class` to reflect your rarity levels. 36 | 2. Change `NUM_CLASSES` to reflect how many classes you have (this gets used for sizing fixed-length arrays in Solidity) 37 | 3. In `constructor`, set the `OptionSettings` for each of your classes. To do this, as in the example, call `setOptionSettings` with 38 | 1. Your option id, 39 | 2. The number of items to issue when the box is opened, 40 | 3. An array of probabilities (basis points, so integers out of 10,000) of receiving each class. Should add up to 10k and be descending in value. 41 | 4. Then follow the instructions below to deploy it! Purchases will auto-open the box. If you'd like to make lootboxes tradable by users (without a purchase auto-opening it), contact us at contact@opensea.io (or better yet, in [Discord](https://discord.gg/ga8EJbv)). 42 | 43 | ## Why are some standard methods overridden? 44 | 45 | This contract overrides the `isApprovedForAll` method in order to whitelist the proxy accounts of OpenSea users. This means that they are automatically able to trade your ERC-1155 items on OpenSea (without having to pay gas for an additional approval). On OpenSea, each user has a "proxy" account that they control, and is ultimately called by the exchange contracts to trade their items. 46 | 47 | Note that this addition does not mean that OpenSea itself has access to the items, simply that the users can list them more easily if they wish to do so! 48 | 49 | # Requirements 50 | 51 | ### Node version 52 | 53 | Either make sure you're running a version of node compliant with the `engines` requirement in `package.json`, or install Node Version Manager [`nvm`](https://github.com/creationix/nvm) and run `nvm use` to use the correct version of node. 54 | 55 | ## Installation 56 | 57 | Run 58 | ```bash 59 | yarn 60 | ``` 61 | 62 | ## Deploying 63 | 64 | ### Deploying to the Rinkeby network. 65 | 66 | 1. You'll need to sign up for [Infura](https://infura.io). and get an API key. 67 | 2. You'll need Rinkeby ether to pay for the gas to deploy your contract. Visit https://faucet.rinkeby.io/ to get some. 68 | 3. Using your API key and the mnemonic for your MetaMask wallet (make sure you're using a MetaMask seed phrase that you're comfortable using for testing purposes), run: 69 | 70 | ``` 71 | export INFURA_KEY="" 72 | export MNEMONIC="" 73 | truffle migrate --network rinkeby 74 | ``` 75 | 76 | ### Deploying to the mainnet Ethereum network. 77 | 78 | Make sure your wallet has at least a few dollars worth of ETH in it. Then run: 79 | 80 | ``` 81 | yarn truffle migrate --network live 82 | ``` 83 | 84 | Look for your newly deployed contract address in the logs! 🥳 85 | 86 | ### Viewing your items on OpenSea 87 | 88 | OpenSea will automatically pick up transfers on your contract. You can visit an asset by going to `https://opensea.io/assets/CONTRACT_ADDRESS/TOKEN_ID`. 89 | 90 | To load all your metadata on your items at once, visit [https://opensea.io/get-listed](https://opensea.io/get-listed) and enter your address to load the metadata into OpenSea! You can even do this for the Rinkeby test network if you deployed there, by going to [https://rinkeby.opensea.io/get-listed](https://rinkeby.opensea.io/get-listed). 91 | 92 | ### Troubleshooting 93 | 94 | #### It doesn't compile! 95 | Install truffle locally: `yarn add truffle`. Then run `yarn truffle migrate ...`. 96 | 97 | You can also debug just the compile step by running `yarn truffle compile`. 98 | 99 | #### It doesn't deploy anything! 100 | This is often due to the truffle-hdwallet provider not being able to connect. Go to infura.io and create a new Infura project. Use your "project ID" as your new `INFURA_KEY` and make sure you export that command-line variable above. 101 | 102 | ### Minting tokens. 103 | 104 | After deploying to the Rinkeby network, there will be a contract on Rinkeby that will be viewable on [Rinkeby Etherscan](https://rinkeby.etherscan.io). For example, here is a [recently deployed contract](https://rinkeby.etherscan.io/address/0xeba05c5521a3b81e23d15ae9b2d07524bc453561). You should set this contract address and the address of your Metamask account as environment variables when running the minting script: 105 | 106 | ``` 107 | export OWNER_ADDRESS="" 108 | export FACTORY_CONTRACT_ADDRESS="" 109 | export NETWORK="rinkeby" 110 | node scripts/advanced/mint.js 111 | ``` 112 | 113 | Note: When running the minting script on mainnet, your environment variable needs to be set to `mainnet` not `live`. The environment variable affects the Infura URL in the minting script, not truffle. When you deploy, you're using truffle and you need to give truffle an argument that corresponds to the naming in truffle.js (`--network live`). But when you mint, you're relying on the environment variable you set to build the URL (https://github.com/ProjectOpenSea/opensea-creatures/blob/master/scripts/mint.js#L54), so you need to use the term that makes Infura happy (`mainnet`). Truffle and Infura use the same terminology for Rinkeby, but different terminology for mainnet. If you start your minting script, but nothing happens, double check your environment variables. 114 | 115 | # License 116 | 117 | These contracts are available to the public under an MIT License. 118 | 119 | ### ERC1155 Implementation 120 | 121 | To implement the ERC1155 standard, these contracts use the Multi Token Standard by [Horizon Games](https://horizongames.net/), available on [npm](https://www.npmjs.com/package/multi-token-standard) and [github](https://github.com/arcadeum/multi-token-standard) and also under the MIT License. 122 | -------------------------------------------------------------------------------- /contracts/ERC1155Tradable.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.12; 2 | 3 | import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; 4 | import 'multi-token-standard/contracts/tokens/ERC1155/ERC1155.sol'; 5 | import 'multi-token-standard/contracts/tokens/ERC1155/ERC1155Metadata.sol'; 6 | import 'multi-token-standard/contracts/tokens/ERC1155/ERC1155MintBurn.sol'; 7 | import "./Strings.sol"; 8 | 9 | contract OwnableDelegateProxy { } 10 | 11 | contract ProxyRegistry { 12 | mapping(address => OwnableDelegateProxy) public proxies; 13 | } 14 | 15 | /** 16 | * @title ERC1155Tradable 17 | * ERC1155Tradable - ERC1155 contract that whitelists an operator address, has create and mint functionality, and supports useful standards from OpenZeppelin, 18 | like _exists(), name(), symbol(), and totalSupply() 19 | */ 20 | contract ERC1155Tradable is ERC1155, ERC1155MintBurn, ERC1155Metadata, Ownable { 21 | using Strings for string; 22 | 23 | address proxyRegistryAddress; 24 | uint256 private _currentTokenID = 0; 25 | mapping (uint256 => address) public creators; 26 | mapping (uint256 => uint256) public tokenSupply; 27 | // Contract name 28 | string public name; 29 | // Contract symbol 30 | string public symbol; 31 | 32 | /** 33 | * @dev Require msg.sender to be the creator of the token id 34 | */ 35 | modifier creatorOnly(uint256 _id) { 36 | require(creators[_id] == msg.sender, "ERC1155Tradable#creatorOnly: ONLY_CREATOR_ALLOWED"); 37 | _; 38 | } 39 | 40 | /** 41 | * @dev Require msg.sender to own more than 0 of the token id 42 | */ 43 | modifier ownersOnly(uint256 _id) { 44 | require(balances[msg.sender][_id] > 0, "ERC1155Tradable#ownersOnly: ONLY_OWNERS_ALLOWED"); 45 | _; 46 | } 47 | 48 | constructor( 49 | string memory _name, 50 | string memory _symbol, 51 | address _proxyRegistryAddress 52 | ) public { 53 | name = _name; 54 | symbol = _symbol; 55 | proxyRegistryAddress = _proxyRegistryAddress; 56 | } 57 | 58 | function uri( 59 | uint256 _id 60 | ) public view returns (string memory) { 61 | require(_exists(_id), "ERC721Tradable#uri: NONEXISTENT_TOKEN"); 62 | return Strings.strConcat( 63 | baseMetadataURI, 64 | Strings.uint2str(_id) 65 | ); 66 | } 67 | 68 | /** 69 | * @dev Returns the total quantity for a token ID 70 | * @param _id uint256 ID of the token to query 71 | * @return amount of token in existence 72 | */ 73 | function totalSupply( 74 | uint256 _id 75 | ) public view returns (uint256) { 76 | return tokenSupply[_id]; 77 | } 78 | 79 | /** 80 | * @dev Will update the base URL of token's URI 81 | * @param _newBaseMetadataURI New base URL of token's URI 82 | */ 83 | function setBaseMetadataURI( 84 | string memory _newBaseMetadataURI 85 | ) public onlyOwner { 86 | _setBaseMetadataURI(_newBaseMetadataURI); 87 | } 88 | 89 | /** 90 | * @dev Creates a new token type and assigns _initialSupply to an address 91 | * NOTE: remove onlyOwner if you want third parties to create new tokens on your contract (which may change your IDs) 92 | * @param _initialOwner address of the first owner of the token 93 | * @param _initialSupply amount to supply the first owner 94 | * @param _uri Optional URI for this token type 95 | * @param _data Data to pass if receiver is contract 96 | * @return The newly created token ID 97 | */ 98 | function create( 99 | address _initialOwner, 100 | uint256 _initialSupply, 101 | string calldata _uri, 102 | bytes calldata _data 103 | ) external onlyOwner returns (uint256) { 104 | 105 | uint256 _id = _getNextTokenID(); 106 | _incrementTokenTypeId(); 107 | creators[_id] = msg.sender; 108 | 109 | if (bytes(_uri).length > 0) { 110 | emit URI(_uri, _id); 111 | } 112 | 113 | _mint(_initialOwner, _id, _initialSupply, _data); 114 | tokenSupply[_id] = _initialSupply; 115 | return _id; 116 | } 117 | 118 | /** 119 | * @dev Mints some amount of tokens to an address 120 | * @param _to Address of the future owner of the token 121 | * @param _id Token ID to mint 122 | * @param _quantity Amount of tokens to mint 123 | * @param _data Data to pass if receiver is contract 124 | */ 125 | function mint( 126 | address _to, 127 | uint256 _id, 128 | uint256 _quantity, 129 | bytes memory _data 130 | ) public creatorOnly(_id) { 131 | _mint(_to, _id, _quantity, _data); 132 | tokenSupply[_id] = tokenSupply[_id].add(_quantity); 133 | } 134 | 135 | /** 136 | * @dev Mint tokens for each id in _ids 137 | * @param _to The address to mint tokens to 138 | * @param _ids Array of ids to mint 139 | * @param _quantities Array of amounts of tokens to mint per id 140 | * @param _data Data to pass if receiver is contract 141 | */ 142 | function batchMint( 143 | address _to, 144 | uint256[] memory _ids, 145 | uint256[] memory _quantities, 146 | bytes memory _data 147 | ) public { 148 | for (uint256 i = 0; i < _ids.length; i++) { 149 | uint256 _id = _ids[i]; 150 | require(creators[_id] == msg.sender, "ERC1155Tradable#batchMint: ONLY_CREATOR_ALLOWED"); 151 | uint256 quantity = _quantities[i]; 152 | tokenSupply[_id] = tokenSupply[_id].add(quantity); 153 | } 154 | _batchMint(_to, _ids, _quantities, _data); 155 | } 156 | 157 | /** 158 | * @dev Change the creator address for given tokens 159 | * @param _to Address of the new creator 160 | * @param _ids Array of Token IDs to change creator 161 | */ 162 | function setCreator( 163 | address _to, 164 | uint256[] memory _ids 165 | ) public { 166 | require(_to != address(0), "ERC1155Tradable#setCreator: INVALID_ADDRESS."); 167 | for (uint256 i = 0; i < _ids.length; i++) { 168 | uint256 id = _ids[i]; 169 | _setCreator(_to, id); 170 | } 171 | } 172 | 173 | /** 174 | * Override isApprovedForAll to whitelist user's OpenSea proxy accounts to enable gas-free listings. 175 | */ 176 | function isApprovedForAll( 177 | address _owner, 178 | address _operator 179 | ) public view returns (bool isOperator) { 180 | // Whitelist OpenSea proxy contract for easy trading. 181 | ProxyRegistry proxyRegistry = ProxyRegistry(proxyRegistryAddress); 182 | if (address(proxyRegistry.proxies(_owner)) == _operator) { 183 | return true; 184 | } 185 | 186 | return ERC1155.isApprovedForAll(_owner, _operator); 187 | } 188 | 189 | /** 190 | * @dev Change the creator address for given token 191 | * @param _to Address of the new creator 192 | * @param _id Token IDs to change creator of 193 | */ 194 | function _setCreator(address _to, uint256 _id) internal creatorOnly(_id) 195 | { 196 | creators[_id] = _to; 197 | } 198 | 199 | /** 200 | * @dev Returns whether the specified token exists by checking to see if it has a creator 201 | * @param _id uint256 ID of the token to query the existence of 202 | * @return bool whether the token exists 203 | */ 204 | function _exists( 205 | uint256 _id 206 | ) internal view returns (bool) { 207 | return creators[_id] != address(0); 208 | } 209 | 210 | /** 211 | * @dev calculates the next token ID based on value of _currentTokenID 212 | * @return uint256 for the next token ID 213 | */ 214 | function _getNextTokenID() private view returns (uint256) { 215 | return _currentTokenID.add(1); 216 | } 217 | 218 | /** 219 | * @dev increments the value of _currentTokenID 220 | */ 221 | function _incrementTokenTypeId() private { 222 | _currentTokenID++; 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /contracts/IFactory.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.12; 2 | 3 | /** 4 | * This is a generic factory contract that can be used to mint tokens. The configuration 5 | * for minting is specified by an _optionId, which can be used to delineate various 6 | * ways of minting. 7 | */ 8 | interface IFactory { 9 | /** 10 | * Returns the name of this factory. 11 | */ 12 | function name() external view returns (string memory); 13 | 14 | /** 15 | * Returns the symbol for this factory. 16 | */ 17 | function symbol() external view returns (string memory); 18 | 19 | /** 20 | * Number of options the factory supports. 21 | */ 22 | function numOptions() external view returns (uint256); 23 | 24 | /** 25 | * @dev Returns whether the option ID can be minted. Can return false if the developer wishes to 26 | * restrict a total supply per option ID (or overall). 27 | */ 28 | function canMint(uint256 _optionId, uint256 _amount) external view returns (bool); 29 | 30 | /** 31 | * @dev Returns a URL specifying some metadata about the option. This metadata can be of the 32 | * same structure as the ERC1155 metadata. 33 | */ 34 | function uri(uint256 _optionId) external view returns (string memory); 35 | 36 | /** 37 | * Indicates that this is a factory contract. Ideally would use EIP 165 supportsInterface() 38 | */ 39 | function supportsFactoryInterface() external view returns (bool); 40 | 41 | /** 42 | * Indicates the Wyvern schema name for assets in this lootbox, e.g. "ERC1155" 43 | */ 44 | function factorySchemaName() external view returns (string memory); 45 | 46 | /** 47 | * @dev Mints asset(s) in accordance to a specific address with a particular "option". This should be 48 | * callable only by the contract owner or the owner's Wyvern Proxy (later universal login will solve this). 49 | * Options should also be delineated 0 - (numOptions() - 1) for convenient indexing. 50 | * @param _optionId the option id 51 | * @param _toAddress address of the future owner of the asset(s) 52 | * @param _amount amount of the option to mint 53 | * @param _data Extra data to pass during safeTransferFrom 54 | */ 55 | function mint(uint256 _optionId, address _toAddress, uint256 _amount, bytes calldata _data) external; 56 | 57 | /////// 58 | // Get things to work on OpenSea with mock methods below 59 | /////// 60 | 61 | function safeTransferFrom(address _from, address _to, uint256 _optionId, uint256 _amount, bytes calldata _data) external; 62 | 63 | function balanceOf(address _owner, uint256 _optionId) external view returns (uint256); 64 | 65 | function isApprovedForAll(address _owner, address _operator) external view returns (bool); 66 | } -------------------------------------------------------------------------------- /contracts/ILootBox.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.12; 2 | 3 | /** 4 | * This is a generic lootbox contract that can be used to mint or sendrandom tokens. The configuration 5 | * of the contract is detailed in MyLootBox.sol 6 | */ 7 | interface ILootBox { 8 | 9 | /** 10 | * Returns the name of this factory. 11 | */ 12 | function name() external view returns (string memory); 13 | 14 | /** 15 | * Returns the symbol for this factory. 16 | */ 17 | function symbol() external view returns (string memory); 18 | 19 | /** 20 | * Number of options the factory supports. 21 | */ 22 | function numOptions() external view returns (uint256); 23 | 24 | /** 25 | * @dev Returns whether the option ID can be minted. Can return false if the developer wishes to 26 | * restrict a total supply per option ID (or overall). 27 | */ 28 | function canMint(uint256 _optionId, uint256 _amount) external view returns (bool); 29 | 30 | /** 31 | * @dev Returns a URL specifying some metadata about the option. This metadata can be of the 32 | * same structure as the ERC1155 metadata. 33 | */ 34 | function uri(uint256 _optionId) external view returns (string memory); 35 | 36 | /** 37 | * Indicates that this is a factory contract. Ideally would use EIP 165 supportsInterface() 38 | */ 39 | function supportsFactoryInterface() external view returns (bool); 40 | 41 | /** 42 | * Indicates the Wyvern schema name for assets in this lootbox, e.g. "ERC1155" 43 | */ 44 | function factorySchemaName() external view returns (string memory); 45 | 46 | /** 47 | * @dev Mints or sends asset(s) in accordance to a specific address with a particular "option". This should be 48 | * callable only by the contract owner or the owner's Wyvern Proxy (later universal login will solve this). 49 | * Options should also be delineated 0 - (numOptions() - 1) for convenient indexing. 50 | * @param _optionId the option id 51 | * @param _toAddress address of the future owner of the asset(s) 52 | * @param _amount amount of the option to mint 53 | */ 54 | function open(uint256 _optionId, address _toAddress, uint256 _amount) external; 55 | 56 | //////// 57 | // ADMINISTRATION 58 | //////// 59 | 60 | /** 61 | * @dev If the tokens for some class are pre-minted and owned by the 62 | * contract owner, they can be used for a given class by setting them here 63 | */ 64 | function setClassForTokenId(uint256 _tokenId, uint256 _classId) external; 65 | 66 | /** 67 | * @dev Remove all token ids for a given class, causing it to fall back to 68 | * creating/minting into the nft address 69 | */ 70 | function resetClass(uint256 _classId) external; 71 | 72 | /** 73 | * @dev Withdraw lootbox revenue 74 | * Only accessible by contract owner 75 | */ 76 | function withdraw() external; 77 | 78 | /////// 79 | // Get things to work on OpenSea with mock methods below 80 | /////// 81 | 82 | function safeTransferFrom(address _from, address _to, uint256 _optionId, uint256 _amount, bytes calldata _data) external; 83 | 84 | function balanceOf(address _owner, uint256 _optionId) external view returns (uint256); 85 | 86 | function isApprovedForAll(address _owner, address _operator) external view returns (bool); 87 | } -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.4.21 <0.6.0; 2 | 3 | contract Migrations { 4 | address public owner; 5 | uint public last_completed_migration; 6 | 7 | constructor() public { 8 | owner = msg.sender; 9 | } 10 | 11 | modifier restricted() { 12 | if (msg.sender == owner) _; 13 | } 14 | 15 | function setCompleted(uint completed) public restricted { 16 | last_completed_migration = completed; 17 | } 18 | 19 | function upgrade(address new_address) public restricted { 20 | Migrations upgraded = Migrations(new_address); 21 | upgraded.setCompleted(last_completed_migration); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /contracts/MyCollectible.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.11; 2 | 3 | import "./ERC1155Tradable.sol"; 4 | 5 | /** 6 | * @title MyCollectible 7 | * MyCollectible - a contract for my semi-fungible tokens. 8 | */ 9 | contract MyCollectible is ERC1155Tradable { 10 | constructor(address _proxyRegistryAddress) 11 | ERC1155Tradable( 12 | "MyCollectible", 13 | "MCB", 14 | _proxyRegistryAddress 15 | ) public { 16 | _setBaseMetadataURI("https://creatures-api.opensea.io/api/creature/"); 17 | } 18 | 19 | function contractURI() public view returns (string memory) { 20 | return "https://creatures-api.opensea.io/contract/opensea-erc1155"; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /contracts/MyFactory.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.11; 2 | 3 | import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; 4 | import "openzeppelin-solidity/contracts/utils/ReentrancyGuard.sol"; 5 | import "./IFactory.sol"; 6 | import "./MyCollectible.sol"; 7 | import "./Strings.sol"; 8 | 9 | // WIP 10 | contract MyFactory is IFactory, Ownable, ReentrancyGuard { 11 | using Strings for string; 12 | using SafeMath for uint256; 13 | 14 | address public proxyRegistryAddress; 15 | address public nftAddress; 16 | string constant internal baseMetadataURI = "https://opensea-creatures-api.herokuapp.com/api/"; 17 | uint256 constant UINT256_MAX = ~uint256(0); 18 | 19 | /** 20 | * Optionally set this to a small integer to enforce limited existence per option/token ID 21 | * (Otherwise rely on sell orders on OpenSea, which can only be made by the factory owner.) 22 | */ 23 | uint256 constant SUPPLY_PER_TOKEN_ID = UINT256_MAX; 24 | 25 | /** 26 | * Three different options for minting MyCollectibles (basic, premium, and gold). 27 | */ 28 | enum Option { 29 | Basic, 30 | Premium, 31 | Gold 32 | } 33 | uint256 constant NUM_OPTIONS = 3; 34 | mapping (uint256 => uint256) public optionToTokenID; 35 | 36 | constructor(address _proxyRegistryAddress, address _nftAddress) public { 37 | proxyRegistryAddress = _proxyRegistryAddress; 38 | nftAddress = _nftAddress; 39 | } 40 | 41 | ///// 42 | // IFACTORY METHODS 43 | ///// 44 | 45 | function name() external view returns (string memory) { 46 | return "My Collectible Pre-Sale"; 47 | } 48 | 49 | function symbol() external view returns (string memory) { 50 | return "MCP"; 51 | } 52 | 53 | function supportsFactoryInterface() external view returns (bool) { 54 | return true; 55 | } 56 | 57 | function factorySchemaName() external view returns (string memory) { 58 | return "ERC1155"; 59 | } 60 | 61 | function numOptions() external view returns (uint256) { 62 | return NUM_OPTIONS; 63 | } 64 | 65 | function canMint(uint256 _optionId, uint256 _amount) external view returns (bool) { 66 | return _canMint(msg.sender, Option(_optionId), _amount); 67 | } 68 | 69 | function mint(uint256 _optionId, address _toAddress, uint256 _amount, bytes calldata _data) external nonReentrant() { 70 | return _mint(Option(_optionId), _toAddress, _amount, _data); 71 | } 72 | 73 | function uri(uint256 _optionId) external view returns (string memory) { 74 | return Strings.strConcat( 75 | baseMetadataURI, 76 | "factory/", 77 | Strings.uint2str(_optionId) 78 | ); 79 | } 80 | 81 | /** 82 | * @dev Main minting logic implemented here! 83 | */ 84 | function _mint( 85 | Option _option, 86 | address _toAddress, 87 | uint256 _amount, 88 | bytes memory _data 89 | ) internal { 90 | require(_canMint(msg.sender, _option, _amount), "MyFactory#_mint: CANNOT_MINT_MORE"); 91 | uint256 optionId = uint256(_option); 92 | MyCollectible nftContract = MyCollectible(nftAddress); 93 | uint256 id = optionToTokenID[optionId]; 94 | if (id == 0) { 95 | id = nftContract.create(_toAddress, _amount, "", _data); 96 | optionToTokenID[optionId] = id; 97 | } else { 98 | nftContract.mint(_toAddress, id, _amount, _data); 99 | } 100 | } 101 | 102 | /** 103 | * Get the factory's ownership of Option. 104 | * Should be the amount it can still mint. 105 | * NOTE: Called by `canMint` 106 | */ 107 | function balanceOf( 108 | address _owner, 109 | uint256 _optionId 110 | ) public view returns (uint256) { 111 | if (!_isOwnerOrProxy(_owner)) { 112 | // Only the factory owner or owner's proxy can have supply 113 | return 0; 114 | } 115 | uint256 id = optionToTokenID[_optionId]; 116 | if (id == 0) { 117 | // Haven't minted yet 118 | return SUPPLY_PER_TOKEN_ID; 119 | } 120 | 121 | MyCollectible nftContract = MyCollectible(nftAddress); 122 | uint256 currentSupply = nftContract.totalSupply(id); 123 | return SUPPLY_PER_TOKEN_ID.sub(currentSupply); 124 | } 125 | 126 | /** 127 | * Hack to get things to work automatically on OpenSea. 128 | * Use safeTransferFrom so the frontend doesn't have to worry about different method names. 129 | */ 130 | function safeTransferFrom( 131 | address /* _from */, 132 | address _to, 133 | uint256 _optionId, 134 | uint256 _amount, 135 | bytes calldata _data 136 | ) external { 137 | _mint(Option(_optionId), _to, _amount, _data); 138 | } 139 | 140 | ////// 141 | // Below methods shouldn't need to be overridden or modified 142 | ////// 143 | 144 | function isApprovedForAll( 145 | address _owner, 146 | address _operator 147 | ) public view returns (bool) { 148 | return owner() == _owner && _isOwnerOrProxy(_operator); 149 | } 150 | 151 | function _canMint( 152 | address _fromAddress, 153 | Option _option, 154 | uint256 _amount 155 | ) internal view returns (bool) { 156 | uint256 optionId = uint256(_option); 157 | return _amount > 0 && balanceOf(_fromAddress, optionId) >= _amount; 158 | } 159 | 160 | function _isOwnerOrProxy( 161 | address _address 162 | ) internal view returns (bool) { 163 | ProxyRegistry proxyRegistry = ProxyRegistry(proxyRegistryAddress); 164 | return owner() == _address || address(proxyRegistry.proxies(owner())) == _address; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /contracts/MyLootBox.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.11; 2 | 3 | import "openzeppelin-solidity/contracts/utils/ReentrancyGuard.sol"; 4 | import "openzeppelin-solidity/contracts/lifecycle/Pausable.sol"; 5 | import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; 6 | import "./MyCollectible.sol"; 7 | import "./MyFactory.sol"; 8 | import "./ILootBox.sol"; 9 | 10 | /** 11 | * @title MyLootBox 12 | * MyLootBox - a randomized and openable lootbox of MyCollectibles 13 | */ 14 | contract MyLootBox is ILootBox, Ownable, Pausable, ReentrancyGuard, MyFactory { 15 | using SafeMath for uint256; 16 | 17 | // Event for logging lootbox opens 18 | event LootBoxOpened(uint256 indexed optionId, address indexed buyer, uint256 boxesPurchased, uint256 itemsMinted); 19 | event Warning(string message, address account); 20 | 21 | // Must be sorted by rarity 22 | enum Class { 23 | Common, 24 | Rare, 25 | Epic, 26 | Legendary, 27 | Divine, 28 | Hidden 29 | } 30 | uint256 constant NUM_CLASSES = 6; 31 | 32 | // NOTE: Price of the lootbox is set via sell orders on OpenSea 33 | struct OptionSettings { 34 | // Number of items to send per open. 35 | // Set to 0 to disable this Option. 36 | uint256 maxQuantityPerOpen; 37 | // Probability in basis points (out of 10,000) of receiving each class (descending) 38 | uint16[NUM_CLASSES] classProbabilities; 39 | // Whether to enable `guarantees` below 40 | bool hasGuaranteedClasses; 41 | // Number of items you're guaranteed to get, for each class 42 | uint16[NUM_CLASSES] guarantees; 43 | } 44 | mapping (uint256 => OptionSettings) public optionToSettings; 45 | mapping (uint256 => uint256[]) public classToTokenIds; 46 | mapping (uint256 => bool) public classIsPreminted; 47 | uint256 seed; 48 | uint256 constant INVERSE_BASIS_POINT = 10000; 49 | 50 | /** 51 | * @dev Example constructor. Calls setOptionSettings for you with 52 | * sample settings 53 | * @param _proxyRegistryAddress The address of the OpenSea/Wyvern proxy registry 54 | * On Rinkeby: "0xf57b2c51ded3a29e6891aba85459d600256cf317" 55 | * On mainnet: "0xa5409ec958c83c3f309868babaca7c86dcb077c1" 56 | * @param _nftAddress The address of the non-fungible/semi-fungible item contract 57 | * that you want to mint/transfer with each open 58 | */ 59 | constructor( 60 | address _proxyRegistryAddress, 61 | address _nftAddress 62 | ) MyFactory( 63 | _proxyRegistryAddress, 64 | _nftAddress 65 | ) public { 66 | // Example settings and probabilities 67 | // you can also call these after deploying 68 | uint16[NUM_CLASSES] memory guarantees; 69 | setOptionSettings(Option.Basic, 3, [7300, 2100, 400, 100, 50, 50], guarantees); 70 | // Note that tokens ids will be one higher than the indexes used here. 71 | guarantees[0] = 3; 72 | setOptionSettings(Option.Premium, 5, [7200, 2100, 400, 200, 50, 50], guarantees); 73 | guarantees[2] = 2; 74 | guarantees[4] = 1; 75 | setOptionSettings(Option.Gold, 7, [7000, 2100, 400, 400, 50, 50], guarantees); 76 | } 77 | 78 | ////// 79 | // INITIALIZATION FUNCTIONS FOR OWNER 80 | ////// 81 | 82 | /** 83 | * @dev If the tokens for some class are pre-minted and owned by the 84 | * contract owner, they can be used for a given class by setting them here 85 | */ 86 | function setClassForTokenId( 87 | uint256 _tokenId, 88 | uint256 _classId 89 | ) public onlyOwner { 90 | _checkTokenApproval(); 91 | _addTokenIdToClass(Class(_classId), _tokenId); 92 | } 93 | 94 | /** 95 | * @dev Alternate way to add token ids to a class 96 | * Note: resets the full list for the class instead of adding each token id 97 | */ 98 | function setTokenIdsForClass( 99 | Class _class, 100 | uint256[] memory _tokenIds 101 | ) public onlyOwner { 102 | uint256 classId = uint256(_class); 103 | classIsPreminted[classId] = true; 104 | classToTokenIds[classId] = _tokenIds; 105 | } 106 | 107 | /** 108 | * @dev Remove all token ids for a given class, causing it to fall back to 109 | * creating/minting into the nft address 110 | */ 111 | function resetClass( 112 | uint256 _classId 113 | ) public onlyOwner { 114 | delete classIsPreminted[_classId]; 115 | delete classToTokenIds[_classId]; 116 | } 117 | 118 | /** 119 | * @dev Set token IDs for each rarity class. Bulk version of `setTokenIdForClass` 120 | * @param _tokenIds List of token IDs to set for each class, specified above in order 121 | */ 122 | function setTokenIdsForClasses( 123 | uint256[NUM_CLASSES] memory _tokenIds 124 | ) public onlyOwner { 125 | _checkTokenApproval(); 126 | for (uint256 i = 0; i < _tokenIds.length; i++) { 127 | Class class = Class(i); 128 | _addTokenIdToClass(class, _tokenIds[i]); 129 | } 130 | } 131 | 132 | /** 133 | * @dev Set the settings for a particular lootbox option 134 | * @param _option The Option to set settings for 135 | * @param _maxQuantityPerOpen Maximum number of items to mint per open. 136 | * Set to 0 to disable this option. 137 | * @param _classProbabilities Array of probabilities (basis points, so integers out of 10,000) 138 | * of receiving each class (the index in the array). 139 | * Should add up to 10k and be descending in value. 140 | * @param _guarantees Array of the number of guaranteed items received for each class 141 | * (the index in the array). 142 | */ 143 | function setOptionSettings( 144 | Option _option, 145 | uint256 _maxQuantityPerOpen, 146 | uint16[NUM_CLASSES] memory _classProbabilities, 147 | uint16[NUM_CLASSES] memory _guarantees 148 | ) public onlyOwner { 149 | 150 | // Allow us to skip guarantees and save gas at mint time 151 | // if there are no classes with guarantees 152 | bool hasGuaranteedClasses = false; 153 | for (uint256 i = 0; i < _guarantees.length; i++) { 154 | if (_guarantees[i] > 0) { 155 | hasGuaranteedClasses = true; 156 | } 157 | } 158 | 159 | OptionSettings memory settings = OptionSettings({ 160 | maxQuantityPerOpen: _maxQuantityPerOpen, 161 | classProbabilities: _classProbabilities, 162 | hasGuaranteedClasses: hasGuaranteedClasses, 163 | guarantees: _guarantees 164 | }); 165 | 166 | optionToSettings[uint256(_option)] = settings; 167 | } 168 | 169 | /** 170 | * @dev Improve pseudorandom number generator by letting the owner set the seed manually, 171 | * making attacks more difficult 172 | * @param _newSeed The new seed to use for the next transaction 173 | */ 174 | function setSeed(uint256 _newSeed) public onlyOwner { 175 | seed = _newSeed; 176 | } 177 | 178 | /////// 179 | // MAIN FUNCTIONS 180 | ////// 181 | 182 | /** 183 | * @dev Open a lootbox manually and send what's inside to _toAddress 184 | * Convenience method for contract owner. 185 | */ 186 | function open( 187 | uint256 _optionId, 188 | address _toAddress, 189 | uint256 _amount 190 | ) external onlyOwner { 191 | _mint(Option(_optionId), _toAddress, _amount, ""); 192 | } 193 | 194 | /** 195 | * @dev Main minting logic for lootboxes 196 | * This is called via safeTransferFrom when MyLootBox extends MyFactory. 197 | * NOTE: prices and fees are determined by the sell order on OpenSea. 198 | */ 199 | function _mint( 200 | Option _option, 201 | address _toAddress, 202 | uint256 _amount, 203 | bytes memory /* _data */ 204 | ) internal whenNotPaused nonReentrant { 205 | // Load settings for this box option 206 | uint256 optionId = uint256(_option); 207 | OptionSettings memory settings = optionToSettings[optionId]; 208 | 209 | require(settings.maxQuantityPerOpen > 0, "MyLootBox#_mint: OPTION_NOT_ALLOWED"); 210 | require(_canMint(msg.sender, _option, _amount), "MyLootBox#_mint: CANNOT_MINT"); 211 | 212 | uint256 totalMinted = 0; 213 | 214 | // Iterate over the quantity of boxes specified 215 | for (uint256 i = 0; i < _amount; i++) { 216 | // Iterate over the box's set quantity 217 | uint256 quantitySent = 0; 218 | if (settings.hasGuaranteedClasses) { 219 | // Process guaranteed token ids 220 | for (uint256 classId = 0; classId < settings.guarantees.length; classId++) { 221 | if (classId > 0) { 222 | uint256 quantityOfGaranteed = settings.guarantees[classId]; 223 | _sendTokenWithClass(Class(classId), _toAddress, quantityOfGaranteed); 224 | quantitySent += quantityOfGaranteed; 225 | } 226 | } 227 | } 228 | 229 | // Process non-guaranteed ids 230 | while (quantitySent < settings.maxQuantityPerOpen) { 231 | uint256 quantityOfRandomized = 1; 232 | Class class = _pickRandomClass(settings.classProbabilities); 233 | _sendTokenWithClass(class, _toAddress, quantityOfRandomized); 234 | quantitySent += quantityOfRandomized; 235 | } 236 | 237 | totalMinted += quantitySent; 238 | } 239 | 240 | // Event emissions 241 | emit LootBoxOpened(optionId, _toAddress, _amount, totalMinted); 242 | } 243 | 244 | function withdraw() public onlyOwner { 245 | msg.sender.transfer(address(this).balance); 246 | } 247 | 248 | ///// 249 | // Metadata methods 250 | ///// 251 | 252 | function name() external view returns (string memory) { 253 | return "My Loot Box"; 254 | } 255 | 256 | function symbol() external view returns (string memory) { 257 | return "MYLOOT"; 258 | } 259 | 260 | function uri(uint256 _optionId) external view returns (string memory) { 261 | return Strings.strConcat( 262 | baseMetadataURI, 263 | "box/", 264 | Strings.uint2str(_optionId) 265 | ); 266 | } 267 | 268 | ///// 269 | // HELPER FUNCTIONS 270 | ///// 271 | 272 | // Returns the tokenId sent to _toAddress 273 | function _sendTokenWithClass( 274 | Class _class, 275 | address _toAddress, 276 | uint256 _amount 277 | ) internal returns (uint256) { 278 | uint256 classId = uint256(_class); 279 | MyCollectible nftContract = MyCollectible(nftAddress); 280 | uint256 tokenId = _pickRandomAvailableTokenIdForClass(_class, _amount); 281 | if (classIsPreminted[classId]) { 282 | nftContract.safeTransferFrom( 283 | owner(), 284 | _toAddress, 285 | tokenId, 286 | _amount, 287 | "" 288 | ); 289 | } else if (tokenId == 0) { 290 | tokenId = nftContract.create(_toAddress, _amount, "", ""); 291 | classToTokenIds[classId].push(tokenId); 292 | } else { 293 | nftContract.mint(_toAddress, tokenId, _amount, ""); 294 | } 295 | return tokenId; 296 | } 297 | 298 | function _pickRandomClass( 299 | uint16[NUM_CLASSES] memory _classProbabilities 300 | ) internal returns (Class) { 301 | uint16 value = uint16(_random().mod(INVERSE_BASIS_POINT)); 302 | // Start at top class (length - 1) 303 | // skip common (0), we default to it 304 | for (uint256 i = _classProbabilities.length - 1; i > 0; i--) { 305 | uint16 probability = _classProbabilities[i]; 306 | if (value < probability) { 307 | return Class(i); 308 | } else { 309 | value = value - probability; 310 | } 311 | } 312 | return Class.Common; 313 | } 314 | 315 | function _pickRandomAvailableTokenIdForClass( 316 | Class _class, 317 | uint256 _minAmount 318 | ) internal returns (uint256) { 319 | uint256 classId = uint256(_class); 320 | uint256[] memory tokenIds = classToTokenIds[classId]; 321 | if (tokenIds.length == 0) { 322 | // Unminted 323 | require( 324 | !classIsPreminted[classId], 325 | "MyLootBox#_pickRandomAvailableTokenIdForClass: NO_TOKEN_ON_PREMINTED_CLASS" 326 | ); 327 | return 0; 328 | } 329 | 330 | uint256 randIndex = _random().mod(tokenIds.length); 331 | 332 | if (classIsPreminted[classId]) { 333 | // Make sure owner() owns enough 334 | MyCollectible nftContract = MyCollectible(nftAddress); 335 | for (uint256 i = randIndex; i < randIndex + tokenIds.length; i++) { 336 | uint256 tokenId = tokenIds[i % tokenIds.length]; 337 | if (nftContract.balanceOf(owner(), tokenId) >= _minAmount) { 338 | return tokenId; 339 | } 340 | } 341 | revert("MyLootBox#_pickRandomAvailableTokenIdForClass: NOT_ENOUGH_TOKENS_FOR_CLASS"); 342 | } else { 343 | return tokenIds[randIndex]; 344 | } 345 | } 346 | 347 | /** 348 | * @dev Pseudo-random number generator 349 | * NOTE: to improve randomness, generate it with an oracle 350 | */ 351 | function _random() internal returns (uint256) { 352 | uint256 randomNumber = uint256(keccak256(abi.encodePacked(blockhash(block.number - 1), msg.sender, seed))); 353 | seed = randomNumber; 354 | return randomNumber; 355 | } 356 | 357 | /** 358 | * @dev emit a Warning if we're not approved to transfer nftAddress 359 | */ 360 | function _checkTokenApproval() internal { 361 | MyCollectible nftContract = MyCollectible(nftAddress); 362 | if (!nftContract.isApprovedForAll(owner(), address(this))) { 363 | emit Warning("Lootbox contract is not approved for trading collectible by:", owner()); 364 | } 365 | } 366 | 367 | function _addTokenIdToClass(Class _class, uint256 _tokenId) internal { 368 | uint256 classId = uint256(_class); 369 | classIsPreminted[classId] = true; 370 | classToTokenIds[classId].push(_tokenId); 371 | } 372 | } 373 | -------------------------------------------------------------------------------- /contracts/SafeMath.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.11; 2 | 3 | // via https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/math/SafeMath.sol 4 | 5 | /** 6 | * @dev Wrappers over Solidity's arithmetic operations with added overflow 7 | * checks. 8 | * 9 | * Arithmetic operations in Solidity wrap on overflow. This can easily result 10 | * in bugs, because programmers usually assume that an overflow raises an 11 | * error, which is the standard behavior in high level programming languages. 12 | * `SafeMath` restores this intuition by reverting the transaction when an 13 | * operation overflows. 14 | * 15 | * Using this library instead of the unchecked operations eliminates an entire 16 | * class of bugs, so it's recommended to use it always. 17 | */ 18 | library SafeMath { 19 | /** 20 | * @dev Returns the addition of two unsigned integers, reverting on 21 | * overflow. 22 | * 23 | * Counterpart to Solidity's `+` operator. 24 | * 25 | * Requirements: 26 | * - Addition cannot overflow. 27 | */ 28 | function add(uint256 a, uint256 b) internal pure returns (uint256) { 29 | uint256 c = a + b; 30 | require(c >= a, "SafeMath: addition overflow"); 31 | 32 | return c; 33 | } 34 | 35 | /** 36 | * @dev Returns the subtraction of two unsigned integers, reverting on 37 | * overflow (when the result is negative). 38 | * 39 | * Counterpart to Solidity's `-` operator. 40 | * 41 | * Requirements: 42 | * - Subtraction cannot overflow. 43 | */ 44 | function sub(uint256 a, uint256 b) internal pure returns (uint256) { 45 | require(b <= a, "SafeMath: subtraction overflow"); 46 | uint256 c = a - b; 47 | 48 | return c; 49 | } 50 | 51 | /** 52 | * @dev Returns the multiplication of two unsigned integers, reverting on 53 | * overflow. 54 | * 55 | * Counterpart to Solidity's `*` operator. 56 | * 57 | * Requirements: 58 | * - Multiplication cannot overflow. 59 | */ 60 | function mul(uint256 a, uint256 b) internal pure returns (uint256) { 61 | // Gas optimization: this is cheaper than requiring 'a' not being zero, but the 62 | // benefit is lost if 'b' is also tested. 63 | // See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522 64 | if (a == 0) { 65 | return 0; 66 | } 67 | 68 | uint256 c = a * b; 69 | require(c / a == b, "SafeMath: multiplication overflow"); 70 | 71 | return c; 72 | } 73 | 74 | /** 75 | * @dev Returns the integer division of two unsigned integers. Reverts on 76 | * division by zero. The result is rounded towards zero. 77 | * 78 | * Counterpart to Solidity's `/` operator. Note: this function uses a 79 | * `revert` opcode (which leaves remaining gas untouched) while Solidity 80 | * uses an invalid opcode to revert (consuming all remaining gas). 81 | * 82 | * Requirements: 83 | * - The divisor cannot be zero. 84 | */ 85 | function div(uint256 a, uint256 b) internal pure returns (uint256) { 86 | // Solidity only automatically asserts when dividing by 0 87 | require(b > 0, "SafeMath: division by zero"); 88 | uint256 c = a / b; 89 | // assert(a == b * c + a % b); // There is no case in which this doesn't hold 90 | 91 | return c; 92 | } 93 | 94 | /** 95 | * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), 96 | * Reverts when dividing by zero. 97 | * 98 | * Counterpart to Solidity's `%` operator. This function uses a `revert` 99 | * opcode (which leaves remaining gas untouched) while Solidity uses an 100 | * invalid opcode to revert (consuming all remaining gas). 101 | * 102 | * Requirements: 103 | * - The divisor cannot be zero. 104 | */ 105 | function mod(uint256 a, uint256 b) internal pure returns (uint256) { 106 | require(b != 0, "SafeMath: modulo by zero"); 107 | return a % b; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /contracts/Strings.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.11; 2 | 3 | library Strings { 4 | // via https://github.com/oraclize/ethereum-api/blob/master/oraclizeAPI_0.5.sol 5 | function strConcat(string memory _a, string memory _b, string memory _c, string memory _d, string memory _e) internal pure returns (string memory) { 6 | bytes memory _ba = bytes(_a); 7 | bytes memory _bb = bytes(_b); 8 | bytes memory _bc = bytes(_c); 9 | bytes memory _bd = bytes(_d); 10 | bytes memory _be = bytes(_e); 11 | string memory abcde = new string(_ba.length + _bb.length + _bc.length + _bd.length + _be.length); 12 | bytes memory babcde = bytes(abcde); 13 | uint k = 0; 14 | for (uint i = 0; i < _ba.length; i++) babcde[k++] = _ba[i]; 15 | for (uint i = 0; i < _bb.length; i++) babcde[k++] = _bb[i]; 16 | for (uint i = 0; i < _bc.length; i++) babcde[k++] = _bc[i]; 17 | for (uint i = 0; i < _bd.length; i++) babcde[k++] = _bd[i]; 18 | for (uint i = 0; i < _be.length; i++) babcde[k++] = _be[i]; 19 | return string(babcde); 20 | } 21 | 22 | function strConcat(string memory _a, string memory _b, string memory _c, string memory _d) internal pure returns (string memory) { 23 | return strConcat(_a, _b, _c, _d, ""); 24 | } 25 | 26 | function strConcat(string memory _a, string memory _b, string memory _c) internal pure returns (string memory) { 27 | return strConcat(_a, _b, _c, "", ""); 28 | } 29 | 30 | function strConcat(string memory _a, string memory _b) internal pure returns (string memory) { 31 | return strConcat(_a, _b, "", "", ""); 32 | } 33 | 34 | function uint2str(uint _i) internal pure returns (string memory _uintAsString) { 35 | if (_i == 0) { 36 | return "0"; 37 | } 38 | uint j = _i; 39 | uint len; 40 | while (j != 0) { 41 | len++; 42 | j /= 10; 43 | } 44 | bytes memory bstr = new bytes(len); 45 | uint k = len - 1; 46 | while (_i != 0) { 47 | bstr[k--] = byte(uint8(48 + _i % 10)); 48 | _i /= 10; 49 | } 50 | return string(bstr); 51 | } 52 | } -------------------------------------------------------------------------------- /contracts/test/MockProxyRegistry.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.12; 2 | 3 | 4 | import 'openzeppelin-solidity/contracts/ownership/Ownable.sol'; 5 | 6 | 7 | /** 8 | * @dev A simple mock ProxyRegistry for use in local tests with minimal security 9 | */ 10 | contract MockProxyRegistry is Ownable { 11 | mapping(address => address) public proxies; 12 | 13 | 14 | /***********************************| 15 | | Public Configuration Functions | 16 | |__________________________________*/ 17 | 18 | /** 19 | * @notice Allow the owner to set a proxy for testing 20 | * @param _address The address that the proxy will act on behalf of 21 | * @param _proxyForAddress The proxy that will act on behalf of the address 22 | */ 23 | function setProxy(address _address, address _proxyForAddress) 24 | external 25 | onlyOwner() 26 | { 27 | proxies[_address] = _proxyForAddress; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /contracts/test/TestForReentrancyAttack.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.11; 2 | 3 | import "multi-token-standard/contracts/interfaces/IERC1155TokenReceiver.sol"; 4 | 5 | import "../MyFactory.sol"; 6 | 7 | 8 | contract TestForReentrancyAttack is IERC1155TokenReceiver { 9 | // bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)")) 10 | bytes4 constant internal ERC1155_RECEIVED_SIG = 0xf23a6e61; 11 | // bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)")) 12 | bytes4 constant internal ERC1155_BATCH_RECEIVED_SIG = 0xbc197c81; 13 | // bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)")) ^ bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)")) 14 | bytes4 constant internal INTERFACE_ERC1155_RECEIVER_FULL = 0x4e2312e0; 15 | //bytes4(keccak256('supportsInterface(bytes4)')) 16 | bytes4 constant internal INTERFACE_ERC165 = 0x01ffc9a7; 17 | 18 | address public factoryAddress; 19 | uint256 private totalToMint; 20 | 21 | constructor() public {} 22 | 23 | function setFactoryAddress(address _factoryAddress) external { 24 | factoryAddress = _factoryAddress; 25 | totalToMint = 3; 26 | } 27 | 28 | /*function attack(uint256 _totalToMint) external { 29 | require(_totalToMint >= 2, "_totalToMint must be >= 2"); 30 | totalToMint = _totalToMint; 31 | MyFactory(factoryAddress).mint(1, address(this), 1, ""); 32 | }*/ 33 | 34 | // We attempt a reentrancy attack here by recursively calling the MyFactory 35 | // that created the MyCollectible ERC1155 token that we are receiving here. 36 | // We expect this to fail if the MyFactory.mint() function defends against 37 | // reentrancy. 38 | 39 | function onERC1155Received( 40 | address /*_operator*/, 41 | address /*_from*/, 42 | uint256 _id, 43 | uint256 /*_amount*/, 44 | bytes calldata /*_data*/ 45 | ) 46 | external 47 | returns(bytes4) 48 | { 49 | uint256 balance = IERC1155(msg.sender).balanceOf(address(this), _id); 50 | if(balance < totalToMint) 51 | { 52 | // 1 is the factory lootbox option, not the token id 53 | MyFactory(factoryAddress).mint(1, address(this), 1, ""); 54 | } 55 | return ERC1155_RECEIVED_SIG; 56 | } 57 | 58 | function supportsInterface(bytes4 interfaceID) 59 | external 60 | view 61 | returns (bool) 62 | { 63 | return interfaceID == INTERFACE_ERC165 || 64 | interfaceID == INTERFACE_ERC1155_RECEIVER_FULL; 65 | } 66 | 67 | // We don't use this but we need it for the interface 68 | 69 | function onERC1155BatchReceived(address /*_operator*/, address /*_from*/, uint256[] memory /*_ids*/, uint256[] memory /*_values*/, bytes memory /*_data*/) 70 | public returns(bytes4) 71 | { 72 | return ERC1155_BATCH_RECEIVED_SIG; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /flatten.sh: -------------------------------------------------------------------------------- 1 | ./node_modules/.bin/truffle-flattener contracts/MyCollectible.sol > flattened/MyCollectible.sol 2 | ./node_modules/.bin/truffle-flattener contracts/MyFactory.sol > flattened/MyFactory.sol 3 | ./node_modules/.bin/truffle-flattener contracts/MyLootBox.sol > flattened/MyLootBox.sol 4 | -------------------------------------------------------------------------------- /lib/testValuesCommon.js: -------------------------------------------------------------------------------- 1 | /* Useful aliases */ 2 | 3 | const toBN = web3.utils.toBN; 4 | 5 | 6 | const URI_BASE = 'https://opensea-creatures-api.herokuapp.com/api/'; 7 | const ADDRESS_ZERO = '0x0000000000000000000000000000000000000000'; 8 | const MAX_UINT256 = '0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'; 9 | const MAX_UINT256_BN = toBN(MAX_UINT256); 10 | 11 | 12 | module.exports = { 13 | URI_BASE, 14 | ADDRESS_ZERO, 15 | MAX_UINT256, 16 | MAX_UINT256_BN 17 | }; 18 | -------------------------------------------------------------------------------- /metadata-api/.gitignore: -------------------------------------------------------------------------------- 1 | env/ 2 | .idea/ 3 | credentials/* 4 | .env 5 | -------------------------------------------------------------------------------- /metadata-api/Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn app:app --log-file=- 2 | -------------------------------------------------------------------------------- /metadata-api/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask import jsonify 3 | from google.cloud import storage 4 | from google.oauth2 import service_account 5 | from PIL import Image 6 | import os 7 | import mimetypes 8 | 9 | GOOGLE_STORAGE_PROJECT = os.environ['GOOGLE_STORAGE_PROJECT'] 10 | GOOGLE_STORAGE_BUCKET = os.environ['GOOGLE_STORAGE_BUCKET'] 11 | 12 | app = Flask(__name__) 13 | 14 | FIRST_NAMES = ['Herbie', 'Sprinkles', 'Boris', 'Dave', 'Randy', 'Captain'] 15 | LAST_NAMES = ['Starbelly', 'Fisherton', 'McCoy'] 16 | 17 | BASES = ['jellyfish', 'starfish', 'crab', 'narwhal', 'tealfish', 'goldfish'] 18 | EYES = ['big', 'joy', 'wink', 'sleepy', 'content'] 19 | MOUTH = ['happy', 'surprised', 'pleased', 'cute'] 20 | 21 | 22 | INT_ATTRIBUTES = [5, 2, 3, 4, 8] 23 | FLOAT_ATTRIBUTES = [1.4, 2.3, 11.7, 90.2, 1.2] 24 | STR_ATTRIBUTES = [ 25 | 'happy', 26 | 'sad', 27 | 'sleepy', 28 | 'boring' 29 | ] 30 | BOOST_ATTRIBUTES = [10, 40, 30] 31 | PERCENT_BOOST_ATTRIBUTES = [5, 10, 15] 32 | NUMBER_ATTRIBUTES = [1, 2, 1, 1] 33 | 34 | 35 | @app.route('/api/creature/') 36 | def creature(token_id): 37 | token_id = int(token_id) 38 | num_first_names = len(FIRST_NAMES) 39 | num_last_names = len(LAST_NAMES) 40 | creature_name = "%s %s" % (FIRST_NAMES[token_id % num_first_names], LAST_NAMES[token_id % num_last_names]) 41 | 42 | base = BASES[token_id % len(BASES)] 43 | eyes = EYES[token_id % len(EYES)] 44 | mouth = MOUTH[token_id % len(MOUTH)] 45 | image_url = _compose_image(['images/bases/base-%s.png' % base, 46 | 'images/eyes/eyes-%s.png' % eyes, 47 | 'images/mouths/mouth-%s.png' % mouth], 48 | token_id) 49 | 50 | attributes = [] 51 | _add_attribute(attributes, 'base', BASES, token_id) 52 | _add_attribute(attributes, 'eyes', EYES, token_id) 53 | _add_attribute(attributes, 'mouth', MOUTH, token_id) 54 | _add_attribute(attributes, 'level', INT_ATTRIBUTES, token_id) 55 | _add_attribute(attributes, 'stamina', FLOAT_ATTRIBUTES, token_id) 56 | _add_attribute(attributes, 'personality', STR_ATTRIBUTES, token_id) 57 | _add_attribute(attributes, 'aqua_power', BOOST_ATTRIBUTES, token_id, display_type="boost_number") 58 | _add_attribute(attributes, 'stamina_increase', PERCENT_BOOST_ATTRIBUTES, token_id, display_type="boost_percentage") 59 | _add_attribute(attributes, 'generation', NUMBER_ATTRIBUTES, token_id, display_type="number") 60 | 61 | 62 | return jsonify({ 63 | 'name': creature_name, 64 | 'description': "Friendly OpenSea Creature that enjoys long swims in the ocean.", 65 | 'image': image_url, 66 | 'external_url': 'https://openseacreatures.io/%s' % token_id, 67 | 'attributes': attributes 68 | }) 69 | 70 | 71 | @app.route('/api/box/') 72 | def box(token_id): 73 | token_id = int(token_id) 74 | image_url = _compose_image(['images/box/lootbox.png'], token_id, "box") 75 | 76 | attributes = [] 77 | _add_attribute(attributes, 'number_inside', [3], token_id) 78 | 79 | return jsonify({ 80 | 'name': "Creature Loot Box", 81 | 'description': "This lootbox contains some OpenSea Creatures! It can also be traded!", 82 | 'image': image_url, 83 | 'external_url': 'https://openseacreatures.io/%s' % token_id, 84 | 'attributes': attributes 85 | }) 86 | 87 | 88 | @app.route('/api/factory/') 89 | def factory(token_id): 90 | token_id = int(token_id) 91 | if token_id == 0: 92 | name = "One OpenSea creature" 93 | description = "When you purchase this option, you will receive a single OpenSea creature of a random variety. " \ 94 | "Enjoy and take good care of your aquatic being!" 95 | image_url = _compose_image(['images/factory/egg.png'], token_id, "factory") 96 | num_inside = 1 97 | elif token_id == 1: 98 | name = "Four OpenSea creatures" 99 | description = "When you purchase this option, you will receive four OpenSea creatures of random variety. " \ 100 | "Enjoy and take good care of your aquatic beings!" 101 | image_url = _compose_image(['images/factory/four-eggs.png'], token_id, "factory") 102 | num_inside = 4 103 | elif token_id == 2: 104 | name = "One OpenSea creature lootbox" 105 | description = "When you purchase this option, you will receive one lootbox, which can be opened to reveal three " \ 106 | "OpenSea creatures of random variety. Enjoy and take good care of these cute aquatic beings!" 107 | image_url = _compose_image(['images/box/lootbox.png'], token_id, "factory") 108 | num_inside = 3 109 | 110 | attributes = [] 111 | _add_attribute(attributes, 'number_inside', [num_inside], token_id) 112 | 113 | return jsonify({ 114 | 'name': name, 115 | 'description': description, 116 | 'image': image_url, 117 | 'external_url': 'https://openseacreatures.io/%s' % token_id, 118 | 'attributes': attributes 119 | }) 120 | 121 | 122 | def _add_attribute(existing, attribute_name, options, token_id, display_type=None): 123 | trait = { 124 | 'trait_type': attribute_name, 125 | 'value': options[token_id % len(options)] 126 | } 127 | if display_type: 128 | trait['display_type'] = display_type 129 | existing.append(trait) 130 | 131 | 132 | def _compose_image(image_files, token_id, path="creature"): 133 | composite = None 134 | for image_file in image_files: 135 | foreground = Image.open(image_file).convert("RGBA") 136 | 137 | if composite: 138 | composite = Image.alpha_composite(composite, foreground) 139 | else: 140 | composite = foreground 141 | 142 | output_path = "images/output/%s.png" % token_id 143 | composite.save(output_path) 144 | 145 | blob = _get_bucket().blob(f"{path}/{token_id}.png") 146 | blob.upload_from_filename(filename=output_path) 147 | return blob.public_url 148 | 149 | 150 | def _get_bucket(): 151 | credentials = service_account.Credentials.from_service_account_file('credentials/google-storage-credentials.json') 152 | if credentials.requires_scopes: 153 | credentials = credentials.with_scopes(['https://www.googleapis.com/auth/devstorage.read_write']) 154 | client = storage.Client(project=GOOGLE_STORAGE_PROJECT, credentials=credentials) 155 | return client.get_bucket(GOOGLE_STORAGE_BUCKET) 156 | 157 | 158 | if __name__ == '__main__': 159 | app.run(debug=True, use_reloader=True) -------------------------------------------------------------------------------- /metadata-api/images/bases/base-crab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Morpheuslabs-io/ml-opensea-erc1155/f3fdeb3fbd013c814686aaa21d657df005dad915/metadata-api/images/bases/base-crab.png -------------------------------------------------------------------------------- /metadata-api/images/bases/base-goldfish.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Morpheuslabs-io/ml-opensea-erc1155/f3fdeb3fbd013c814686aaa21d657df005dad915/metadata-api/images/bases/base-goldfish.png -------------------------------------------------------------------------------- /metadata-api/images/bases/base-jellyfish.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Morpheuslabs-io/ml-opensea-erc1155/f3fdeb3fbd013c814686aaa21d657df005dad915/metadata-api/images/bases/base-jellyfish.png -------------------------------------------------------------------------------- /metadata-api/images/bases/base-narwhal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Morpheuslabs-io/ml-opensea-erc1155/f3fdeb3fbd013c814686aaa21d657df005dad915/metadata-api/images/bases/base-narwhal.png -------------------------------------------------------------------------------- /metadata-api/images/bases/base-starfish.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Morpheuslabs-io/ml-opensea-erc1155/f3fdeb3fbd013c814686aaa21d657df005dad915/metadata-api/images/bases/base-starfish.png -------------------------------------------------------------------------------- /metadata-api/images/bases/base-tealfish.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Morpheuslabs-io/ml-opensea-erc1155/f3fdeb3fbd013c814686aaa21d657df005dad915/metadata-api/images/bases/base-tealfish.png -------------------------------------------------------------------------------- /metadata-api/images/box/lootbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Morpheuslabs-io/ml-opensea-erc1155/f3fdeb3fbd013c814686aaa21d657df005dad915/metadata-api/images/box/lootbox.png -------------------------------------------------------------------------------- /metadata-api/images/box/multiple-eggs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Morpheuslabs-io/ml-opensea-erc1155/f3fdeb3fbd013c814686aaa21d657df005dad915/metadata-api/images/box/multiple-eggs.png -------------------------------------------------------------------------------- /metadata-api/images/eyes/eyes-big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Morpheuslabs-io/ml-opensea-erc1155/f3fdeb3fbd013c814686aaa21d657df005dad915/metadata-api/images/eyes/eyes-big.png -------------------------------------------------------------------------------- /metadata-api/images/eyes/eyes-content.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Morpheuslabs-io/ml-opensea-erc1155/f3fdeb3fbd013c814686aaa21d657df005dad915/metadata-api/images/eyes/eyes-content.png -------------------------------------------------------------------------------- /metadata-api/images/eyes/eyes-joy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Morpheuslabs-io/ml-opensea-erc1155/f3fdeb3fbd013c814686aaa21d657df005dad915/metadata-api/images/eyes/eyes-joy.png -------------------------------------------------------------------------------- /metadata-api/images/eyes/eyes-sleepy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Morpheuslabs-io/ml-opensea-erc1155/f3fdeb3fbd013c814686aaa21d657df005dad915/metadata-api/images/eyes/eyes-sleepy.png -------------------------------------------------------------------------------- /metadata-api/images/eyes/eyes-wink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Morpheuslabs-io/ml-opensea-erc1155/f3fdeb3fbd013c814686aaa21d657df005dad915/metadata-api/images/eyes/eyes-wink.png -------------------------------------------------------------------------------- /metadata-api/images/factory/egg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Morpheuslabs-io/ml-opensea-erc1155/f3fdeb3fbd013c814686aaa21d657df005dad915/metadata-api/images/factory/egg.png -------------------------------------------------------------------------------- /metadata-api/images/factory/four-eggs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Morpheuslabs-io/ml-opensea-erc1155/f3fdeb3fbd013c814686aaa21d657df005dad915/metadata-api/images/factory/four-eggs.png -------------------------------------------------------------------------------- /metadata-api/images/mouths/mouth-cute.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Morpheuslabs-io/ml-opensea-erc1155/f3fdeb3fbd013c814686aaa21d657df005dad915/metadata-api/images/mouths/mouth-cute.png -------------------------------------------------------------------------------- /metadata-api/images/mouths/mouth-happy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Morpheuslabs-io/ml-opensea-erc1155/f3fdeb3fbd013c814686aaa21d657df005dad915/metadata-api/images/mouths/mouth-happy.png -------------------------------------------------------------------------------- /metadata-api/images/mouths/mouth-pleased.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Morpheuslabs-io/ml-opensea-erc1155/f3fdeb3fbd013c814686aaa21d657df005dad915/metadata-api/images/mouths/mouth-pleased.png -------------------------------------------------------------------------------- /metadata-api/images/mouths/mouth-smirk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Morpheuslabs-io/ml-opensea-erc1155/f3fdeb3fbd013c814686aaa21d657df005dad915/metadata-api/images/mouths/mouth-smirk.png -------------------------------------------------------------------------------- /metadata-api/images/mouths/mouth-surprised.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Morpheuslabs-io/ml-opensea-erc1155/f3fdeb3fbd013c814686aaa21d657df005dad915/metadata-api/images/mouths/mouth-surprised.png -------------------------------------------------------------------------------- /metadata-api/images/output/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Morpheuslabs-io/ml-opensea-erc1155/f3fdeb3fbd013c814686aaa21d657df005dad915/metadata-api/images/output/1.png -------------------------------------------------------------------------------- /metadata-api/images/output/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Morpheuslabs-io/ml-opensea-erc1155/f3fdeb3fbd013c814686aaa21d657df005dad915/metadata-api/images/output/3.png -------------------------------------------------------------------------------- /metadata-api/images/output/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Morpheuslabs-io/ml-opensea-erc1155/f3fdeb3fbd013c814686aaa21d657df005dad915/metadata-api/images/output/7.png -------------------------------------------------------------------------------- /metadata-api/images/output/temp.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Morpheuslabs-io/ml-opensea-erc1155/f3fdeb3fbd013c814686aaa21d657df005dad915/metadata-api/images/output/temp.txt -------------------------------------------------------------------------------- /metadata-api/requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==2.2.5 2 | alabaster==0.7.10 3 | asn1crypto==0.23.0 4 | async-timeout==1.3.0 5 | Babel==2.5.0 6 | bitcoin==1.1.42 7 | cachetools==2.0.1 8 | certifi==2017.7.27.1 9 | cffi==1.10.0 10 | chardet==3.0.4 11 | click==6.7 12 | coincurve==6.0.0 13 | cytoolz==0.8.2 14 | decorator==4.1.2 15 | docutils==0.14 16 | eth-testrpc==1.3.0 17 | ethereum==1.6.1 18 | ethereum-abi-utils==0.4.0 19 | ethereum-utils==0.4.0 20 | Flask==0.12.2 21 | google-api-core==1.1.1 22 | google-auth==1.4.1 23 | google-cloud-core==0.28.1 24 | google-cloud-storage==1.8.0 25 | google-resumable-media==0.3.1 26 | googleapis-common-protos==1.5.3 27 | gunicorn==19.7.1 28 | httpretty==0.8.14 29 | idna==2.6 30 | imagesize==0.7.1 31 | itsdangerous==0.24 32 | Jinja2==2.9.6 33 | json-rpc==1.10.3 34 | MarkupSafe==1.0 35 | multidict==3.1.3 36 | networkx==1.11 37 | pbkdf2==1.3 38 | pbr==3.1.1 39 | Pillow==5.1.0 40 | ply==3.10 41 | protobuf==3.5.2.post1 42 | pyasn1==0.4.2 43 | pyasn1-modules==0.2.1 44 | pycparser==2.18 45 | pycryptodome==3.4.7 46 | pyethash==0.1.27 47 | Pygments==2.2.0 48 | pylru==1.0.9 49 | pysha3==1.0.2 50 | pytz==2017.2 51 | PyYAML==3.12 52 | pyzmq==16.0.2 53 | repoze.lru==0.6 54 | requests==2.18.4 55 | rlp==0.5.1 56 | rsa==3.4.2 57 | scrypt==0.8.0 58 | secp256k1==0.13.2 59 | six==1.11.0 60 | snowballstemmer==1.2.1 61 | sortedcontainers==1.5.7 62 | sphinxcontrib-websupport==1.0.1 63 | stevedore==1.27.1 64 | thriftpy==0.3.9 65 | tinydb==3.3.1 66 | toolz==0.8.2 67 | urllib3==1.22 68 | virtualenv==15.1.0 69 | virtualenv-clone==0.2.6 70 | virtualenvwrapper==4.8.2 71 | web3==3.11.0 72 | Werkzeug==0.12.2 73 | yarl==0.12.0 74 | zmq==0.0.0 75 | -------------------------------------------------------------------------------- /metadata-api/src/pip-delete-this-directory.txt: -------------------------------------------------------------------------------- 1 | This file is placed here by pip to indicate the source was put 2 | here by pip. 3 | 4 | Once this package is successfully installed this source code will be 5 | deleted (unless you remove this file). 6 | -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | const Migrations = artifacts.require("Migrations"); 2 | 3 | module.exports = function(deployer, network, accounts) { 4 | console.log(`Using network: ${network}`); 5 | console.log(`Using accounts`, accounts); 6 | deployer.deploy(Migrations); 7 | }; 8 | -------------------------------------------------------------------------------- /migrations/2_deploy_contracts.js: -------------------------------------------------------------------------------- 1 | const MyCollectible = artifacts.require("MyCollectible"); 2 | const MyLootBox = artifacts.require("MyLootBox"); 3 | 4 | // Set to false if you only want the collectible to deploy 5 | const ENABLE_LOOTBOX = true; 6 | // Set if you want to create your own collectible 7 | const NFT_ADDRESS_TO_USE = undefined; // e.g. Enjin: '0xfaafdc07907ff5120a76b34b731b278c38d6043c' 8 | // If you want to set preminted token ids for specific classes 9 | const TOKEN_ID_MAPPING = undefined; // { [key: number]: Array<[tokenId: string]> } 10 | 11 | module.exports = function(deployer, network) { 12 | // OpenSea proxy registry addresses for rinkeby and mainnet. 13 | let proxyRegistryAddress; 14 | if (network === 'rinkeby') { 15 | proxyRegistryAddress = "0xf57b2c51ded3a29e6891aba85459d600256cf317"; 16 | } else { 17 | proxyRegistryAddress = "0xa5409ec958c83c3f309868babaca7c86dcb077c1"; 18 | } 19 | 20 | if (!ENABLE_LOOTBOX) { 21 | deployer.deploy(MyCollectible, proxyRegistryAddress, {gas: 5000000}); 22 | } else if (NFT_ADDRESS_TO_USE) { 23 | deployer.deploy(MyLootBox, proxyRegistryAddress, NFT_ADDRESS_TO_USE, {gas: 5000000}) 24 | .then(setupLootbox); 25 | } else { 26 | deployer.deploy(MyCollectible, proxyRegistryAddress, {gas: 5000000}) 27 | .then(() => { 28 | return deployer.deploy(MyLootBox, proxyRegistryAddress, MyCollectible.address, {gas: 5000000}); 29 | }) 30 | .then(setupLootbox); 31 | } 32 | }; 33 | 34 | async function setupLootbox() { 35 | if (!NFT_ADDRESS_TO_USE) { 36 | const collectible = await MyCollectible.deployed(); 37 | await collectible.transferOwnership(MyLootBox.address); 38 | } 39 | 40 | if (TOKEN_ID_MAPPING) { 41 | const lootbox = await MyLootBox.deployed(); 42 | for (const rarity in TOKEN_ID_MAPPING) { 43 | console.log(`Setting token ids for rarity ${rarity}`); 44 | const tokenIds = TOKEN_ID_MAPPING[rarity]; 45 | await lootbox.setTokenIdsForClass(rarity, tokenIds); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "opensea-erc1155", 3 | "version": "1.0.0", 4 | "description": "Starter contracts for an ERC-1155 project.", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "truffle compile", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/ProjectOpenSea/opensea-erc1155.git" 13 | }, 14 | "author": "alex.atallah@opensea.io", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/ProjectOpenSea/opensea-erc1155/issues" 18 | }, 19 | "homepage": "https://github.com/ProjectOpenSea/opensea-erc1155#readme", 20 | "dependencies": { 21 | "@0x/subproviders": "^2.1.4", 22 | "multi-token-standard": "github:ProjectOpenSea/multi-token-standard", 23 | "opensea-js": "^1.1.5", 24 | "openzeppelin-solidity": "^2.1.3", 25 | "truffle": "^5.1.12", 26 | "truffle-assertions": "^0.9.2", 27 | "truffle-flattener": "1.4.2", 28 | "truffle-hdwallet-provider": "1.0.17", 29 | "web3": "1.0.0-beta.34" 30 | }, 31 | "devDependencies": { 32 | "eth-gas-reporter": "^0.2.14" 33 | }, 34 | "engines": { 35 | "node": ">=8.11.x", 36 | "npm": ">=5.6.x" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /scripts/advanced/mint.js: -------------------------------------------------------------------------------- 1 | const HDWalletProvider = require("truffle-hdwallet-provider") 2 | const web3 = require('web3') 3 | const MNEMONIC = process.env.MNEMONIC 4 | const INFURA_KEY = process.env.INFURA_KEY 5 | const FACTORY_CONTRACT_ADDRESS = process.env.FACTORY_CONTRACT_ADDRESS 6 | const OWNER_ADDRESS = process.env.OWNER_ADDRESS 7 | const NETWORK = process.env.NETWORK 8 | 9 | if (!MNEMONIC || !INFURA_KEY || !OWNER_ADDRESS || !NETWORK) { 10 | console.error("Please set a mnemonic, infura key, owner, network, and contract address.") 11 | return 12 | } 13 | 14 | const FACTORY_ABI = [{ 15 | "constant": false, 16 | "inputs": [ 17 | { 18 | "internalType": "uint256", 19 | "name": "_optionId", 20 | "type": "uint256" 21 | }, 22 | { 23 | "internalType": "address", 24 | "name": "_toAddress", 25 | "type": "address" 26 | }, 27 | { 28 | "internalType": "uint256", 29 | "name": "_amount", 30 | "type": "uint256" 31 | } 32 | ], 33 | "name": "open", 34 | "outputs": [], 35 | "payable": false, 36 | "stateMutability": "nonpayable", 37 | "type": "function" 38 | }] 39 | 40 | /** 41 | * For now, this script just opens a lootbox. 42 | */ 43 | async function main() { 44 | const provider = new HDWalletProvider(MNEMONIC, `https://${NETWORK}.infura.io/v3/${INFURA_KEY}`) 45 | const web3Instance = new web3( 46 | provider 47 | ) 48 | 49 | if (!FACTORY_CONTRACT_ADDRESS) { 50 | console.error("Please set an NFT contract address.") 51 | return 52 | } 53 | 54 | const factoryContract = new web3Instance.eth.Contract(FACTORY_ABI, FACTORY_CONTRACT_ADDRESS, { gasLimit: "1000000" }) 55 | const result = await factoryContract.methods.open(0, OWNER_ADDRESS, 1).send({ from: OWNER_ADDRESS }); 56 | console.log("Created. Transaction: " + result.transactionHash) 57 | } 58 | 59 | main() 60 | -------------------------------------------------------------------------------- /scripts/initial_sale.js: -------------------------------------------------------------------------------- 1 | const opensea = require('opensea-js') 2 | const { WyvernSchemaName } = require("opensea-js/lib/types") 3 | const OpenSeaPort = opensea.OpenSeaPort; 4 | const Network = opensea.Network; 5 | const MnemonicWalletSubprovider = require('@0x/subproviders').MnemonicWalletSubprovider 6 | const RPCSubprovider = require('web3-provider-engine/subproviders/rpc') 7 | const Web3ProviderEngine = require('web3-provider-engine') 8 | 9 | const MNEMONIC = process.env.MNEMONIC 10 | const INFURA_KEY = process.env.INFURA_KEY 11 | const FACTORY_CONTRACT_ADDRESS = process.env.FACTORY_CONTRACT_ADDRESS 12 | const OWNER_ADDRESS = process.env.OWNER_ADDRESS 13 | const NETWORK = process.env.NETWORK 14 | const API_KEY = process.env.API_KEY || "" // API key is optional but useful if you're doing a high volume of requests. 15 | 16 | const FIXED_PRICE_OPTION_IDS = ["0", "1", "2"]; 17 | const FIXED_PRICES_ETH = [0.1, 0.2, 0.3]; 18 | const NUM_FIXED_PRICE_AUCTIONS = [1000, 1000, 1000]; // [2034, 2103, 2202]; 19 | 20 | if (!MNEMONIC || !INFURA_KEY || !NETWORK || !OWNER_ADDRESS) { 21 | console.error("Please set a mnemonic, infura key, owner, network, API key, nft contract, and factory contract address.") 22 | return 23 | } 24 | 25 | if (!FACTORY_CONTRACT_ADDRESS) { 26 | console.error("Please specify a factory contract address.") 27 | return 28 | } 29 | 30 | const BASE_DERIVATION_PATH = `44'/60'/0'/0` 31 | 32 | const mnemonicWalletSubprovider = new MnemonicWalletSubprovider({ mnemonic: MNEMONIC, baseDerivationPath: BASE_DERIVATION_PATH}) 33 | const infuraRpcSubprovider = new RPCSubprovider({ 34 | rpcUrl: 'https://' + NETWORK + '.infura.io/v3/' + INFURA_KEY, 35 | }) 36 | 37 | const providerEngine = new Web3ProviderEngine() 38 | providerEngine.addProvider(mnemonicWalletSubprovider) 39 | providerEngine.addProvider(infuraRpcSubprovider) 40 | providerEngine.start(); 41 | 42 | const seaport = new OpenSeaPort(providerEngine, { 43 | networkName: NETWORK === 'mainnet' ? Network.Main : Network.Rinkeby, 44 | apiKey: API_KEY 45 | }, (arg) => console.log(arg)) 46 | 47 | async function main() { 48 | // Example: many fixed price auctions for a factory option. 49 | for (let i = 0; i < FIXED_PRICE_OPTION_IDS.length; i++) { 50 | const optionId = FIXED_PRICE_OPTION_IDS[i]; 51 | console.log(`Creating fixed price auctions for ${optionId}...`) 52 | const numOrders = await seaport.createFactorySellOrders({ 53 | assets: [{ 54 | tokenId: optionId, 55 | tokenAddress: FACTORY_CONTRACT_ADDRESS, 56 | // Comment the next line if this is an ERC-721 asset (defaults to ERC721): 57 | schemaName: WyvernSchemaName.ERC1155 58 | }], 59 | // Quantity of each asset to issue 60 | quantity: 1, 61 | accountAddress: OWNER_ADDRESS, 62 | startAmount: FIXED_PRICES_ETH[i], 63 | // Number of times to repeat creating the same order for each asset. If greater than 5, creates them in batches of 5. Requires an `apiKey` to be set during seaport initialization: 64 | numberOfOrders: NUM_FIXED_PRICE_AUCTIONS[i] 65 | }) 66 | console.log(`Successfully made ${numOrders} fixed-price sell orders!\n`) 67 | } 68 | } 69 | 70 | main().catch(e => console.error(e)) 71 | -------------------------------------------------------------------------------- /scripts/sell.js: -------------------------------------------------------------------------------- 1 | const opensea = require('opensea-js') 2 | const { WyvernSchemaName } = require("opensea-js/lib/types") 3 | const OpenSeaPort = opensea.OpenSeaPort; 4 | const Network = opensea.Network; 5 | const MnemonicWalletSubprovider = require('@0x/subproviders').MnemonicWalletSubprovider 6 | const RPCSubprovider = require('web3-provider-engine/subproviders/rpc') 7 | const Web3ProviderEngine = require('web3-provider-engine') 8 | 9 | const MNEMONIC = process.env.MNEMONIC 10 | const INFURA_KEY = process.env.INFURA_KEY 11 | const FACTORY_CONTRACT_ADDRESS = process.env.FACTORY_CONTRACT_ADDRESS 12 | const NFT_CONTRACT_ADDRESS = process.env.NFT_CONTRACT_ADDRESS 13 | const OWNER_ADDRESS = process.env.OWNER_ADDRESS 14 | const NETWORK = process.env.NETWORK 15 | const API_KEY = process.env.API_KEY || "" // API key is optional but useful if you're doing a high volume of requests. 16 | 17 | if (!MNEMONIC || !INFURA_KEY || !NETWORK || !OWNER_ADDRESS) { 18 | console.error("Please set a mnemonic, infura key, owner, network, API key, nft contract, and factory contract address.") 19 | return 20 | } 21 | 22 | if (!FACTORY_CONTRACT_ADDRESS && !NFT_CONTRACT_ADDRESS) { 23 | console.error("Please either set a factory or NFT contract address.") 24 | return 25 | } 26 | 27 | const BASE_DERIVATION_PATH = `44'/60'/0'/0` 28 | 29 | const mnemonicWalletSubprovider = new MnemonicWalletSubprovider({ mnemonic: MNEMONIC, baseDerivationPath: BASE_DERIVATION_PATH}) 30 | const infuraRpcSubprovider = new RPCSubprovider({ 31 | rpcUrl: 'https://' + NETWORK + '.infura.io/v3/' + INFURA_KEY, 32 | }) 33 | 34 | const providerEngine = new Web3ProviderEngine() 35 | providerEngine.addProvider(mnemonicWalletSubprovider) 36 | providerEngine.addProvider(infuraRpcSubprovider) 37 | providerEngine.start(); 38 | 39 | const seaport = new OpenSeaPort(providerEngine, { 40 | networkName: NETWORK === 'mainnet' ? Network.Main : Network.Rinkeby, 41 | apiKey: API_KEY 42 | }, (arg) => console.log(arg)) 43 | 44 | async function main() { 45 | 46 | // Example: simple fixed-price sale of an item owned by a user. 47 | console.log("Auctioning an item for a fixed price...") 48 | const fixedPriceSellOrder = await seaport.createSellOrder({ 49 | asset: { 50 | tokenId: "1", 51 | tokenAddress: NFT_CONTRACT_ADDRESS, 52 | schemaName: WyvernSchemaName.ERC1155 53 | }, 54 | startAmount: .05, 55 | expirationTime: 0, 56 | accountAddress: OWNER_ADDRESS, 57 | }) 58 | console.log(`Successfully created a fixed-price sell order! ${fixedPriceSellOrder.asset.openseaLink}\n`) 59 | 60 | // // Example: Dutch auction. 61 | console.log("Dutch auctioning an item...") 62 | const expirationTime = Math.round(Date.now() / 1000 + 60 * 60 * 24) 63 | const dutchAuctionSellOrder = await seaport.createSellOrder({ 64 | asset: { 65 | tokenId: "2", 66 | tokenAddress: NFT_CONTRACT_ADDRESS, 67 | schemaName: WyvernSchemaName.ERC1155 68 | }, 69 | startAmount: .05, 70 | endAmount: .01, 71 | expirationTime: expirationTime, 72 | accountAddress: OWNER_ADDRESS 73 | }) 74 | console.log(`Successfully created a dutch auction sell order! ${dutchAuctionSellOrder.asset.openseaLink}\n`) 75 | 76 | // Example: multiple item sale for ERC20 token 77 | console.log("Selling multiple items for an ERC20 token (WETH)") 78 | const wethAddress = NETWORK == 'mainnet' ? '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' : "0xc778417e063141139fce010982780140aa0cd5ab" 79 | const englishAuctionSellOrder = await seaport.createSellOrder({ 80 | asset: { 81 | tokenId: "3", 82 | tokenAddress: NFT_CONTRACT_ADDRESS, 83 | schemaName: WyvernSchemaName.ERC1155 84 | }, 85 | startAmount: .03, 86 | quantity: 2, 87 | expirationTime: expirationTime, 88 | paymentTokenAddress: wethAddress, 89 | accountAddress: OWNER_ADDRESS, 90 | }) 91 | console.log(`Successfully created bulk-item sell order! ${englishAuctionSellOrder.asset.openseaLink}\n`) 92 | 93 | } 94 | 95 | main() -------------------------------------------------------------------------------- /test/ERC1155Tradable.js: -------------------------------------------------------------------------------- 1 | /* libraries used */ 2 | 3 | const truffleAssert = require('truffle-assertions'); 4 | 5 | 6 | const vals = require('../lib/testValuesCommon.js'); 7 | 8 | 9 | /* Contracts in this test */ 10 | 11 | const ERC1155Tradable = artifacts.require("../contracts/ERC1155Tradable.sol"); 12 | const MockProxyRegistry = artifacts.require( 13 | "../contracts/MockProxyRegistry.sol" 14 | ); 15 | 16 | 17 | /* Useful aliases */ 18 | 19 | const toBN = web3.utils.toBN; 20 | 21 | 22 | contract("ERC1155Tradable - ERC 1155", (accounts) => { 23 | const NAME = 'ERC-1155 Test Contract'; 24 | const SYMBOL = 'ERC1155Test'; 25 | 26 | const INITIAL_TOKEN_ID = 1; 27 | const NON_EXISTENT_TOKEN_ID = 99999999; 28 | const MINT_AMOUNT = toBN(100); 29 | 30 | const OVERFLOW_NUMBER = toBN(2, 10).pow(toBN(256, 10)).sub(toBN(1, 10)); 31 | 32 | const owner = accounts[0]; 33 | const creator = accounts[1]; 34 | const userA = accounts[2]; 35 | const userB = accounts[3]; 36 | const proxyForOwner = accounts[5]; 37 | 38 | let instance; 39 | let proxy; 40 | 41 | // Keep track of token ids as we progress through the tests, rather than 42 | // hardcoding numbers that we will have to change if we add/move tests. 43 | // For example if test A assumes that it will create token ID 1 and test B 44 | // assumes that it will create token 2, changing test A later so that it 45 | // creates another token will break this as test B will now create token ID 3. 46 | // Doing this avoids this scenario. 47 | let tokenId = 0; 48 | 49 | // Because we need to deploy and use a mock ProxyRegistry, we deploy our own 50 | // instance of ERC1155Tradable instead of using the one that Truffle deployed. 51 | 52 | before(async () => { 53 | proxy = await MockProxyRegistry.new(); 54 | await proxy.setProxy(owner, proxyForOwner); 55 | instance = await ERC1155Tradable.new(NAME, SYMBOL, proxy.address); 56 | }); 57 | 58 | describe('#constructor()', () => { 59 | it('should set the token name and symbol', async () => { 60 | const name = await instance.name(); 61 | assert.equal(name, NAME); 62 | const symbol = await instance.symbol(); 63 | assert.equal(symbol, SYMBOL); 64 | // We cannot check the proxyRegistryAddress as there is no accessor for it 65 | }); 66 | }); 67 | 68 | describe('#create()', () => { 69 | it('should allow the contract owner to create tokens with zero supply', 70 | async () => { 71 | tokenId += 1; 72 | truffleAssert.eventEmitted( 73 | await instance.create(owner, 0, "", "0x0", { from: owner }), 74 | 'TransferSingle', 75 | { 76 | _operator: owner, 77 | _from: vals.ADDRESS_ZERO, 78 | _to: owner, 79 | _id: toBN(tokenId), 80 | _amount: toBN(0) 81 | } 82 | ); 83 | const supply = await instance.tokenSupply(tokenId); 84 | assert.ok(supply.eq(toBN(0))); 85 | }); 86 | 87 | it('should allow the contract owner to create tokens with initial supply', 88 | async () => { 89 | tokenId += 1; 90 | truffleAssert.eventEmitted( 91 | await instance.create( 92 | owner, 93 | MINT_AMOUNT, 94 | "", 95 | "0x0", 96 | { from: owner } 97 | ), 98 | 'TransferSingle', 99 | { 100 | _operator: owner, 101 | _from: vals.ADDRESS_ZERO, 102 | _to: owner, 103 | _id: toBN(tokenId), 104 | _amount: MINT_AMOUNT 105 | } 106 | ); 107 | const supply = await instance.tokenSupply(tokenId); 108 | assert.ok(supply.eq(MINT_AMOUNT)); 109 | }); 110 | 111 | // We check some of this in the other create() tests but this makes it 112 | // explicit and is more thorough. 113 | it('should set tokenSupply on creation', 114 | async () => { 115 | tokenId += 1; 116 | truffleAssert.eventEmitted( 117 | await instance.create(owner, 33, "", "0x0", { from: owner }), 118 | 'TransferSingle', 119 | { _id: toBN(tokenId) } 120 | ); 121 | const balance = await instance.balanceOf(owner, tokenId); 122 | assert.ok(balance.eq(toBN(33))); 123 | const supply = await instance.tokenSupply(tokenId); 124 | assert.ok(supply.eq(toBN(33))); 125 | assert.ok(supply.eq(balance)); 126 | }); 127 | 128 | it('should increment the token type id', 129 | async () => { 130 | // We can't check this with an accessor, so we make an explicit check 131 | // that it increases in consecutive creates() using the value emitted 132 | // in their events. 133 | tokenId += 1; 134 | await truffleAssert.eventEmitted( 135 | await instance.create(owner, 0, "", "0x0", { from: owner }), 136 | 'TransferSingle', 137 | { _id: toBN(tokenId) } 138 | ); 139 | tokenId += 1; 140 | await truffleAssert.eventEmitted( 141 | await instance.create(owner, 0, "", "0x0", { from: owner }), 142 | 'TransferSingle', 143 | { _id: toBN(tokenId) } 144 | ); 145 | }); 146 | 147 | it('should not allow a non-owner to create tokens', 148 | async () => { 149 | truffleAssert.fails( 150 | instance.create(userA, 0, "", "0x0", { from: userA }), 151 | truffleAssert.ErrorType.revert, 152 | 'caller is not the owner' 153 | ); 154 | }); 155 | 156 | it('should allow the contract owner to create tokens and emit a URI', 157 | async () => { 158 | tokenId += 1; 159 | truffleAssert.eventEmitted( 160 | await instance.create( 161 | owner, 162 | 0, 163 | vals.URI_BASE, 164 | "0x0", 165 | { from: owner } 166 | ), 167 | 'URI', 168 | { 169 | _uri: vals.URI_BASE, 170 | _id: toBN(tokenId) 171 | } 172 | ); 173 | }); 174 | 175 | it('should not emit a URI if none is passed', 176 | async () => { 177 | tokenId += 1; 178 | truffleAssert.eventNotEmitted( 179 | await instance.create(owner, 0, "", "0x0", { from: owner }), 180 | 'URI' 181 | ); 182 | }); 183 | }); 184 | 185 | describe('#totalSupply()', () => { 186 | it('should return correct value for token supply', 187 | async () => { 188 | tokenId += 1; 189 | await instance.create(owner, MINT_AMOUNT, "", "0x0", { from: owner }); 190 | const balance = await instance.balanceOf(owner, tokenId); 191 | assert.ok(balance.eq(MINT_AMOUNT)); 192 | // Use the created getter for the map 193 | const supplyGetterValue = await instance.tokenSupply(tokenId); 194 | assert.ok(supplyGetterValue.eq(MINT_AMOUNT)); 195 | // Use the hand-crafted accessor 196 | const supplyAccessorValue = await instance.totalSupply(tokenId); 197 | assert.ok(supplyAccessorValue.eq(MINT_AMOUNT)); 198 | 199 | // Make explicitly sure everything mateches 200 | assert.ok(supplyGetterValue.eq(balance)); 201 | assert.ok(supplyAccessorValue.eq(balance)); 202 | }); 203 | 204 | it('should return zero for non-existent token', 205 | async () => { 206 | const balanceValue = await instance.balanceOf( 207 | owner, 208 | NON_EXISTENT_TOKEN_ID 209 | ); 210 | assert.ok(balanceValue.eq(toBN(0))); 211 | const supplyAccessorValue = await instance.totalSupply( 212 | NON_EXISTENT_TOKEN_ID 213 | ); 214 | assert.ok(supplyAccessorValue.eq(toBN(0))); 215 | }); 216 | }); 217 | 218 | describe('#setCreator()', () => { 219 | it('should allow the token creator to set creator to another address', 220 | async () => { 221 | instance.setCreator(userA, [INITIAL_TOKEN_ID], {from: owner}); 222 | const tokenCreator = await instance.creators(INITIAL_TOKEN_ID); 223 | assert.equal(tokenCreator, userA); 224 | }); 225 | 226 | it('should allow the new creator to set creator to another address', 227 | async () => { 228 | await instance.setCreator(creator, [INITIAL_TOKEN_ID], {from: userA}); 229 | const tokenCreator = await instance.creators(INITIAL_TOKEN_ID); 230 | assert.equal(tokenCreator, creator); 231 | }); 232 | 233 | it('should not allow the token creator to set creator to 0x0', 234 | () => truffleAssert.fails( 235 | instance.setCreator( 236 | vals.ADDRESS_ZERO, 237 | [INITIAL_TOKEN_ID], 238 | { from: creator } 239 | ), 240 | truffleAssert.ErrorType.revert, 241 | 'ERC1155Tradable#setCreator: INVALID_ADDRESS.' 242 | )); 243 | 244 | it('should not allow a non-token-creator to set creator', 245 | // Check both a user and the owner of the contract 246 | async () => { 247 | await truffleAssert.fails( 248 | instance.setCreator(userA, [INITIAL_TOKEN_ID], {from: userA}), 249 | truffleAssert.ErrorType.revert, 250 | 'ERC1155Tradable#creatorOnly: ONLY_CREATOR_ALLOWED' 251 | ); 252 | await truffleAssert.fails( 253 | instance.setCreator(owner, [INITIAL_TOKEN_ID], {from: owner}), 254 | truffleAssert.ErrorType.revert, 255 | 'ERC1155Tradable#creatorOnly: ONLY_CREATOR_ALLOWED' 256 | ); 257 | }); 258 | }); 259 | 260 | describe('#mint()', () => { 261 | it('should allow creator to mint tokens', 262 | async () => { 263 | await instance.mint( 264 | userA, 265 | INITIAL_TOKEN_ID, 266 | MINT_AMOUNT, 267 | "0x0", 268 | { from: creator } 269 | ); 270 | let supply = await instance.totalSupply(INITIAL_TOKEN_ID); 271 | assert.isOk(supply.eq(MINT_AMOUNT)); 272 | }); 273 | 274 | it('should update token totalSupply when minting', async () => { 275 | let supply = await instance.totalSupply(INITIAL_TOKEN_ID); 276 | assert.isOk(supply.eq(MINT_AMOUNT)); 277 | await instance.mint( 278 | userA, 279 | INITIAL_TOKEN_ID, 280 | MINT_AMOUNT, 281 | "0x0", 282 | { from: creator } 283 | ); 284 | supply = await instance.totalSupply(INITIAL_TOKEN_ID); 285 | assert.isOk(supply.eq(MINT_AMOUNT.mul(toBN(2)))); 286 | }); 287 | 288 | it('should not overflow token balances', 289 | async () => { 290 | const supply = await instance.totalSupply(INITIAL_TOKEN_ID); 291 | assert.isOk(supply.eq(MINT_AMOUNT.add(MINT_AMOUNT))); 292 | await truffleAssert.fails( 293 | instance.mint( 294 | userB, 295 | INITIAL_TOKEN_ID, 296 | OVERFLOW_NUMBER, 297 | "0x0", 298 | {from: creator} 299 | ), 300 | truffleAssert.ErrorType.revert, 301 | 'OVERFLOW' 302 | ); 303 | }); 304 | }); 305 | 306 | describe('#batchMint()', () => { 307 | it('should correctly set totalSupply', 308 | async () => { 309 | await instance.batchMint( 310 | userA, 311 | [INITIAL_TOKEN_ID], 312 | [MINT_AMOUNT], 313 | "0x0", 314 | { from: creator } 315 | ); 316 | const supply = await instance.totalSupply(INITIAL_TOKEN_ID); 317 | assert.isOk( 318 | supply.eq(MINT_AMOUNT.mul(toBN(3))) 319 | ); 320 | }); 321 | 322 | it('should not overflow token balances', 323 | () => truffleAssert.fails( 324 | instance.batchMint( 325 | userB, 326 | [INITIAL_TOKEN_ID], 327 | [OVERFLOW_NUMBER], 328 | "0x0", 329 | { from: creator } 330 | ), 331 | truffleAssert.ErrorType.revert, 332 | 'OVERFLOW' 333 | ) 334 | ); 335 | 336 | it('should require that caller has permission to mint each token', 337 | async () => truffleAssert.fails( 338 | instance.batchMint( 339 | userA, 340 | [INITIAL_TOKEN_ID], 341 | [MINT_AMOUNT], 342 | "0x0", 343 | { from: userB } 344 | ), 345 | truffleAssert.ErrorType.revert, 346 | 'ERC1155Tradable#batchMint: ONLY_CREATOR_ALLOWED' 347 | )); 348 | }); 349 | 350 | describe ('#setBaseMetadataURI()', () => { 351 | it('should allow the owner to set the base metadata url', async () => 352 | truffleAssert.passes( 353 | instance.setBaseMetadataURI(vals.URI_BASE, { from: owner }) 354 | )); 355 | 356 | it('should not allow non-owner to set the base metadata url', async () => 357 | truffleAssert.fails( 358 | instance.setBaseMetadataURI(vals.URI_BASE, { from: userA }), 359 | truffleAssert.ErrorType.revert, 360 | 'Ownable: caller is not the owner' 361 | )); 362 | }); 363 | 364 | describe ('#uri()', () => { 365 | it('should return the correct uri for a token', async () => { 366 | const uriTokenId = 1; 367 | const uri = await instance.uri(uriTokenId); 368 | assert.equal(uri, `${vals.URI_BASE}${uriTokenId}`); 369 | }); 370 | 371 | it('should not return the uri for a non-existent token', async () => 372 | truffleAssert.fails( 373 | instance.uri(NON_EXISTENT_TOKEN_ID), 374 | truffleAssert.ErrorType.revert, 375 | 'NONEXISTENT_TOKEN' 376 | ) 377 | ); 378 | }); 379 | 380 | describe('#isApprovedForAll()', () => { 381 | it('should approve proxy address as _operator', async () => { 382 | assert.isOk( 383 | await instance.isApprovedForAll(owner, proxyForOwner) 384 | ); 385 | }); 386 | 387 | it('should not approve non-proxy address as _operator', async () => { 388 | assert.isNotOk( 389 | await instance.isApprovedForAll(owner, userB) 390 | ); 391 | }); 392 | 393 | it('should reject proxy as _operator for non-owner _owner', async () => { 394 | assert.isNotOk( 395 | await instance.isApprovedForAll(userA, proxyForOwner) 396 | ); 397 | }); 398 | 399 | it('should accept approved _operator for _owner', async () => { 400 | await instance.setApprovalForAll(userB, true, { from: userA }); 401 | assert.isOk(await instance.isApprovedForAll(userA, userB)); 402 | // Reset it here 403 | await instance.setApprovalForAll(userB, false, { from: userA }); 404 | }); 405 | 406 | it('should not accept non-approved _operator for _owner', async () => { 407 | await instance.setApprovalForAll(userB, false, { from: userA }); 408 | assert.isNotOk(await instance.isApprovedForAll(userA, userB)); 409 | }); 410 | }); 411 | }); 412 | -------------------------------------------------------------------------------- /test/MyCollectible.js: -------------------------------------------------------------------------------- 1 | /* Contracts in this test */ 2 | 3 | const MyCollectible = artifacts.require("../contracts/MyCollectible.sol"); 4 | 5 | 6 | contract("MyCollectible", (accounts) => { 7 | const URI_BASE = 'https://creatures-api.opensea.io'; 8 | const CONTRACT_URI = `${URI_BASE}/contract/opensea-erc1155`; 9 | let myCollectible; 10 | 11 | before(async () => { 12 | myCollectible = await MyCollectible.deployed(); 13 | }); 14 | 15 | // This is all we test for now 16 | 17 | // This also tests contractURI() 18 | 19 | describe('#constructor()', () => { 20 | it('should set the contractURI to the supplied value', async () => { 21 | assert.equal(await myCollectible.contractURI(), CONTRACT_URI); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/MyFactory.js: -------------------------------------------------------------------------------- 1 | /* libraries used */ 2 | 3 | const truffleAssert = require('truffle-assertions'); 4 | 5 | const vals = require('../lib/testValuesCommon.js'); 6 | 7 | /* Contracts in this test */ 8 | 9 | const MockProxyRegistry = artifacts.require( 10 | "../contracts/MockProxyRegistry.sol" 11 | ); 12 | const MyFactory = artifacts.require("../contracts/MyFactory.sol"); 13 | const MyCollectible = artifacts.require("../contracts/MyCollectible.sol"); 14 | const TestForReentrancyAttack = artifacts.require( 15 | "../contracts/TestForReentrancyAttack.sol" 16 | ); 17 | 18 | 19 | /* Useful aliases */ 20 | 21 | const toBN = web3.utils.toBN; 22 | 23 | 24 | /* NOTE: 25 | * We rely on the accident of collectible token IDs starting at 1, and mint 26 | our PREMIUM token first to make option ID match token ID for PREMIUM and 27 | GOLD. 28 | * We never mint BASIC tokens, as there is no zero token ID in the 29 | collectible. 30 | * For testing paths that must work if no token has been minted, use BASIC. 31 | * We mint PREMIUM and GOLD while testing mint(). 32 | * Therefore any tests that must work with and without tokens minted, use 33 | BASIC for unminted and PREMIUM for minted, *after* mint() is tested. 34 | * Do not test transferFrom() with BASIC as that would create the token as 3. 35 | transferFrom() uses _create, which is tested in create(), so this is fine. 36 | */ 37 | 38 | contract("MyFactory", (accounts) => { 39 | // As set in (or inferred from) the contract 40 | const BASIC = 0; 41 | const PREMIUM =1; 42 | const GOLD = 2; 43 | const NUM_OPTIONS = 3; 44 | const NO_SUCH_OPTION = NUM_OPTIONS + 10; 45 | 46 | const owner = accounts[0]; 47 | const userA = accounts[1]; 48 | const userB = accounts[2]; 49 | const proxyForOwner = accounts[8]; 50 | 51 | let myFactory; 52 | let myCollectible; 53 | let attacker; 54 | let proxy; 55 | 56 | // To install the proxy mock and the attack contract we deploy our own 57 | // instances of all the classes here rather than using the ones that Truffle 58 | // deployed. 59 | 60 | before(async () => { 61 | proxy = await MockProxyRegistry.new(); 62 | await proxy.setProxy(owner, proxyForOwner); 63 | myCollectible = await MyCollectible.new(proxy.address); 64 | myFactory = await MyFactory.new( 65 | proxy.address, 66 | myCollectible.address); 67 | await myCollectible.transferOwnership(myFactory.address); 68 | //await myCollectible.setFactoryAddress(myFactory.address); 69 | attacker = await TestForReentrancyAttack.new(); 70 | await attacker.setFactoryAddress(myFactory.address); 71 | }); 72 | 73 | // This also tests the proxyRegistryAddress and nftAddress accessors. 74 | 75 | describe('#constructor()', () => { 76 | it('should set proxyRegistryAddress to the supplied value', async () => { 77 | assert.equal(await myFactory.proxyRegistryAddress(), proxy.address); 78 | assert.equal(await myFactory.nftAddress(), myCollectible.address); 79 | }); 80 | }); 81 | 82 | describe('#name()', () => { 83 | it('should return the correct name', async () => { 84 | assert.equal(await myFactory.name(), 'My Collectible Pre-Sale'); 85 | }); 86 | }); 87 | 88 | describe('#symbol()', () => { 89 | it('should return the correct symbol', async () => { 90 | assert.equal(await myFactory.symbol(), 'MCP'); 91 | }); 92 | }); 93 | 94 | describe('#supportsFactoryInterface()', () => { 95 | it('should return true', async () => { 96 | assert.isOk(await myFactory.supportsFactoryInterface()); 97 | }); 98 | }); 99 | 100 | describe('#factorySchemaName()', () => { 101 | it('should return the schema name', async () => { 102 | assert.equal(await myFactory.factorySchemaName(), 'ERC1155'); 103 | }); 104 | }); 105 | 106 | describe('#numOptions()', () => { 107 | it('should return the correct number of options', async () => { 108 | assert.equal(await myFactory.numOptions(), NUM_OPTIONS); 109 | }); 110 | }); 111 | 112 | //NOTE: We test this early relative to its place in the source code as we 113 | // mint tokens that we rely on the existence of in later tests here. 114 | 115 | describe('#mint()', () => { 116 | it('should not allow non-owner or non-operator to mint', async () => { 117 | await truffleAssert.fails( 118 | myFactory.mint(PREMIUM, userA, 1000, "0x0", { from: userA }), 119 | truffleAssert.ErrorType.revert, 120 | 'MyFactory#_mint: CANNOT_MINT_MORE' 121 | ); 122 | }); 123 | 124 | it('should allow owner to mint', async () => { 125 | const quantity = toBN(1000); 126 | await myFactory.mint(PREMIUM, userA, quantity, "0x0", { from: owner }); 127 | // Check that the recipient got the correct quantity 128 | const balanceUserA = await myCollectible.balanceOf(userA, PREMIUM); 129 | assert.isOk(balanceUserA.eq(quantity)); 130 | // Check that balance is correct 131 | const balanceOf = await myFactory.balanceOf(owner, PREMIUM); 132 | assert.isOk(balanceOf.eq(vals.MAX_UINT256_BN.sub(quantity))); 133 | // Check that total supply is correct 134 | const totalSupply = await myCollectible.totalSupply(PREMIUM); 135 | assert.isOk(totalSupply.eq(quantity)); 136 | }); 137 | 138 | it('should successfully use both create or mint internally', async () => { 139 | const quantity = toBN(1000); 140 | const total = quantity.mul(toBN(2)); 141 | // It would be nice to check the logs from these, but: 142 | // https://ethereum.stackexchange.com/questions/71785/how-to-test-events-that-were-sent-by-inner-transaction-delegate-call 143 | // Will use create. 144 | await myFactory.mint(GOLD, userA, quantity, "0x0", { from: owner }); 145 | // Will use mint 146 | await myFactory.mint(GOLD, userB, quantity, "0x0", { from: owner }); 147 | // Check that the recipients got the correct quantity 148 | const balanceUserA = await myCollectible.balanceOf(userA, GOLD); 149 | assert.isOk(balanceUserA.eq(quantity)); 150 | const balanceUserB = await myCollectible.balanceOf(userB, GOLD); 151 | assert.isOk(balanceUserB.eq(quantity)); 152 | // Check that balance is correct 153 | const balanceOf = await myFactory.balanceOf(owner, GOLD); 154 | assert.isOk(balanceOf.eq(vals.MAX_UINT256_BN.sub(total))); 155 | // Check that total supply is correct 156 | const totalSupply1 = await myCollectible.totalSupply(2); 157 | assert.isOk(totalSupply1.eq(total)); 158 | }); 159 | 160 | it('should allow proxy to mint', async () => { 161 | const quantity = toBN(100); 162 | //FIXME: move all quantities to top level constants 163 | const total = toBN(1100); 164 | await myFactory.mint( 165 | PREMIUM, 166 | userA, 167 | quantity, 168 | "0x0", 169 | { from: proxyForOwner } 170 | ); 171 | // Check that the recipient got the correct quantity 172 | const balanceUserA = await myCollectible.balanceOf(userA, PREMIUM); 173 | assert.isOk(balanceUserA.eq(total)); 174 | // Check that balance is correct 175 | const balanceOf = await myFactory.balanceOf(owner, PREMIUM); 176 | assert.isOk(balanceOf.eq(vals.MAX_UINT256_BN.sub(total))); 177 | // Check that total supply is correct 178 | const totalSupply = await myCollectible.totalSupply(PREMIUM); 179 | assert.isOk(totalSupply.eq(total)); 180 | }); 181 | }); 182 | 183 | describe('#canMint()', () => { 184 | it('should return false for zero _amount', async () => { 185 | assert.isNotOk(await myFactory.canMint(BASIC, 0, { from: userA })); 186 | assert.isNotOk(await myFactory.canMint(BASIC, 0, { from: owner })); 187 | assert.isNotOk( 188 | await myFactory.canMint(BASIC, 0, { from: proxyForOwner }) 189 | ); 190 | }); 191 | 192 | it('should return false for non-owner and non-proxy', async () => { 193 | assert.isNotOk(await myFactory.canMint(BASIC, 100, { from: userA })); 194 | }); 195 | 196 | it('should return true for un-minted token', async () => { 197 | assert.isOk(await myFactory.canMint(BASIC, 1, { from: owner })); 198 | //FIXME: Why 'invalid opcode'? 199 | //assert.isOk(await myFactory.canMint(GOLD, MAX_UINT256_BN, { from: owner })); 200 | assert.isOk(await myFactory.canMint(BASIC, 1, { from: proxyForOwner })); 201 | }); 202 | 203 | it('should return true for minted token for available amount', async () => { 204 | assert.isOk(await myFactory.canMint(PREMIUM, 1, { from: owner })); 205 | assert.isOk(await myFactory.canMint(PREMIUM, 1, { from: proxyForOwner })); 206 | }); 207 | }); 208 | 209 | describe('#uri()', () => { 210 | it('should return the correct uri for an option', async () => 211 | assert.equal(await myFactory.uri(BASIC), `${vals.URI_BASE}factory/0`) 212 | ); 213 | 214 | it('should format any number as an option uri', async () => 215 | assert.equal( 216 | await myFactory.uri(vals.MAX_UINT256), 217 | `${vals.URI_BASE}factory/${toBN(vals.MAX_UINT256).toString()}` 218 | )); 219 | }); 220 | 221 | describe('#balanceOf()', () => { 222 | it('should return max supply for un-minted token', async () => { 223 | const balanceOwner = await myFactory.balanceOf(owner, BASIC); 224 | assert.isOk(balanceOwner.eq(vals.MAX_UINT256_BN)); 225 | const balanceProxy = await myFactory.balanceOf( 226 | proxyForOwner, 227 | NO_SUCH_OPTION 228 | ); 229 | assert.isOk(balanceProxy.eq(vals.MAX_UINT256_BN)); 230 | }); 231 | 232 | it('should return balance of minted token', async () => { 233 | const balance = vals.MAX_UINT256_BN.sub(toBN(1100)); 234 | const balanceOwner = await myFactory.balanceOf(owner, PREMIUM); 235 | assert.isOk(balanceOwner.eq(balance)); 236 | const balanceProxy = await myFactory.balanceOf(proxyForOwner, PREMIUM); 237 | assert.isOk(balanceProxy.eq(balance)); 238 | }); 239 | 240 | it('should return zero for non-owner or non-proxy', async () => { 241 | assert.isOk((await myFactory.balanceOf(userA, BASIC)).eq(toBN(0))); 242 | assert.isOk((await myFactory.balanceOf(userB, GOLD)).eq(toBN(0))); 243 | }); 244 | }); 245 | 246 | //NOTE: we should test safeTransferFrom with both an existing and not-yet- 247 | // created token to exercise both paths in its calls of _create(). 248 | // But we test _create() in create() and we don't reset the contracts 249 | // between describe() calls so we only test one path here, and let 250 | // the other be tested in create(). 251 | 252 | describe('#safeTransferFrom()', () => { 253 | it('should work for owner()', async () => { 254 | const amount = toBN(100); 255 | const userBBalance = await myCollectible.balanceOf(userB, PREMIUM); 256 | await myFactory.safeTransferFrom( 257 | vals.ADDRESS_ZERO, 258 | userB, 259 | PREMIUM, 260 | amount, 261 | "0x0" 262 | ); 263 | const newUserBBalance = await myCollectible.balanceOf(userB, PREMIUM); 264 | assert.isOk(newUserBBalance.eq(userBBalance.add(amount))); 265 | }); 266 | 267 | it('should work for proxy', async () => { 268 | const amount = toBN(100); 269 | const userBBalance = await myCollectible.balanceOf(userB, PREMIUM); 270 | await myFactory.safeTransferFrom( 271 | vals.ADDRESS_ZERO, 272 | userB, 273 | PREMIUM, 274 | 100, 275 | "0x0", 276 | { from: proxyForOwner } 277 | ); 278 | const newUserBBalance = await myCollectible.balanceOf(userB, PREMIUM); 279 | assert.isOk(newUserBBalance.eq(userBBalance.add(amount))); 280 | }); 281 | 282 | it('should not be callable by non-owner() and non-proxy', async () => { 283 | const amount = toBN(100); 284 | await truffleAssert.fails( 285 | myFactory.safeTransferFrom( 286 | vals.ADDRESS_ZERO, 287 | userB, 288 | PREMIUM, 289 | amount, 290 | "0x0", 291 | { from: userB } 292 | ), 293 | truffleAssert.ErrorType.revert, 294 | 'MyFactory#_mint: CANNOT_MINT_MORE' 295 | ); 296 | }); 297 | }); 298 | 299 | describe('#isApprovedForAll()', () => { 300 | it('should approve owner as both _owner and _operator', async () => { 301 | assert.isOk( 302 | await myFactory.isApprovedForAll(owner, owner) 303 | ); 304 | }); 305 | 306 | it('should not approve non-owner as _owner', async () => { 307 | assert.isNotOk( 308 | await myFactory.isApprovedForAll(userA, owner) 309 | ); 310 | assert.isNotOk( 311 | await myFactory.isApprovedForAll(userB, userA) 312 | ); 313 | }); 314 | 315 | it('should not approve non-proxy address as _operator', async () => { 316 | assert.isNotOk( 317 | await myFactory.isApprovedForAll(owner, userB) 318 | ); 319 | }); 320 | 321 | it('should approve proxy address as _operator', async () => { 322 | assert.isOk( 323 | await myFactory.isApprovedForAll(owner, proxyForOwner) 324 | ); 325 | }); 326 | 327 | it('should reject proxy as _operator for non-owner _owner', async () => { 328 | assert.isNotOk( 329 | await myFactory.isApprovedForAll(userA, proxyForOwner) 330 | ); 331 | }); 332 | }); 333 | 334 | /** 335 | * NOTE: This check is difficult to test in a development 336 | * environment, due to the OwnableDelegateProxy. To get around 337 | * this, in order to test this function below, you'll need to: 338 | * 339 | * 1. go to MyFactory.sol, and 340 | * 2. modify _isOwnerOrProxy 341 | * 342 | * --> Modification is: 343 | * comment out 344 | * return owner() == _address || address(proxyRegistry.proxies(owner())) == _address; 345 | * replace with 346 | * return true; 347 | * Then run, you'll get the reentrant error, which passes the test 348 | **/ 349 | 350 | describe('Re-Entrancy Check', () => { 351 | it('Should have the correct factory address set', 352 | async () => { 353 | assert.equal(await attacker.factoryAddress(), myFactory.address); 354 | }); 355 | 356 | // With unmodified code, this fails with: 357 | // MyFactory#_mint: CANNOT_MINT_MORE 358 | // which is the correct behavior (no reentrancy) for the wrong reason 359 | // (the attacker is not the owner or proxy). 360 | 361 | xit('Minting from factory should disallow re-entrancy attack', 362 | async () => { 363 | await truffleAssert.passes( 364 | myFactory.mint(1, userA, 1, "0x0", { from: owner }) 365 | ); 366 | await truffleAssert.passes( 367 | myFactory.mint(1, userA, 1, "0x0", { from: userA }) 368 | ); 369 | await truffleAssert.fails( 370 | myFactory.mint( 371 | 1, 372 | attacker.address, 373 | 1, 374 | "0x0", 375 | { from: attacker.address } 376 | ), 377 | truffleAssert.ErrorType.revert, 378 | 'ReentrancyGuard: reentrant call' 379 | ); 380 | }); 381 | }); 382 | }); 383 | -------------------------------------------------------------------------------- /test/MyLootBox.js: -------------------------------------------------------------------------------- 1 | /* libraries used */ 2 | 3 | const truffleAssert = require('truffle-assertions'); 4 | 5 | const vals = require('../lib/testValuesCommon.js'); 6 | 7 | /* Contracts in this test */ 8 | 9 | const MockProxyRegistry = artifacts.require( 10 | "../contracts/MockProxyRegistry.sol" 11 | ); 12 | const MyLootBox = artifacts.require("../contracts/MyLootBox.sol"); 13 | const MyCollectible = artifacts.require("../contracts/MyCollectible.sol"); 14 | 15 | 16 | /* Useful aliases */ 17 | 18 | const toBN = web3.utils.toBN; 19 | 20 | 21 | /* Utility Functions */ 22 | 23 | // Not a function, the fields of the TransferSingle event. 24 | 25 | const TRANSFER_SINGLE_FIELDS = [ 26 | { type: 'address', name: '_operator', indexed: true }, 27 | { type: 'address', name: '_from', indexed: true }, 28 | { type: 'address', name: '_to', indexed: true }, 29 | { type: 'uint256', name: '_id' }, 30 | { type: 'uint256', name: '_amount' } 31 | ]; 32 | 33 | // Not a function, the keccak of the TransferSingle event. 34 | 35 | const TRANSFER_SINGLE_SIG = web3.eth.abi.encodeEventSignature({ 36 | name: 'TransferSingle', 37 | type: 'event', 38 | inputs: TRANSFER_SINGLE_FIELDS 39 | }); 40 | 41 | // Check the option settings to make sure the values in the smart contract 42 | // match the expected ones. 43 | 44 | const checkOption = async ( 45 | myLootBox, index, maxQuantityPerOpen, hasGuaranteedClasses 46 | ) => { 47 | const option = await myLootBox.optionToSettings(index); 48 | assert.isOk(option.maxQuantityPerOpen.eq(toBN(maxQuantityPerOpen))); 49 | assert.equal(option.hasGuaranteedClasses, hasGuaranteedClasses); 50 | }; 51 | 52 | // Total the number of tokens in the transaction's emitted TransferSingle events 53 | // Keep a total for each token id number (1:..2:..) 54 | // and a total for *all* tokens as total:. 55 | 56 | const totalEventTokens = (receipt, recipient) => { 57 | // total is our running total for all tokens 58 | const totals = {total: toBN(0)}; 59 | // Parse each log from the event 60 | for (let i = 0; i < receipt.receipt.rawLogs.length; i++) { 61 | const raw = receipt.receipt.rawLogs[i]; 62 | // Filter events so we process only the TransferSingle events 63 | // Note that topic[0] is the event signature hash 64 | if (raw.topics[0] === TRANSFER_SINGLE_SIG) { 65 | // Fields of TransferSingle 66 | let parsed = web3.eth.abi.decodeLog( 67 | TRANSFER_SINGLE_FIELDS, 68 | raw.data, 69 | // Exclude event signature hash from topics that we process here. 70 | raw.topics.slice(1) 71 | ); 72 | // Make sure the correct recipient got the tokens. 73 | assert.equal(parsed._to, recipient); 74 | // Keep a running total for each token id. 75 | const id = parsed._id; 76 | if (! totals[id]) { 77 | totals[id] = toBN(0); 78 | } 79 | const amount = toBN(parsed._amount); 80 | totals[id] = totals[id].add(amount); 81 | // Keep a running total for all token ids. 82 | totals.total = totals.total.add(amount); 83 | } 84 | } 85 | return totals; 86 | }; 87 | 88 | // Compare the token amounts map generated by totalEventTokens to a spec object. 89 | // The spec should match the guarantees[] array for the option. 90 | 91 | const compareTokenTotals = (totals, spec, option) => { 92 | Object.keys(spec).forEach(key => { 93 | assert.isOk( 94 | // Because it's an Object.keys() value, key is a string. 95 | // We want that for the spec, as it is the correct key. 96 | // But to add one we want a number, so we parse it then add one. 97 | // Why do we want to add one? 98 | // Because due to the internals of the smart contract, the token id 99 | // will be one higher than the guarantees index. 100 | totals[parseInt(key) + 1] || toBN(0).gte(spec[key]), 101 | `Mismatch for option ${option} guarantees[${key}]` 102 | ); 103 | }); 104 | }; 105 | 106 | 107 | /* Tests */ 108 | 109 | contract("MyLootBox", (accounts) => { 110 | // As set in (or inferred from) the contract 111 | const BASIC = toBN(0); 112 | const PREMIUM = toBN(1); 113 | const GOLD = toBN(2); 114 | const OPTIONS = [BASIC, PREMIUM, GOLD]; 115 | const NUM_OPTIONS = OPTIONS.length; 116 | const NO_SUCH_OPTION = toBN(NUM_OPTIONS + 10); 117 | const OPTIONS_AMOUNTS = [toBN(3), toBN(5), toBN(7)]; 118 | const OPTION_GUARANTEES = [ 119 | {}, 120 | { 0: toBN(3) }, 121 | { 0: toBN(3), 2: toBN(2), 4: toBN(1) } 122 | ]; 123 | 124 | const owner = accounts[0]; 125 | const userA = accounts[1]; 126 | const userB = accounts[2]; 127 | const proxyForOwner = accounts[8]; 128 | 129 | let myLootBox; 130 | let myCollectible; 131 | let proxy; 132 | 133 | before(async () => { 134 | proxy = await MockProxyRegistry.new(); 135 | await proxy.setProxy(owner, proxyForOwner); 136 | myCollectible = await MyCollectible.new(proxy.address); 137 | myLootBox = await MyLootBox.new( 138 | proxy.address, 139 | myCollectible.address 140 | ); 141 | await myCollectible.transferOwnership(myLootBox.address); 142 | }); 143 | 144 | // This also tests the proxyRegistryAddress and nftAddress accessors. 145 | 146 | describe('#constructor()', () => { 147 | it('should set proxyRegistryAddress to the supplied value', async () => { 148 | assert.equal(await myLootBox.proxyRegistryAddress(), proxy.address); 149 | assert.equal(await myLootBox.nftAddress(), myCollectible.address); 150 | }); 151 | 152 | it('should set options to values in constructor', async () => { 153 | await checkOption(myLootBox, BASIC, 3, false); 154 | await checkOption(myLootBox, PREMIUM, 5, true); 155 | await checkOption(myLootBox, GOLD, 7, true); 156 | }); 157 | }); 158 | 159 | // Calls _mint() 160 | 161 | describe('#safeTransferFrom()', () => { 162 | it('should work for owner()', async () => { 163 | const option = BASIC; 164 | const amount = toBN(1); 165 | const receipt = await myLootBox.safeTransferFrom( 166 | vals.ADDRESS_ZERO, 167 | userB, 168 | option, 169 | amount, 170 | "0x0", 171 | { from: owner } 172 | ); 173 | truffleAssert.eventEmitted( 174 | receipt, 175 | 'LootBoxOpened', 176 | { 177 | boxesPurchased: amount, 178 | optionId: option, 179 | buyer: userB, 180 | itemsMinted: OPTIONS_AMOUNTS[option] 181 | } 182 | ); 183 | const totals = totalEventTokens(receipt, userB); 184 | assert.ok(totals.total.eq(OPTIONS_AMOUNTS[option])); 185 | }); 186 | 187 | it('should work for proxy', async () => { 188 | const option = BASIC; 189 | const amount = toBN(1); 190 | const receipt = await myLootBox.safeTransferFrom( 191 | vals.ADDRESS_ZERO, 192 | userB, 193 | option, 194 | amount, 195 | "0x0", 196 | { from: proxyForOwner } 197 | ); 198 | truffleAssert.eventEmitted( 199 | receipt, 200 | 'LootBoxOpened', 201 | { 202 | boxesPurchased: amount, 203 | optionId: option, 204 | buyer: userB, 205 | itemsMinted: OPTIONS_AMOUNTS[option] 206 | } 207 | ); 208 | const totals = totalEventTokens(receipt, userB); 209 | assert.ok(totals.total.eq(OPTIONS_AMOUNTS[option])); 210 | }); 211 | 212 | it('should not be callable by non-owner() and non-proxy', async () => { 213 | const amount = toBN(1); 214 | await truffleAssert.fails( 215 | myLootBox.safeTransferFrom( 216 | vals.ADDRESS_ZERO, 217 | userB, 218 | PREMIUM, 219 | amount, 220 | "0x0", 221 | { from: userB } 222 | ), 223 | truffleAssert.ErrorType.REVERT, 224 | 'MyLootBox#_mint: CANNOT_MINT' 225 | ); 226 | }); 227 | 228 | it('should not work for invalid option', async () => { 229 | const amount = toBN(1); 230 | await truffleAssert.fails( 231 | myLootBox.safeTransferFrom( 232 | vals.ADDRESS_ZERO, 233 | userB, 234 | NO_SUCH_OPTION, 235 | amount, 236 | "0x0", 237 | { from: owner } 238 | ), 239 | // The bad Option cast gives an invalid opcode exception. 240 | truffleAssert.ErrorType.INVALID_OPCODE 241 | ); 242 | }); 243 | 244 | it('should mint guaranteed class amounts for each option', async () => { 245 | for (let i = 0; i < NUM_OPTIONS; i++) { 246 | const option = OPTIONS[i]; 247 | const amount = toBN(1); 248 | const receipt = await myLootBox.safeTransferFrom( 249 | vals.ADDRESS_ZERO, 250 | userB, 251 | option, 252 | amount, 253 | "0x0", 254 | { from: owner } 255 | ); 256 | truffleAssert.eventEmitted( 257 | receipt, 258 | 'LootBoxOpened', 259 | { 260 | boxesPurchased: amount, 261 | optionId: option, 262 | buyer: userB, 263 | itemsMinted: OPTIONS_AMOUNTS[option] 264 | } 265 | ); 266 | const totals = totalEventTokens(receipt, userB); 267 | assert.ok(totals.total.eq(OPTIONS_AMOUNTS[option])); 268 | compareTokenTotals(totals, OPTION_GUARANTEES[option], option); 269 | } 270 | }); 271 | }); 272 | }); 273 | -------------------------------------------------------------------------------- /truffle-config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this file to configure your truffle project. It's seeded with some 3 | * common settings for different networks and features like migrations, 4 | * compilation and testing. Uncomment the ones you need or modify 5 | * them to suit your project as necessary. 6 | * 7 | * More information about configuration can be found at: 8 | * 9 | * truffleframework.com/docs/advanced/configuration 10 | * 11 | * To deploy via Infura you'll need a wallet provider (like truffle-hdwallet-provider) 12 | * to sign your transactions before they're sent to a remote public node. Infura accounts 13 | * are available for free at: infura.io/register. 14 | * 15 | * You'll also need a mnemonic - the twelve word phrase the wallet uses to generate 16 | * public/private key pairs. If you're publishing your code to GitHub make sure you load this 17 | * phrase from a file you've .gitignored so it doesn't accidentally become public. 18 | * 19 | */ 20 | 21 | const HDWalletProvider = require('truffle-hdwallet-provider'); 22 | const MNEMONIC = process.env.MNEMONIC; 23 | const INFURA_KEY = process.env.INFURA_KEY; 24 | 25 | const needsInfura = process.env.npm_config_argv && 26 | (process.env.npm_config_argv.includes('rinkeby') || 27 | process.env.npm_config_argv.includes('live')); 28 | 29 | if ((!MNEMONIC || !INFURA_KEY) && needsInfura) { 30 | console.error('Please set a mnemonic and infura key.'); 31 | process.exit(0); 32 | } 33 | 34 | module.exports = { 35 | /** 36 | * Networks define how you connect to your ethereum client and let you set the 37 | * defaults web3 uses to send transactions. If you don't specify one truffle 38 | * will spin up a development blockchain for you on port 9545 when you 39 | * run `develop` or `test`. You can ask a truffle command to use a specific 40 | * network from the command line, e.g 41 | * 42 | * $ truffle test --network 43 | */ 44 | 45 | networks: { 46 | 47 | // Useful for testing. The `development` name is special - truffle uses it by default 48 | // if it's defined here and no other network is specified at the command line. 49 | // You should run a client (like ganache-cli, geth or parity) in a separate terminal 50 | // tab if you use this network and you must also set the `host`, `port` and `network_id` 51 | // options below to some value. 52 | development: { 53 | host: 'localhost', 54 | port: 7545, 55 | gas: 4600000, 56 | network_id: '*' // Match any network id 57 | }, 58 | 59 | rinkeby: { 60 | provider: function() { 61 | return new HDWalletProvider( 62 | MNEMONIC, 63 | "https://rinkeby.infura.io/v3/" + INFURA_KEY 64 | ); 65 | }, 66 | network_id: "*", 67 | gas: 4600000 68 | }, 69 | 70 | live: { 71 | network_id: 1, 72 | provider: function() { 73 | return new HDWalletProvider( 74 | MNEMONIC, 75 | "https://mainnet.infura.io/v3/" + INFURA_KEY 76 | ); 77 | }, 78 | gas: 4000000, 79 | gasPrice: 20000000000 80 | } 81 | }, 82 | 83 | // Set default mocha options here, use special reporters etc. 84 | mocha: { 85 | reporter: 'eth-gas-reporter', 86 | reporterOptions : { 87 | currency: 'USD', 88 | gasPrice: 2 89 | } 90 | }, 91 | 92 | // Configure your compilers 93 | compilers: { 94 | solc: { 95 | version: "0.5.12" 96 | // docker: true, // Use "0.5.1" you've installed locally with docker (default: false) 97 | // settings: { // See the solidity docs for advice about optimization and evmVersion 98 | // optimizer: { 99 | // enabled: false, 100 | // runs: 200 101 | // }, 102 | // evmVersion: "byzantium" 103 | // } 104 | } 105 | } 106 | }; 107 | --------------------------------------------------------------------------------