├── erc1155 ├── images │ ├── Mage_Wand.png │ ├── Fighter_Sword.png │ └── Fighter_Shield.png └── metadata.json ├── .env.example ├── docs ├── images │ └── rinkeby-opensea.png └── opensea-erc1155-specs.md ├── migrations ├── 1_initial_migration.js └── 2_deploy_contracts.js ├── .gitignore ├── contracts ├── Counter.sol ├── Migrations.sol ├── test │ ├── MockProxyRegistry.sol │ └── TestForReentrancyAttack.sol ├── ERC1155Opensea.sol ├── Strings.sol ├── IFactory.sol ├── ILootBox.sol ├── SafeMath.sol ├── MyFactory.sol ├── ERC1155Tradable.sol ├── Tournaments.sol ├── ERC721CryptoPizza.sol └── MyLootBox.sol ├── test ├── ERC721CryptoPizza.test.js ├── ERC1155Opensea.test.js └── Counter.test.js ├── LICENSE ├── package.json ├── scripts ├── advanced │ └── mint.js ├── initial_sale.js ├── sell.js └── erc1155_deploy_to_ipfs.js ├── README.md └── truffle-config.js /erc1155/images/Mage_Wand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alto-io/contractor/HEAD/erc1155/images/Mage_Wand.png -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | GANACHE_MNEMONIC= 2 | TESTNET_MNEMONIC= 3 | INFURA_API_KEY= 4 | PINATA_API_KEY= 5 | PINATA_SECRET_API_KEY= -------------------------------------------------------------------------------- /docs/images/rinkeby-opensea.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alto-io/contractor/HEAD/docs/images/rinkeby-opensea.png -------------------------------------------------------------------------------- /erc1155/images/Fighter_Sword.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alto-io/contractor/HEAD/erc1155/images/Fighter_Sword.png -------------------------------------------------------------------------------- /erc1155/images/Fighter_Shield.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alto-io/contractor/HEAD/erc1155/images/Fighter_Shield.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | **/node_modules 3 | /.pnp 4 | .pnp.js 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | **/build 11 | **/dist 12 | 13 | # misc 14 | .env 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | packages/client/src/contracts 21 | packages/server/ipfs 22 | packages/server/orbitdb 23 | packages/server/report.*.json 24 | **/temp_metadata 25 | 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | .vscode/ 30 | -------------------------------------------------------------------------------- /contracts/Counter.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.11; 2 | 3 | contract Counter { 4 | 5 | uint256 counter; 6 | 7 | function decrementCounter() public returns (uint256) { 8 | counter = counter - 1; 9 | return counter; 10 | } 11 | 12 | function incrementCounter() public returns (uint256) { 13 | counter = counter + 1; 14 | return counter; 15 | } 16 | 17 | function reset() public { 18 | counter = 0; 19 | } 20 | 21 | function getCounter() public view returns (uint256){ 22 | return counter; 23 | } 24 | } -------------------------------------------------------------------------------- /test/ERC721CryptoPizza.test.js: -------------------------------------------------------------------------------- 1 | var CryptoPizza = artifacts.require('./ERC721CryptoPizza.sol') 2 | let gasPrice = 1000000000 // 1GWEI 3 | 4 | let _ = ' ' 5 | 6 | contract('CryptoPizza', accounts => { 7 | it("first account should have 0 balance", () => 8 | CryptoPizza.deployed() 9 | .then(instance => instance.getPizzasByOwner.call(accounts[0])) 10 | .then(balance => { 11 | assert.equal( 12 | balance.valueOf(), 13 | 0, 14 | "first account had non-zero balance" 15 | ) 16 | }) 17 | ) 18 | }) -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /test/ERC1155Opensea.test.js: -------------------------------------------------------------------------------- 1 | /* Contracts in this test */ 2 | 3 | const ERC1155Opensea = artifacts.require("../contracts/ERC1155Opensea.sol"); 4 | const contractDetails = require('../temp_metadata/contracturi.json'); 5 | const contractConfig = require('../temp_metadata/erc1155config.json'); 6 | let _ = ' ' 7 | 8 | contract("ERC1155Opensea", (accounts) => { 9 | const CONTRACT_URI = contractConfig.gatewayUrl + "/" + contractConfig.contractUriHash 10 | 11 | let myCollectible; 12 | 13 | before(async () => { 14 | myCollectible = await ERC1155Opensea.deployed(); 15 | }); 16 | 17 | // This is all we test for now 18 | 19 | // This also tests contractURI() 20 | 21 | describe('#constructor()', () => { 22 | it('should set the contractURI to the supplied value', async () => { 23 | let uri = await myCollectible.contractURI(); 24 | console.log(_ + "uri: " + uri) 25 | assert.equal(uri, CONTRACT_URI); 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /contracts/ERC1155Opensea.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 ERC1155Opensea is ERC1155Tradable { 10 | 11 | string internal openseaContractUri; 12 | 13 | constructor(address _proxyRegistryAddress, string memory _contractUri, string memory _metadataUri, string memory _name, string memory _symbol) 14 | ERC1155Tradable( 15 | _name, 16 | _symbol, 17 | _proxyRegistryAddress 18 | ) public { 19 | _setBaseMetadataURI(_metadataUri); 20 | _setContractURI(_contractUri); 21 | } 22 | 23 | /** 24 | * @notice Will update the base URL of token's URI 25 | * @param _newContractiURI New base URL of token's URI 26 | */ 27 | function _setContractURI(string memory _newContractiURI) internal { 28 | openseaContractUri = _newContractiURI; 29 | } 30 | 31 | function contractURI() public view returns (string memory) { 32 | return openseaContractUri; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2020 Paul Gadi 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/Counter.test.js: -------------------------------------------------------------------------------- 1 | var Counter = artifacts.require('./Counter.sol') 2 | var Tournaments = artifacts.require('./Tournaments.sol') 3 | 4 | contract('Counter & Tournaments', accounts => { //moved them to a single file to fix weird ganache nonce issue 5 | 6 | let instance; 7 | beforeEach(async () => { 8 | instance = await Counter.new(); 9 | tourneyInstance = await Tournaments.new(); 10 | }); 11 | 12 | 13 | it("tournament creation return a tournament id", () => 14 | tourneyInstance.createTournament(accounts[0], Date.now(), "TEST", 3) 15 | .then(tx => { 16 | 17 | assert.notEqual( 18 | tx.logs[0].args.tournamentId, 19 | undefined, 20 | "tourney creation returns undefined" 21 | ) 22 | }) 23 | ) 24 | 25 | it("Counter is 0", () => 26 | instance.getCounter.call() 27 | .then(counter => { 28 | assert.equal(counter, 0, "counter is non-zero") 29 | } 30 | ) 31 | ) 32 | 33 | it("Counter increments when incrementCounter is called", () => 34 | instance => instance.incrementCounter.call() 35 | .then(counter => { 36 | assert.equal(counter, 1, "counter is not 1") 37 | } 38 | ) 39 | ) 40 | 41 | it("Counter decrements when decrementCounter is called", () => 42 | instance => instance.decrementCounter.call() 43 | .then(counter => { 44 | assert.equal(counter, 0, "counter did not go back to 0") 45 | } 46 | ) 47 | ) 48 | 49 | }) -------------------------------------------------------------------------------- /docs/opensea-erc1155-specs.md: -------------------------------------------------------------------------------- 1 | ### ERC1155 Token 2 | 3 | **Contract Uri**: Shows the token info on Opensea 4 | 5 | **example**: https://creatures-api.opensea.io/contract/opensea-erc1155 6 | 7 | 8 | ``` 9 | { 10 | "description": "Fun and useful accessories for your OpenSea creatures.", 11 | "external_link": "https://github.com/ProjectOpenSea/opensea-erc1155/", <-- link to Website button in opensea webpage 12 | "image": "https://example.com/image.png", 13 | "name": "OpenSea Creature Accessories" 14 | } 15 | ``` 16 | 17 | **BaseMetadataUri:** Gets metadata given a token id 18 | 19 | **Base:** https://creatures-api.opensea.io/api/creature/ 20 | 21 | **example token:** https://creatures-api.opensea.io/api/creature/1 22 | 23 | ``` 24 | { 25 | "attributes": [ 26 | { 27 | "trait_type": "Base", 28 | "value": "starfish" 29 | }, 30 | { 31 | "trait_type": "Eyes", 32 | "value": "joy" 33 | }, 34 | { 35 | "trait_type": "Mouth", 36 | "value": "surprised" 37 | }, 38 | { 39 | "trait_type": "Level", 40 | "value": 2 41 | }, 42 | { 43 | "trait_type": "Stamina", 44 | "value": 2.3 45 | }, 46 | { 47 | "trait_type": "Personality", 48 | "value": "Sad" 49 | }, 50 | { 51 | "display_type": "boost_number", 52 | "trait_type": "Aqua Power", 53 | "value": 40 54 | }, 55 | { 56 | "display_type": "boost_percentage", 57 | "trait_type": "Stamina Increase", 58 | "value": 10 59 | }, 60 | { 61 | "display_type": "number", 62 | "trait_type": "Generation", 63 | "value": 2 64 | } 65 | ], 66 | "description": "Friendly OpenSea Creature that enjoys long swims in the ocean.", 67 | "external_url": "https://openseacreatures.io/1", 68 | "image": "https://storage.googleapis.com/opensea-prod.appspot.com/creature/1.png", 69 | "name": "Sprinkles Fisherton" 70 | } 71 | ``` 72 | -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Contractor", 3 | "version": "0.0.1", 4 | "private": true, 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/alto-io/contractor" 8 | }, 9 | "author": { 10 | "name": "Paul Gadi", 11 | "url": "https://outplay.games" 12 | }, 13 | "workspaces": { 14 | "packages": [ 15 | "packages/*" 16 | ] 17 | }, 18 | "engines": { 19 | "node": ">=0.12" 20 | }, 21 | "scripts": { 22 | "demo": "yarn ipfs:deploy:erc1155 && yarn sol:deploy:rinkeby:reset && yarn mint:erc1155", 23 | "ipfs:deploy:erc1155": "node scripts/erc1155_deploy_to_ipfs.js", 24 | "sol:deploy": "truffle migrate --reset --compile-all", 25 | "sol:deploy:rinkeby": "truffle migrate --network rinkeby", 26 | "sol:deploy:rinkeby:reset": "truffle migrate --network rinkeby --reset --compile-all", 27 | "mint:erc1155": "node scripts/advanced/mint.js", 28 | "sol:test": "truffle test", 29 | "clean:dependencies": "rimraf ./**/node_modules", 30 | "clean:builds": "rimraf ./**/build", 31 | "clean": "concurrently --kill-others-on-fail \"yarn clean:dependencies\" \"yarn clean:builds\"", 32 | "lint": "yarn workspace @contractor/client lint; yarn workspace @contractor/server lint", 33 | "lint:fix": "yarn workspace @contractor/client lint:fix; yarn workspace @contractor/server lint:fix", 34 | "start:common": "yarn workspace @contractor/common start", 35 | "start:client": "yarn workspace @contractor/client start", 36 | "start:server": "yarn workspace @contractor/server start", 37 | "start": "yarn build:common && concurrently --kill-others-on-fail \"yarn start:common\" \"yarn start:client\" \"yarn start:server\"", 38 | "build:common": "yarn workspace @contractor/common build", 39 | "build:client": "yarn workspace @contractor/client build", 40 | "build:server": "yarn workspace @contractor/server build", 41 | "build:move": "mv ./packages/client/build ./packages/server/build/public", 42 | "build": "yarn build:common && yarn build:client && yarn build:server && yarn build:move", 43 | "serve": "yarn workspace @contractor/server serve", 44 | "postinstall": "rimraf node_modules/@types/react-native" 45 | }, 46 | "dependencies": { 47 | "@openzeppelin/contracts": "2.5.0", 48 | "axios": "^0.19.2", 49 | "base-path-converter": "^1.0.2", 50 | "concurrently": "^4.1.1", 51 | "dotenv": "^8.2.0", 52 | "form-data": "^3.0.0", 53 | "multi-token-standard": "github:ProjectOpenSea/multi-token-standard", 54 | "openzeppelin-solidity": "2.1.3", 55 | "recursive-fs": "^2.1.0", 56 | "truffle": "^5.0.2", 57 | "truffle-flattener": "^1.3.0", 58 | "truffle-hdwallet-provider": "^1.0.17", 59 | "tslint": "^5.19.0" 60 | }, 61 | "resolutions": { 62 | "cids/multicodec": "1.0.1", 63 | "ipfs-bitswap/multicodec": "0.5.5", 64 | "**/redis": "3.0.2" 65 | }, 66 | "devDependencies": { 67 | "rimraf": "^2.6.2", 68 | "typescript": "^3.5.3" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /scripts/advanced/mint.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | 3 | const deployInfo = require('../../temp_metadata/deployinfo.json'); 4 | 5 | const HDWalletProvider = require("truffle-hdwallet-provider") 6 | const web3 = require('web3') 7 | const INFURA_KEY = process.env.INFURA_API_KEY 8 | const FACTORY_CONTRACT_ADDRESS = deployInfo.FactoryAddress 9 | const OWNER_ADDRESS = deployInfo.ownerAddress 10 | const NETWORK = deployInfo.network 11 | 12 | 13 | let MNEMONIC; 14 | let RPC_URL; 15 | 16 | if (NETWORK == 'ganache') 17 | { 18 | MNEMONIC = process.env.GANACHE_MNEMONIC 19 | RPC_URL = "http://localhost:7545/"; 20 | } 21 | 22 | else 23 | { 24 | MNEMONIC = process.env.TESTNET_MNEMONIC 25 | RPC_URL = `https://${NETWORK}.infura.io/v3/${INFURA_KEY}`; 26 | } 27 | 28 | if (!MNEMONIC || !INFURA_KEY || !OWNER_ADDRESS || !NETWORK) { 29 | console.error("Please set a mnemonic, infura key, owner, network, and contract address.") 30 | return 31 | } 32 | 33 | const FACTORY_ABI = [ 34 | { 35 | "constant": false, 36 | "inputs": [ 37 | { 38 | "internalType": "uint256", 39 | "name": "_optionId", 40 | "type": "uint256" 41 | }, 42 | { 43 | "internalType": "address", 44 | "name": "_toAddress", 45 | "type": "address" 46 | }, 47 | { 48 | "internalType": "uint256", 49 | "name": "_amount", 50 | "type": "uint256" 51 | } 52 | ], 53 | "name": "open", 54 | "outputs": [], 55 | "payable": false, 56 | "stateMutability": "nonpayable", 57 | "type": "function" 58 | }, 59 | { 60 | "constant": false, 61 | "inputs": [ 62 | { 63 | "internalType": "address", 64 | "name": "_toAddress", 65 | "type": "address" 66 | }, 67 | ], 68 | "name": "contractorTestMint", 69 | "outputs": [], 70 | "payable": false, 71 | "stateMutability": "nonpayable", 72 | "type": "function" 73 | } 74 | ] 75 | 76 | /** 77 | * For now, this script just opens a lootbox. 78 | */ 79 | async function main() { 80 | const provider = new HDWalletProvider(MNEMONIC, RPC_URL); 81 | const web3Instance = new web3(provider); 82 | 83 | if (!FACTORY_CONTRACT_ADDRESS) { 84 | console.error("Please set an NFT contract address.") 85 | return 86 | } 87 | 88 | const factoryContract = new web3Instance.eth.Contract(FACTORY_ABI, FACTORY_CONTRACT_ADDRESS, { gasLimit: "1000000" }) 89 | 90 | // const result = await factoryContract.methods.open(0, OWNER_ADDRESS, 1).send({ from: OWNER_ADDRESS }); 91 | 92 | const result = await factoryContract.methods.contractorTestMint(OWNER_ADDRESS).send({ from: OWNER_ADDRESS }); 93 | console.log("Created. Transaction: " + result.transactionHash) 94 | console.log("-------------------") 95 | console.log("Mint Finished, press Ctrl+C to exit.") 96 | 97 | } 98 | 99 | main() 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🕴 The CONTRACTOR 2 | 3 | Here to help you create your smart contracts! 4 | 5 | The CONTRACTOR can help you: 6 | 7 | * Mint an ERC1155 on Opensea in just 3 easy steps 8 | * ... and more! 9 | 10 | ## Required software and accounts 11 | 12 | * [Node.js](https://nodejs.org/en/download/) 13 | * [Yarn](https://yarnpkg.com/getting-started/install) 14 | * [Infura](https://infura.io/) 15 | * [Pinata](https://pinata.cloud/) 16 | 17 | #### [Optional] For Local Testing: 18 | * [Ganache](https://www.trufflesuite.com/ganache) 19 | 20 | ---- 21 | ## 🚀 Getting Started With ERC1155 Example 22 | 23 | ### 1. Fork the repository 24 | 25 | Click here to fork and create your own project. 26 | * [![Fork Contractor](https://img.shields.io/github/forks/alto-io/contractor.svg?style=social&label=Fork%20contractor&maxAge=2592000)](https://GitHub.com/alto-io/contractor/fork) 27 | 28 | ### 2. Create an .env file with the correct mnemonic and API keys 29 | 30 | Inside the project directory, create a .env file containing the mnemonics and API keys. You can copy the [env.example](./.env.example). 31 | ``` 32 | GANACHE_MNEMONIC=[not needed for demo] 33 | TESTNET_MNEMONIC=[mnemonic for wallet with about 2 Rinkeby ETH] 34 | INFURA_API_KEY=[yourInfuraKey] 35 | PINATA_API_KEY=[yourPinataAPIKey] 36 | PINATA_SECRET_API_KEY=[yourPinataSecretAPIKey] 37 | ``` 38 | 39 | ### 3. Have 🕴 The CONTRACTOR create your contracts and mint your items 40 | 41 | * run `yarn demo` inside project directory, then wait a bit ⌛ The script will take a while to deploy the contracts on Rinkeby testnet. 42 | * Once finished, the logs will display the contract details. Copy the `FactoryAddress` to your clipboard 43 | * In a browser, go to [https://rinkeby.opensea.io/get-listed/step-two](https://rinkeby.opensea.io/get-listed/step-two) and paste the contract address. 44 | * *That's it!* See the items you created on Opensea 45 | 46 | ![Rinkeby Opensea Image](./docs/images/rinkeby-opensea.png) 47 | 48 | ---- 49 | 50 | ## Creating Your Own Item Specs 51 | 52 | * Put all images inside [erc155/images](./erc1155/images) 53 | * Define the metadata and contract URI in [erc1155/metadata.json](./erc1155/metadata.json) 54 | 55 | ## Minting Items 56 | 57 | The demo mint parameters are defined in [MyLootBox.sol](./contracts/MyLootBox.sol#L186-L194), which pre-mints 1, 5, and 10 copies of the first 3 tokens respectively to the owner account. 58 | 59 | This is a custom function based on the Opensea Factory implementation of ERC1155, [more info on how to customize it can be found here](https://docs.opensea.io/docs/opensea-erc1155-tutorial). 60 | 61 | ## Local Testing 62 | 63 | For quicker development, you can use [Ganache](https://www.trufflesuite.com/ganache) for local smart contract deployment and testing. 64 | 65 | * `yarn sol:deploy` deploys the smart contracts to ganache 66 | * `GANACHE_MNEMONIC` should be defined in the `.env` file 67 | * Make sure Ganache uses http://localhost:7545/ as the rpc url as specified in [truffle-config.js](./truffle-config.js) 68 | * `yarn sol:test` runs tests in the [test directory](./test) 69 | 70 | ## Licenses 71 | 72 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg?style=for-the-badge)](https://github.com/alto-io/contractor/blob/master/LICENSE) -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /truffle-config.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | const HDWalletProvider = require("truffle-hdwallet-provider"); 3 | const path = require("path"); 4 | 5 | module.exports = { 6 | contracts_build_directory: path.join(__dirname, "packages/client/src/contracts"), 7 | networks: { 8 | develop: { 9 | provider() { 10 | return new HDWalletProvider( 11 | process.env.GANACHE_MNEMONIC, 12 | "http://localhost:7545/" 13 | ); 14 | }, 15 | host: "localhost", 16 | port: 7545, 17 | network_id: 5777, 18 | gas: 6721975, 19 | gasPrice: 1000000000 20 | }, 21 | ganache: { 22 | provider() { 23 | return new HDWalletProvider( 24 | process.env.GANACHE_MNEMONIC, 25 | "http://localhost:7545/" 26 | ); 27 | }, 28 | host: "localhost", 29 | port: 7545, 30 | network_id: 5777, 31 | gas: 6721975, 32 | gasPrice: 1000000000 33 | }, 34 | test: { 35 | provider() { 36 | return new HDWalletProvider( 37 | process.env.GANACHE_MNEMONIC, 38 | "http://localhost:7545/" 39 | ); 40 | }, 41 | host: "localhost", 42 | port: 7545, 43 | network_id: 5777, 44 | gas: 6721975, 45 | gasPrice: 1000000000 46 | }, 47 | mainnet: { 48 | provider() { 49 | // using wallet at index 1 ----------------------------------------------------------------------------------------v 50 | return new HDWalletProvider( 51 | process.env.TESTNET_MNEMONIC, 52 | "https://mainnet.infura.io/v3/" + process.env.INFURA_API_KEY, 53 | 1 54 | ); 55 | }, 56 | network_id: 1 57 | // gas: 5561260 58 | }, 59 | kovan: { 60 | provider() { 61 | // using wallet at index 1 ----------------------------------------------------------------------------------------v 62 | return new HDWalletProvider( 63 | process.env.TESTNET_MNEMONIC, 64 | "https://kovan.infura.io/v3/" + process.env.INFURA_API_KEY, 65 | 1 66 | ); 67 | }, 68 | network_id: 42 69 | // gas: 5561260 70 | }, 71 | rinkeby: { 72 | provider() { 73 | return new HDWalletProvider( 74 | process.env.TESTNET_MNEMONIC, 75 | "https://rinkeby.infura.io/v3/" + process.env.INFURA_API_KEY 76 | ); 77 | }, 78 | network_id: 4, 79 | // gas: 4700000, 80 | gasPrice: 200000000000 // 200 GWEI 81 | }, 82 | ropsten: { 83 | provider() { 84 | return new HDWalletProvider( 85 | process.env.TESTNET_MNEMONIC, 86 | "https://ropsten.infura.io/v3/" + process.env.INFURA_API_KEY 87 | ); 88 | }, 89 | network_id: 2 90 | // gas: 4700000 91 | }, 92 | sokol: { 93 | provider() { 94 | return new HDWalletProvider( 95 | process.env.TESTNET_MNEMONIC, 96 | "https://sokol.poa.network" 97 | ); 98 | }, 99 | gasPrice: 1000000000, 100 | network_id: 77 101 | }, 102 | poa: { 103 | provider() { 104 | return new HDWalletProvider( 105 | process.env.TESTNET_MNEMONIC, 106 | "https://core.poa.network" 107 | ); 108 | }, 109 | gasPrice: 1000000000, 110 | network_id: 99 111 | } 112 | }, 113 | compilers: { 114 | solc: { 115 | version: "0.5.12" 116 | } 117 | } 118 | }; 119 | -------------------------------------------------------------------------------- /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() -------------------------------------------------------------------------------- /erc1155/metadata.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "contractUri": 4 | { 5 | "description": "Items created by The CONTRACTOR.", 6 | "external_link": "https://github.com/alto-io/contractor/", 7 | "image": "https://raw.githubusercontent.com/cj1128/emoji-images/master/imgs/1f574.png", 8 | "name": "Contractor ERC1155 Items", 9 | "symbol": "ERC1155-CTR" 10 | }, 11 | "metadata": [ 12 | { 13 | "id": "1", 14 | "name": "Fighter Sword", 15 | "description": "For poking.", 16 | "image": "Fighter_Sword.png", 17 | "attributes": [ 18 | { 19 | "trait_type": "Base", 20 | "value": "Sword" 21 | }, 22 | { 23 | "trait_type": "Attack", 24 | "value": 10 25 | }, 26 | { 27 | "trait_type": "Speed", 28 | "value": 0.5 29 | }, 30 | { 31 | "display_type": "boost_number", 32 | "trait_type": "Enchantment", 33 | "value": 40 34 | }, 35 | { 36 | "display_type": "boost_percentage", 37 | "trait_type": "Chance to Hit", 38 | "value": 50 39 | }, 40 | { 41 | "display_type": "number", 42 | "trait_type": "Generation", 43 | "value": 1 44 | } 45 | ] 46 | }, 47 | { 48 | "id": "2", 49 | "name": "Fighter Shield", 50 | "description": "For blocking. ", 51 | "image": "Fighter_Shield.png", 52 | "attributes": [ 53 | { 54 | "trait_type": "Base", 55 | "value": "Shield" 56 | }, 57 | { 58 | "trait_type": "Defense", 59 | "value": 10 60 | }, 61 | { 62 | "trait_type": "Armor", 63 | "value": 0.5 64 | }, 65 | { 66 | "display_type": "boost_number", 67 | "trait_type": "Enchantment", 68 | "value": 20 69 | }, 70 | { 71 | "display_type": "boost_percentage", 72 | "trait_type": "Chance to Block", 73 | "value": 30 74 | }, 75 | { 76 | "display_type": "number", 77 | "trait_type": "Generation", 78 | "value": 1 79 | } 80 | ] 81 | }, 82 | { 83 | "id": "3", 84 | "name": "Mage Wand", 85 | "description": "For casting.", 86 | "image": "Mage_Wand.png", 87 | "attributes": [ 88 | { 89 | "trait_type": "Base", 90 | "value": "Sword" 91 | }, 92 | { 93 | "trait_type": "Magic", 94 | "value": 3 95 | }, 96 | { 97 | "trait_type": "Cooldown", 98 | "value": 0.2 99 | }, 100 | { 101 | "display_type": "boost_number", 102 | "trait_type": "Enchantment", 103 | "value": 20 104 | }, 105 | { 106 | "display_type": "boost_percentage", 107 | "trait_type": "Chance to Hit", 108 | "value": 40 109 | }, 110 | { 111 | "display_type": "number", 112 | "trait_type": "Generation", 113 | "value": 1 114 | } 115 | ] 116 | } 117 | 118 | 119 | ] 120 | 121 | 122 | 123 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /migrations/2_deploy_contracts.js: -------------------------------------------------------------------------------- 1 | const ERC1155Opensea = artifacts.require("ERC1155Opensea"); 2 | const MyLootBox = artifacts.require("MyLootBox"); 3 | const fs = require('fs'); 4 | 5 | 6 | // const ERC721CryptoPizza = artifacts.require("ERC721CryptoPizza") 7 | // const Tournament = artifacts.require("Tournaments"); 8 | // const Counter = artifacts.require("Counter"); 9 | 10 | // Set to false if you only want the collectible to deploy 11 | const ENABLE_LOOTBOX = true; 12 | // Set if you want to create your own collectible 13 | const NFT_ADDRESS_TO_USE = undefined; // e.g. Enjin: '0xfaafdc07907ff5120a76b34b731b278c38d6043c' 14 | // If you want to set preminted token ids for specific classes 15 | const TOKEN_ID_MAPPING = undefined; // { [key: number]: Array<[tokenId: string]> } 16 | 17 | let ownerAddress; 18 | let deployNetwork; 19 | let _ = ' ' 20 | 21 | module.exports = function(deployer, network, accounts) { 22 | 23 | // OpenSea proxy registry addresses for rinkeby and mainnet. 24 | let proxyRegistryAddress; 25 | if (network === 'rinkeby') { 26 | proxyRegistryAddress = "0xf57b2c51ded3a29e6891aba85459d600256cf317"; 27 | } else { 28 | proxyRegistryAddress = "0xa5409ec958c83c3f309868babaca7c86dcb077c1"; 29 | } 30 | 31 | // get token details 32 | const erc1155config = require('../temp_metadata/erc1155config.json'); 33 | const contractconfig = require('../temp_metadata/contracturi.json') 34 | const baseMetadataUri = erc1155config.gatewayUrl + "/" + erc1155config.metadataHash + "/"; 35 | const contractUri = erc1155config.gatewayUrl + "/" + erc1155config.contractUriHash; 36 | const name = contractconfig.name; 37 | const symbol = contractconfig.symbol; 38 | ownerAddress = accounts[0]; 39 | deployNetwork = network; 40 | 41 | console.log("baseMetadataUri : " + baseMetadataUri) 42 | console.log("contractUri : " + contractUri) 43 | console.log("ownerAddress : " + ownerAddress) 44 | 45 | // In progress: non-opensea contracts 46 | /* 47 | deployer.then(async () => { 48 | try { 49 | 50 | // Deploy CryptoPizza.sol 51 | await deployer.deploy(ERC721CryptoPizza, baseMetadataUri); 52 | let contract = await ERC721CryptoPizza.deployed(); 53 | console.log( 54 | _ + "ERC721CryptoPizza deployed at: " + contract.address 55 | ); 56 | 57 | // Deploy Token.sol 58 | await deployer.deploy(Token); 59 | let token = await Token.deployed(); 60 | console.log( 61 | _ + "Token deployed at: " + token.address 62 | ); 63 | 64 | // Deploy Tournament.sol 65 | await deployer.deploy(Tournament); 66 | let tournament = await Tournament.deployed(); 67 | console.log( 68 | _ + "Tournament deployed at: " + tournament.address 69 | ); 70 | 71 | // Deploy Counter.sol 72 | await deployer.deploy(Counter); 73 | let counter = await Counter.deployed(); 74 | console.log( 75 | _ + "Counter deployed at: " + counter.address 76 | ); 77 | 78 | 79 | } catch (error) { 80 | console.log(error); 81 | } 82 | }); 83 | */ 84 | 85 | if (!ENABLE_LOOTBOX) { 86 | deployer.deploy(ERC1155Opensea, proxyRegistryAddress, contractUri, baseMetadataUri, name, symbol, {gas: 5000000}); 87 | } else if (NFT_ADDRESS_TO_USE) { 88 | deployer.deploy(MyLootBox, proxyRegistryAddress, NFT_ADDRESS_TO_USE, {gas: 5000000}) 89 | .then(setupLootbox); 90 | } 91 | 92 | // default path 93 | else { 94 | deployer.deploy(ERC1155Opensea, proxyRegistryAddress, contractUri, baseMetadataUri, name, symbol, {gas: 5000000}) 95 | .then(() => { 96 | return deployer.deploy(MyLootBox, proxyRegistryAddress, ERC1155Opensea.address, {gas: 5000000}); 97 | }) 98 | .then(setupLootbox); 99 | } 100 | 101 | }; 102 | 103 | async function setupLootbox() { 104 | if (!NFT_ADDRESS_TO_USE) { 105 | const collectible = await ERC1155Opensea.deployed(); 106 | await collectible.transferOwnership(MyLootBox.address); 107 | } 108 | 109 | if (TOKEN_ID_MAPPING) { 110 | const lootbox = await MyLootBox.deployed(); 111 | for (const rarity in TOKEN_ID_MAPPING) { 112 | console.log(`Setting token ids for rarity ${rarity}`); 113 | const tokenIds = TOKEN_ID_MAPPING[rarity]; 114 | await lootbox.setTokenIdsForClass(rarity, tokenIds); 115 | } 116 | } 117 | 118 | saveContractDeploymentInfo(); 119 | } 120 | 121 | function saveContractDeploymentInfo() { 122 | const temp_metadata_dir = './temp_metadata'; 123 | const config_file = temp_metadata_dir + "/deployinfo.json"; 124 | 125 | 126 | const deploy_info = { 127 | "ownerAddress": ownerAddress, 128 | "ERC1155Address": ERC1155Opensea.address, 129 | "FactoryAddress": MyLootBox.address, 130 | "network": deployNetwork 131 | } 132 | 133 | const st = JSON.stringify(deploy_info, null, 2); 134 | 135 | console.log("------") 136 | console.log(st); 137 | console.log("------") 138 | 139 | fs.writeFileSync(config_file, st); 140 | } 141 | -------------------------------------------------------------------------------- /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 "./ERC1155Opensea.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 ERC1155Opensea (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 | ERC1155Opensea nftContract = ERC1155Opensea(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 | ERC1155Opensea nftContract = ERC1155Opensea(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 | -------------------------------------------------------------------------------- /scripts/erc1155_deploy_to_ipfs.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const fs = require('fs'); 3 | const FormData = require('form-data'); 4 | const recursive = require('recursive-fs'); 5 | const basePathConverter = require('base-path-converter'); 6 | const path = require('path'); 7 | const rimraf = require('rimraf'); 8 | 9 | require('dotenv').config() 10 | 11 | const url = `https://api.pinata.cloud/pinning/pinFileToIPFS`; 12 | const metadata_file = '../erc1155/metadata.json'; 13 | const temp_metadata_dir = './temp_metadata'; 14 | 15 | let images_hash, metadata_hash, contractUri_hash; 16 | 17 | // deploy images first to get the ipfs hash 18 | startDeploy = () => { 19 | const images_dir = './erc1155/images'; 20 | let imageCount = 0; 21 | 22 | //we gather the files from a local directory in this example, but a valid readStream is all that's needed for each file in the directory. 23 | recursive.readdirr(images_dir, function (err, dirs, files) { 24 | let data = new FormData(); 25 | files.forEach((file) => { 26 | imageCount++; 27 | //for each file stream, we need to include the correct relative file path 28 | data.append(`file`, fs.createReadStream(file), { 29 | filepath: basePathConverter(images_dir, file) 30 | }) 31 | }); 32 | 33 | return axios.post(url, 34 | data, 35 | { 36 | maxContentLength: 'Infinity', //this is needed to prevent axios from erroring out with large directories 37 | headers: { 38 | 'Content-Type': `multipart/form-data; boundary=${data._boundary}`, 39 | 'pinata_api_key': process.env.PINATA_API_KEY, 40 | 'pinata_secret_api_key': process.env.PINATA_SECRET_API_KEY 41 | } 42 | } 43 | ).then(function (response) { 44 | //handle response here 45 | images_hash = response.data.IpfsHash; 46 | 47 | console.log("image count: " + imageCount); 48 | console.log("image hash : " + images_hash); 49 | 50 | deployMetadata(); 51 | 52 | }).catch(function (error) { 53 | //handle error here 54 | console.log("Error ---"); 55 | console.log(error); 56 | }); 57 | }); 58 | }; 59 | 60 | // deploy erc1155 formatted json data to an ipfs directory 61 | deployMetadata = () => { 62 | const gateway_url = "https://gateway.pinata.cloud/ipfs/" + images_hash; 63 | const tokens_dir = temp_metadata_dir + '/tokens'; 64 | let tokenCount = 0; 65 | 66 | // delete all files in temp_metadata_dir 67 | rimraf.sync(temp_metadata_dir); 68 | 69 | // recreate directory 70 | fs.mkdirSync(tokens_dir, { recursive: true }); 71 | 72 | 73 | let jsonData = require(metadata_file).metadata; 74 | 75 | // upload each metadata to a specific json file 76 | jsonData.forEach( (metadata) => { 77 | tokenCount++; 78 | // replace image url with link on ipfs gateway 79 | metadata.image = gateway_url + "/" + metadata.image; 80 | // use it also as external_url 81 | metadata.external_url = metadata.image; 82 | 83 | const filename = tokens_dir +"/" + metadata.id; 84 | const st = JSON.stringify(metadata, null, 2); 85 | fs.writeFileSync(filename, st); 86 | }); 87 | 88 | //we gather the files from a local directory in this example, but a valid readStream is all that's needed for each file in the directory. 89 | recursive.readdirr(tokens_dir, function (err, dirs, files) { 90 | let data = new FormData(); 91 | files.forEach((file) => { 92 | //for each file stream, we need to include the correct relative file path 93 | data.append(`file`, fs.createReadStream(file), { 94 | filepath: basePathConverter(tokens_dir, file) 95 | }) 96 | }); 97 | 98 | return axios.post(url, 99 | data, 100 | { 101 | maxContentLength: 'Infinity', //this is needed to prevent axios from erroring out with large directories 102 | headers: { 103 | 'Content-Type': `multipart/form-data; boundary=${data._boundary}`, 104 | 'pinata_api_key': process.env.PINATA_API_KEY, 105 | 'pinata_secret_api_key': process.env.PINATA_SECRET_API_KEY 106 | } 107 | } 108 | ).then(function (response) { 109 | //handle response here 110 | metadata_hash = response.data.IpfsHash; 111 | 112 | console.log("token count: " + tokenCount); 113 | console.log("token hash : " + metadata_hash); 114 | 115 | deployContractUri(); 116 | 117 | }).catch(function (error) { 118 | //handle error here 119 | console.log("Error ---"); 120 | console.log(error); 121 | }); 122 | }); 123 | 124 | 125 | } 126 | 127 | deployContractUri = () => { 128 | const contractUriFile = temp_metadata_dir + '/contracturi.json'; 129 | 130 | // read contractUri 131 | let contractUri = require(metadata_file).contractUri; 132 | const st = JSON.stringify(contractUri, null, 2); 133 | fs.writeFileSync(contractUriFile, st); 134 | 135 | let data = new FormData(); 136 | data.append(`file`, fs.createReadStream(contractUriFile)); 137 | 138 | 139 | return axios.post(url, 140 | data, 141 | { 142 | maxContentLength: 'Infinity', //this is needed to prevent axios from erroring out with large directories 143 | headers: { 144 | 'Content-Type': `multipart/form-data; boundary=${data._boundary}`, 145 | 'pinata_api_key': process.env.PINATA_API_KEY, 146 | 'pinata_secret_api_key': process.env.PINATA_SECRET_API_KEY 147 | } 148 | } 149 | ).then(function (response) { 150 | //handle response here 151 | contractUri_hash = response.data.IpfsHash; 152 | 153 | console.log("-----Contract Uri -----"); 154 | console.log(st); 155 | console.log("-----------------------") 156 | console.log("contract URI hash : " + contractUri_hash); 157 | 158 | saveConfigFile(); 159 | 160 | }).catch(function (error) { 161 | //handle error here 162 | console.log("Error ---"); 163 | console.log(error); 164 | }); 165 | 166 | } 167 | 168 | 169 | saveConfigFile = () => { 170 | const config_file = temp_metadata_dir + "/erc1155config.json"; 171 | 172 | const metadata_config = { 173 | "gatewayUrl": "https://gateway.pinata.cloud/ipfs", 174 | "metadataHash": metadata_hash, 175 | "imagesHash": images_hash, 176 | "contractUriHash": contractUri_hash 177 | } 178 | 179 | const st = JSON.stringify(metadata_config, null, 2); 180 | fs.writeFileSync(config_file, st); 181 | } 182 | 183 | startDeploy(); -------------------------------------------------------------------------------- /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/Tournaments.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.11; 2 | 3 | import "@openzeppelin/contracts/ownership/Ownable.sol"; 4 | 5 | contract Tournaments is Ownable { 6 | /** 7 | `Data 8 | */ 9 | enum TournamentState { 10 | Draft, 11 | Active, 12 | Ended 13 | } 14 | 15 | struct Tournament { 16 | address payable organizer; 17 | uint endTime; 18 | string data; 19 | uint prize; 20 | TournamentState state; 21 | uint balance; 22 | } 23 | 24 | struct GameResult { 25 | bool winner; 26 | address payable player; 27 | string data; 28 | } 29 | 30 | Tournament[] public tournaments; 31 | mapping(uint => GameResult[]) public results; 32 | 33 | /** 34 | Events 35 | */ 36 | event TournamentCreated(uint tournamentId); 37 | event TournamentActivated(uint tournamentId); 38 | event ResultSubmitted(uint tournamentId, address indexed player, 39 | uint256 indexed resultId); 40 | event WinnerDeclared(uint tournamentId, address indexed player, uint256 indexed resultId); 41 | event TournamentStopped(uint tournamentId); 42 | 43 | /** 44 | Modifiers 45 | */ 46 | modifier amountNotZero(uint amount) { 47 | require(amount != 0, "Amount must not be zero"); 48 | _; 49 | } 50 | 51 | modifier noTournamentsOverflow(){ 52 | require((tournaments.length + 1) > tournaments.length, 53 | "Too many tournaments"); 54 | _; 55 | } 56 | 57 | modifier noResultsOverflow(uint tournamentId){ 58 | require((results[tournamentId].length + 1) > results[tournamentId].length, 59 | "Too many results"); 60 | _; 61 | } 62 | 63 | modifier notInPast(uint time) { 64 | require(time > now, "Incorrect time"); 65 | _; 66 | } 67 | 68 | modifier tournamentIdIsCorrect(uint id){ 69 | require(id < tournaments.length, "Incorrect tournament id"); 70 | _; 71 | } 72 | 73 | modifier resultIdIsCorrect(uint tournamentId, uint resultId) { 74 | require(resultId < results[tournamentId].length, "Incorrect result Id"); 75 | _; 76 | } 77 | 78 | modifier tournamentNotEnded(uint id) { 79 | require(now < tournaments[id].endTime, "Tournament has ended"); 80 | _; 81 | } 82 | 83 | modifier onlyOrganizer(uint tournamentId) { 84 | require(msg.sender == tournaments[tournamentId].organizer, 85 | "Must be tournament's organizer"); 86 | _; 87 | } 88 | 89 | modifier notOrganizer(uint tournamentId) { 90 | require(msg.sender != tournaments[tournamentId].organizer, 91 | "Must not be tournament's organizer"); 92 | _; 93 | } 94 | 95 | modifier correctPaymentAmount(uint amount) { 96 | require((amount * 1 wei) == msg.value, 97 | "Incorrent payment amount"); 98 | _; 99 | } 100 | 101 | modifier correctTournamentState(uint tournamentId, TournamentState state) { 102 | require(tournaments[tournamentId].state == state, 103 | "Incorrect tournament state"); 104 | _; 105 | } 106 | 107 | modifier resultIsNotWinner(uint tournamentId, uint resultId) { 108 | require(results[tournamentId][resultId].winner == false, 109 | "Result is already winner"); 110 | _; 111 | } 112 | 113 | modifier enoughBalanceForPrize(uint tournamentId) { 114 | require(tournaments[tournamentId].balance >= tournaments[tournamentId].prize, 115 | "Not enough tournament balance"); 116 | _; 117 | } 118 | 119 | /** 120 | Methods 121 | */ 122 | function createTournament( 123 | address payable organizer, 124 | uint endTime, 125 | string calldata data, 126 | uint256 prize 127 | ) 128 | external 129 | notInPast(endTime) 130 | amountNotZero(prize) 131 | noTournamentsOverflow() 132 | returns (uint) 133 | { 134 | tournaments.push(Tournament(organizer, endTime, data, prize, TournamentState.Draft, 0)); 135 | emit TournamentCreated(tournaments.length - 1); 136 | return (tournaments.length - 1); 137 | } 138 | 139 | function activateTournament(uint tournamentId, uint value) 140 | external 141 | payable 142 | tournamentIdIsCorrect(tournamentId) 143 | tournamentNotEnded(tournamentId) 144 | onlyOrganizer(tournamentId) 145 | correctPaymentAmount(value) 146 | correctTournamentState(tournamentId, TournamentState.Draft) 147 | { 148 | tournaments[tournamentId].balance += value; 149 | require (tournaments[tournamentId].balance >= tournaments[tournamentId].prize, 150 | "Payment amount is lower than prize"); 151 | tournaments[tournamentId].state = TournamentState.Active; 152 | emit TournamentActivated(tournamentId); 153 | } 154 | 155 | function submitResult(uint tournamentId, string calldata data) 156 | external 157 | tournamentIdIsCorrect(tournamentId) 158 | noResultsOverflow(tournamentId) 159 | correctTournamentState(tournamentId, TournamentState.Active) 160 | tournamentNotEnded(tournamentId) 161 | notOrganizer(tournamentId) 162 | { 163 | results[tournamentId].push(GameResult(false, msg.sender, data)); 164 | emit ResultSubmitted(tournamentId, msg.sender, (results[tournamentId].length - 1)); 165 | } 166 | 167 | function declareWinner(uint tournamentId, uint resultId) 168 | external 169 | tournamentIdIsCorrect(tournamentId) 170 | resultIdIsCorrect(tournamentId, resultId) 171 | onlyOrganizer(tournamentId) 172 | correctTournamentState(tournamentId, TournamentState.Active) 173 | resultIsNotWinner(tournamentId, resultId) 174 | enoughBalanceForPrize(tournamentId) 175 | { 176 | results[tournamentId][resultId].winner = true; 177 | results[tournamentId][resultId].player.transfer(tournaments[tournamentId].prize); 178 | tournaments[tournamentId].balance -= tournaments[tournamentId].prize; 179 | emit WinnerDeclared(tournamentId, msg.sender, resultId); 180 | } 181 | 182 | function stopTournament(uint tournamentId) 183 | external 184 | tournamentIdIsCorrect(tournamentId) 185 | onlyOrganizer(tournamentId) 186 | correctTournamentState(tournamentId, TournamentState.Active) 187 | { 188 | tournaments[tournamentId].state = TournamentState.Ended; 189 | uint oldBalance = tournaments[tournamentId].balance; 190 | tournaments[tournamentId].balance = 0; 191 | if (oldBalance > 0){ 192 | tournaments[tournamentId].organizer.transfer(oldBalance); 193 | } 194 | emit TournamentStopped(tournamentId); 195 | } 196 | 197 | function getTournament(uint tournamentId) 198 | public 199 | view 200 | tournamentIdIsCorrect(tournamentId) 201 | returns (address, uint, uint, uint, uint) 202 | { 203 | return (tournaments[tournamentId].organizer, 204 | tournaments[tournamentId].endTime, 205 | tournaments[tournamentId].prize, 206 | uint(tournaments[tournamentId].state), 207 | tournaments[tournamentId].balance); 208 | } 209 | 210 | function getResult(uint tournamentId, uint resultId) 211 | public 212 | view 213 | tournamentIdIsCorrect(tournamentId) 214 | resultIdIsCorrect(tournamentId, resultId) 215 | returns (bool, address, string memory) 216 | { 217 | return (results[tournamentId][resultId].winner, 218 | results[tournamentId][resultId].player, 219 | results[tournamentId][resultId].data); 220 | } 221 | 222 | function getTournamentsCount() 223 | public 224 | view 225 | returns (uint) 226 | { 227 | return tournaments.length; 228 | } 229 | 230 | function getResultsCount(uint tournamentId) 231 | public 232 | view 233 | tournamentIdIsCorrect(tournamentId) 234 | returns (uint) 235 | { 236 | return results[tournamentId].length; 237 | } 238 | 239 | 240 | } 241 | -------------------------------------------------------------------------------- /contracts/ERC721CryptoPizza.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.12; 2 | 3 | // Imports symbols from other files into the current contract. 4 | // In this case, a series of helper contracts from OpenZeppelin. 5 | // Learn more: https://solidity.readthedocs.io/en/v0.5.10/layout-of-source-files.html#importing-other-source-files 6 | import "../node_modules/@openzeppelin/contracts/token/ERC721/IERC721.sol"; 7 | import "../node_modules/@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; 8 | import "../node_modules/@openzeppelin/contracts/introspection/ERC165.sol"; 9 | import "../node_modules/@openzeppelin/contracts/math/SafeMath.sol"; 10 | 11 | // The `is` keyword is used to inherit functions and keywords from external contracts. 12 | // In this case, `CryptoPizza` inherits from the `IERC721` and `ERC165` contracts. 13 | // Learn more: https://solidity.readthedocs.io/en/v0.5.10/contracts.html#inheritance 14 | contract ERC721CryptoPizza is IERC721, ERC165 { 15 | // Uses OpenZeppelin's SafeMath library to perform arithmetic operations safely. 16 | // Learn more: https://docs.openzeppelin.com/contracts/2.x/api/math#SafeMath 17 | using SafeMath for uint256; 18 | 19 | // Constant state variables in Solidity are similar to other languages 20 | // but you must assign from an expression which is constant at compile time. 21 | // Learn more: https://solidity.readthedocs.io/en/v0.5.10/contracts.html#constant-state-variables 22 | uint256 constant dnaDigits = 10; 23 | uint256 constant dnaModulus = 10 ** dnaDigits; 24 | bytes4 private constant _ERC721_RECEIVED = 0x150b7a02; 25 | 26 | // Struct types let you define your own type 27 | // Learn more: https://solidity.readthedocs.io/en/v0.5.10/types.html#structs 28 | struct Pizza { 29 | string name; 30 | uint256 dna; 31 | } 32 | 33 | // Creates an empty array of Pizza structs 34 | Pizza[] public pizzas; 35 | 36 | // Mapping from owner's address to id of Pizza 37 | mapping(uint256 => address) public pizzaToOwner; 38 | 39 | // Mapping from owner's address to number of owned token 40 | mapping(address => uint256) public ownerPizzaCount; 41 | 42 | // Mapping from token ID to approved address 43 | mapping(uint256 => address) pizzaApprovals; 44 | 45 | // You can nest mappings, this example maps owner to operator approvals 46 | mapping(address => mapping(address => bool)) private operatorApprovals; 47 | 48 | // Internal function to create a random Pizza from string (name) and DNA 49 | function _createPizza(string memory _name, uint256 _dna) 50 | // The `internal` keyword means this function is only visible 51 | // within this contract and contracts that derive this contract 52 | // Learn more: https://solidity.readthedocs.io/en/v0.5.10/contracts.html#visibility-and-getters 53 | internal 54 | // `isUnique` is a function modifier that checks if the pizza already exists 55 | // Learn more: https://solidity.readthedocs.io/en/v0.5.10/structure-of-a-contract.html#function-modifiers 56 | isUnique(_name, _dna) 57 | { 58 | // Adds Pizza to array of Pizzas and get id 59 | uint256 id = SafeMath.sub(pizzas.push(Pizza(_name, _dna)), 1); 60 | 61 | // Checks that Pizza owner is the same as current user 62 | // Learn more: https://solidity.readthedocs.io/en/v0.5.10/control-structures.html#error-handling-assert-require-revert-and-exceptions 63 | assert(pizzaToOwner[id] == address(0)); 64 | 65 | // Maps the Pizza to the owner 66 | pizzaToOwner[id] = msg.sender; 67 | ownerPizzaCount[msg.sender] = SafeMath.add( 68 | ownerPizzaCount[msg.sender], 69 | 1 70 | ); 71 | } 72 | 73 | // Creates a random Pizza from string (name) 74 | function createRandomPizza(string memory _name) public { 75 | uint256 randDna = generateRandomDna(_name, msg.sender); 76 | _createPizza(_name, randDna); 77 | } 78 | 79 | // Generates random DNA from string (name) and address of the owner (creator) 80 | function generateRandomDna(string memory _str, address _owner) 81 | public 82 | // Functions marked as `pure` promise not to read from or modify the state 83 | // Learn more: https://solidity.readthedocs.io/en/v0.5.10/contracts.html#pure-functions 84 | pure 85 | returns (uint256) 86 | { 87 | // Generates random uint from string (name) + address (owner) 88 | uint256 rand = uint256(keccak256(abi.encodePacked(_str))) + 89 | uint256(_owner); 90 | rand = rand % dnaModulus; 91 | return rand; 92 | } 93 | 94 | // Returns array of Pizzas found by owner 95 | function getPizzasByOwner(address _owner) 96 | public 97 | // Functions marked as `view` promise not to modify state 98 | // Learn more: https://solidity.readthedocs.io/en/v0.5.10/contracts.html#view-functions 99 | view 100 | returns (uint256[] memory) 101 | { 102 | // Uses the `memory` storage location to store values only for the 103 | // lifecycle of this function call. 104 | // Learn more: https://solidity.readthedocs.io/en/v0.5.10/introduction-to-smart-contracts.html#storage-memory-and-the-stack 105 | uint256[] memory result = new uint256[](ownerPizzaCount[_owner]); 106 | uint256 counter = 0; 107 | for (uint256 i = 0; i < pizzas.length; i++) { 108 | if (pizzaToOwner[i] == _owner) { 109 | result[counter] = i; 110 | counter++; 111 | } 112 | } 113 | return result; 114 | } 115 | 116 | // Transfers Pizza and ownership to other address 117 | function transferFrom(address _from, address _to, uint256 _pizzaId) public { 118 | require(_from != address(0) && _to != address(0), "Invalid address."); 119 | require(_exists(_pizzaId), "Pizza does not exist."); 120 | require(_from != _to, "Cannot transfer to the same address."); 121 | require(_isApprovedOrOwner(msg.sender, _pizzaId), "Address is not approved."); 122 | 123 | ownerPizzaCount[_to] = SafeMath.add(ownerPizzaCount[_to], 1); 124 | ownerPizzaCount[_from] = SafeMath.sub(ownerPizzaCount[_from], 1); 125 | pizzaToOwner[_pizzaId] = _to; 126 | 127 | // Emits event defined in the imported IERC721 contract 128 | emit Transfer(_from, _to, _pizzaId); 129 | _clearApproval(_to, _pizzaId); 130 | } 131 | 132 | /** 133 | * Safely transfers the ownership of a given token ID to another address 134 | * If the target address is a contract, it must implement `onERC721Received`, 135 | * which is called upon a safe transfer, and return the magic value 136 | * `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`; 137 | * otherwise, the transfer is reverted. 138 | */ 139 | function safeTransferFrom(address from, address to, uint256 pizzaId) 140 | public 141 | { 142 | // solium-disable-next-line arg-overflow 143 | this.safeTransferFrom(from, to, pizzaId, ""); 144 | } 145 | 146 | /** 147 | * Safely transfers the ownership of a given token ID to another address 148 | * If the target address is a contract, it must implement `onERC721Received`, 149 | * which is called upon a safe transfer, and return the magic value 150 | * `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`; 151 | * otherwise, the transfer is reverted. 152 | */ 153 | function safeTransferFrom( 154 | address from, 155 | address to, 156 | uint256 pizzaId, 157 | bytes memory _data 158 | ) public { 159 | this.transferFrom(from, to, pizzaId); 160 | require(_checkOnERC721Received(from, to, pizzaId, _data), "Must implmement onERC721Received."); 161 | } 162 | 163 | /** 164 | * Internal function to invoke `onERC721Received` on a target address 165 | * The call is not executed if the target address is not a contract 166 | */ 167 | function _checkOnERC721Received( 168 | address from, 169 | address to, 170 | uint256 pizzaId, 171 | bytes memory _data 172 | ) internal returns (bool) { 173 | if (!isContract(to)) { 174 | return true; 175 | } 176 | 177 | bytes4 retval = IERC721Receiver(to).onERC721Received( 178 | msg.sender, 179 | from, 180 | pizzaId, 181 | _data 182 | ); 183 | return (retval == _ERC721_RECEIVED); 184 | } 185 | 186 | // Burns a Pizza - destroys Token completely 187 | // The `external` function modifier means this function is 188 | // part of the contract interface and other contracts can call it 189 | function burn(uint256 _pizzaId) external { 190 | require(msg.sender != address(0), "Invalid address."); 191 | require(_exists(_pizzaId), "Pizza does not exist."); 192 | require(_isApprovedOrOwner(msg.sender, _pizzaId), "Address is not approved."); 193 | 194 | ownerPizzaCount[msg.sender] = SafeMath.sub( 195 | ownerPizzaCount[msg.sender], 196 | 1 197 | ); 198 | pizzaToOwner[_pizzaId] = address(0); 199 | } 200 | 201 | // Returns count of Pizzas by address 202 | function balanceOf(address _owner) public view returns (uint256 _balance) { 203 | return ownerPizzaCount[_owner]; 204 | } 205 | 206 | // Returns owner of the Pizza found by id 207 | function ownerOf(uint256 _pizzaId) public view returns (address _owner) { 208 | address owner = pizzaToOwner[_pizzaId]; 209 | require(owner != address(0), "Invalid Pizza ID."); 210 | return owner; 211 | } 212 | 213 | // Approves other address to transfer ownership of Pizza 214 | function approve(address _to, uint256 _pizzaId) public { 215 | require(msg.sender == pizzaToOwner[_pizzaId], "Must be the Pizza owner."); 216 | pizzaApprovals[_pizzaId] = _to; 217 | emit Approval(msg.sender, _to, _pizzaId); 218 | } 219 | 220 | // Returns approved address for specific Pizza 221 | function getApproved(uint256 _pizzaId) 222 | public 223 | view 224 | returns (address operator) 225 | { 226 | require(_exists(_pizzaId), "Pizza does not exist."); 227 | return pizzaApprovals[_pizzaId]; 228 | } 229 | 230 | /** 231 | * Private function to clear current approval of a given token ID 232 | * Reverts if the given address is not indeed the owner of the token 233 | */ 234 | function _clearApproval(address owner, uint256 _pizzaId) private { 235 | require(pizzaToOwner[_pizzaId] == owner, "Must be pizza owner."); 236 | require(_exists(_pizzaId), "Pizza does not exist."); 237 | if (pizzaApprovals[_pizzaId] != address(0)) { 238 | pizzaApprovals[_pizzaId] = address(0); 239 | } 240 | } 241 | 242 | /* 243 | * Sets or unsets the approval of a given operator 244 | * An operator is allowed to transfer all tokens of the sender on their behalf 245 | */ 246 | function setApprovalForAll(address to, bool approved) public { 247 | require(to != msg.sender, "Cannot approve own address"); 248 | operatorApprovals[msg.sender][to] = approved; 249 | emit ApprovalForAll(msg.sender, to, approved); 250 | } 251 | 252 | // Tells whether an operator is approved by a given owner 253 | function isApprovedForAll(address owner, address operator) 254 | public 255 | view 256 | returns (bool) 257 | { 258 | return operatorApprovals[owner][operator]; 259 | } 260 | 261 | // Takes ownership of Pizza - only for approved users 262 | function takeOwnership(uint256 _pizzaId) public { 263 | require(_isApprovedOrOwner(msg.sender, _pizzaId), "Address is not approved."); 264 | address owner = this.ownerOf(_pizzaId); 265 | this.transferFrom(owner, msg.sender, _pizzaId); 266 | } 267 | 268 | // Checks if Pizza exists 269 | function _exists(uint256 pizzaId) internal view returns (bool) { 270 | address owner = pizzaToOwner[pizzaId]; 271 | return owner != address(0); 272 | } 273 | 274 | // Checks if address is owner or is approved to transfer Pizza 275 | function _isApprovedOrOwner(address spender, uint256 pizzaId) 276 | internal 277 | view 278 | returns (bool) 279 | { 280 | address owner = pizzaToOwner[pizzaId]; 281 | // Disable solium check because of 282 | // https://github.com/duaraghav8/Solium/issues/175 283 | // solium-disable-next-line operator-whitespace 284 | return (spender == owner || 285 | this.getApproved(pizzaId) == spender || 286 | this.isApprovedForAll(owner, spender)); 287 | } 288 | 289 | // Check if Pizza is unique and doesn't exist yet 290 | modifier isUnique(string memory _name, uint256 _dna) { 291 | bool result = true; 292 | for (uint256 i = 0; i < pizzas.length; i++) { 293 | if ( 294 | keccak256(abi.encodePacked(pizzas[i].name)) == 295 | keccak256(abi.encodePacked(_name)) && 296 | pizzas[i].dna == _dna 297 | ) { 298 | result = false; 299 | } 300 | } 301 | require(result, "Pizza with such name already exists."); 302 | _; 303 | } 304 | 305 | // Returns whether the target address is a contract 306 | function isContract(address account) internal view returns (bool) { 307 | uint256 size; 308 | // Currently there is no better way to check if there is a contract in an address 309 | // than to check the size of the code at that address. 310 | // See https://ethereum.stackexchange.com/a/14016/36603 311 | // for more details about how this works. 312 | // TODO Check this again before the Serenity release, because all addresses will be 313 | // contracts then. 314 | // solium-disable-next-line security/no-inline-assembly 315 | assembly { 316 | size := extcodesize(account) 317 | } 318 | return size > 0; 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /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 "./ERC1155Opensea.sol"; 7 | import "./MyFactory.sol"; 8 | import "./ILootBox.sol"; 9 | 10 | /** 11 | * @title MyLootBox 12 | * MyLootBox - a randomized and openable lootbox of ERC1155Opensea 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 Test mint function for The Contractor. Just for testing, replace eventually with a better reusable one. 184 | * Creates 1 token of id 1, 2 tokens of id 2 and 3 tokens of id 3 for owner account 185 | */ 186 | function contractorTestMint( 187 | address _toAddress 188 | ) external onlyOwner { 189 | 190 | ERC1155Opensea nftContract = ERC1155Opensea(nftAddress); 191 | nftContract.create(_toAddress, 1, "", ""); 192 | nftContract.create(_toAddress, 5, "", ""); 193 | nftContract.create(_toAddress, 10, "", ""); 194 | } 195 | 196 | 197 | /** 198 | * @dev Open a lootbox manually and send what's inside to _toAddress 199 | * Convenience method for contract owner. 200 | */ 201 | function open( 202 | uint256 _optionId, 203 | address _toAddress, 204 | uint256 _amount 205 | ) external onlyOwner { 206 | _mint(Option(_optionId), _toAddress, _amount, ""); 207 | } 208 | 209 | /** 210 | * @dev Main minting logic for lootboxes 211 | * This is called via safeTransferFrom when MyLootBox extends MyFactory. 212 | * NOTE: prices and fees are determined by the sell order on OpenSea. 213 | */ 214 | function _mint( 215 | Option _option, 216 | address _toAddress, 217 | uint256 _amount, 218 | bytes memory /* _data */ 219 | ) internal whenNotPaused nonReentrant { 220 | // Load settings for this box option 221 | uint256 optionId = uint256(_option); 222 | OptionSettings memory settings = optionToSettings[optionId]; 223 | 224 | require(settings.maxQuantityPerOpen > 0, "MyLootBox#_mint: OPTION_NOT_ALLOWED"); 225 | require(_canMint(msg.sender, _option, _amount), "MyLootBox#_mint: CANNOT_MINT"); 226 | 227 | uint256 totalMinted = 0; 228 | 229 | // Iterate over the quantity of boxes specified 230 | for (uint256 i = 0; i < _amount; i++) { 231 | // Iterate over the box's set quantity 232 | uint256 quantitySent = 0; 233 | if (settings.hasGuaranteedClasses) { 234 | // Process guaranteed token ids 235 | for (uint256 classId = 0; classId < settings.guarantees.length; classId++) { 236 | if (classId > 0) { 237 | uint256 quantityOfGaranteed = settings.guarantees[classId]; 238 | _sendTokenWithClass(Class(classId), _toAddress, quantityOfGaranteed); 239 | quantitySent += quantityOfGaranteed; 240 | } 241 | } 242 | } 243 | 244 | // Process non-guaranteed ids 245 | while (quantitySent < settings.maxQuantityPerOpen) { 246 | uint256 quantityOfRandomized = 1; 247 | Class class = _pickRandomClass(settings.classProbabilities); 248 | _sendTokenWithClass(class, _toAddress, quantityOfRandomized); 249 | quantitySent += quantityOfRandomized; 250 | } 251 | 252 | totalMinted += quantitySent; 253 | } 254 | 255 | // Event emissions 256 | emit LootBoxOpened(optionId, _toAddress, _amount, totalMinted); 257 | } 258 | 259 | function withdraw() public onlyOwner { 260 | msg.sender.transfer(address(this).balance); 261 | } 262 | 263 | ///// 264 | // Metadata methods 265 | ///// 266 | 267 | function name() external view returns (string memory) { 268 | return "My Loot Box"; 269 | } 270 | 271 | function symbol() external view returns (string memory) { 272 | return "MYLOOT"; 273 | } 274 | 275 | function uri(uint256 _optionId) external view returns (string memory) { 276 | return Strings.strConcat( 277 | baseMetadataURI, 278 | "box/", 279 | Strings.uint2str(_optionId) 280 | ); 281 | } 282 | 283 | ///// 284 | // HELPER FUNCTIONS 285 | ///// 286 | 287 | // Returns the tokenId sent to _toAddress 288 | function _sendTokenWithClass( 289 | Class _class, 290 | address _toAddress, 291 | uint256 _amount 292 | ) internal returns (uint256) { 293 | uint256 classId = uint256(_class); 294 | ERC1155Opensea nftContract = ERC1155Opensea(nftAddress); 295 | uint256 tokenId = _pickRandomAvailableTokenIdForClass(_class, _amount); 296 | if (classIsPreminted[classId]) { 297 | nftContract.safeTransferFrom( 298 | owner(), 299 | _toAddress, 300 | tokenId, 301 | _amount, 302 | "" 303 | ); 304 | } else if (tokenId == 0) { 305 | tokenId = nftContract.create(_toAddress, _amount, "", ""); 306 | classToTokenIds[classId].push(tokenId); 307 | } else { 308 | nftContract.mint(_toAddress, tokenId, _amount, ""); 309 | } 310 | return tokenId; 311 | } 312 | 313 | function _pickRandomClass( 314 | uint16[NUM_CLASSES] memory _classProbabilities 315 | ) internal returns (Class) { 316 | uint16 value = uint16(_random().mod(INVERSE_BASIS_POINT)); 317 | // Start at top class (length - 1) 318 | // skip common (0), we default to it 319 | for (uint256 i = _classProbabilities.length - 1; i > 0; i--) { 320 | uint16 probability = _classProbabilities[i]; 321 | if (value < probability) { 322 | return Class(i); 323 | } else { 324 | value = value - probability; 325 | } 326 | } 327 | return Class.Common; 328 | } 329 | 330 | function _pickRandomAvailableTokenIdForClass( 331 | Class _class, 332 | uint256 _minAmount 333 | ) internal returns (uint256) { 334 | uint256 classId = uint256(_class); 335 | uint256[] memory tokenIds = classToTokenIds[classId]; 336 | if (tokenIds.length == 0) { 337 | // Unminted 338 | require( 339 | !classIsPreminted[classId], 340 | "MyLootBox#_pickRandomAvailableTokenIdForClass: NO_TOKEN_ON_PREMINTED_CLASS" 341 | ); 342 | return 0; 343 | } 344 | 345 | uint256 randIndex = _random().mod(tokenIds.length); 346 | 347 | if (classIsPreminted[classId]) { 348 | // Make sure owner() owns enough 349 | ERC1155Opensea nftContract = ERC1155Opensea(nftAddress); 350 | for (uint256 i = randIndex; i < randIndex + tokenIds.length; i++) { 351 | uint256 tokenId = tokenIds[i % tokenIds.length]; 352 | if (nftContract.balanceOf(owner(), tokenId) >= _minAmount) { 353 | return tokenId; 354 | } 355 | } 356 | revert("MyLootBox#_pickRandomAvailableTokenIdForClass: NOT_ENOUGH_TOKENS_FOR_CLASS"); 357 | } else { 358 | return tokenIds[randIndex]; 359 | } 360 | } 361 | 362 | /** 363 | * @dev Pseudo-random number generator 364 | * NOTE: to improve randomness, generate it with an oracle 365 | */ 366 | function _random() internal returns (uint256) { 367 | uint256 randomNumber = uint256(keccak256(abi.encodePacked(blockhash(block.number - 1), msg.sender, seed))); 368 | seed = randomNumber; 369 | return randomNumber; 370 | } 371 | 372 | /** 373 | * @dev emit a Warning if we're not approved to transfer nftAddress 374 | */ 375 | function _checkTokenApproval() internal { 376 | ERC1155Opensea nftContract = ERC1155Opensea(nftAddress); 377 | if (!nftContract.isApprovedForAll(owner(), address(this))) { 378 | emit Warning("Lootbox contract is not approved for trading collectible by:", owner()); 379 | } 380 | } 381 | 382 | function _addTokenIdToClass(Class _class, uint256 _tokenId) internal { 383 | uint256 classId = uint256(_class); 384 | classIsPreminted[classId] = true; 385 | classToTokenIds[classId].push(_tokenId); 386 | } 387 | } 388 | --------------------------------------------------------------------------------