├── test ├── .gitkeep └── GameNFTs.js ├── README.md ├── .eslintignore ├── .gitignore ├── .solcover.js ├── contracts ├── IGameNFTs.sol ├── utils │ ├── AttributeClass.sol │ ├── Arrays.sol │ └── Operatable.sol ├── Migrations.sol ├── attributes │ ├── IEvolutiveAttribute.sol │ ├── IUpgradableAttribute.sol │ ├── IGenericAttribute.sol │ ├── ITransferableAttribute.sol │ ├── GenericAttribute.sol │ ├── UpgradableAttribute.sol │ ├── TransferableAttribute.sol │ └── EvolutiveAttribute.sol └── GameNFTs.sol ├── migrations ├── 1_initial_migration.js └── 2_deploy_contracts.js ├── .solhint.json ├── package.json └── truffle-config.js /test/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gNFT 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/* 2 | coverage/* 3 | migrations/* -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | .idea/* 4 | coverage.json 5 | coverage/ 6 | yarn-error.log 7 | crytic-export 8 | mcore_* -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | port: 8555, 3 | skipFiles: [ 4 | 'Migrations.sol', 5 | 'test' 6 | ], 7 | testrpcOptions: "-p 8555 -d" 8 | }; -------------------------------------------------------------------------------- /contracts/IGameNFTs.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | /** 6 | * @dev Interface of the NFT attribute. 7 | */ 8 | interface IGameNFTs { 9 | function creatorOf(uint256 _id) external view returns (address); 10 | } 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "rules": { 4 | "mark-callable-contracts": "off", 5 | "event-name-camelcase": "off", 6 | "const-name-snakecase": "off", 7 | "max-line-length": ["warn", 120], 8 | "indent": ["error", 4] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /contracts/utils/AttributeClass.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | abstract contract AttributeClass { 6 | // 1 => Generic, 7 | // 2 => Upgradable, 8 | // 3 => Transferable, 9 | // 4 => Evolutive 10 | // more expand... 11 | uint16 private _class; 12 | 13 | constructor (uint16 class_) { 14 | _class = class_; 15 | } 16 | 17 | /** 18 | * @dev Returns the class of the attribute. 19 | */ 20 | function class() public view virtual returns (uint16) { 21 | return _class; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /contracts/utils/Arrays.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | /** 6 | * @dev Collection of functions related to array types. 7 | */ 8 | library Arrays { 9 | function find(uint256[] storage values, uint value) public view returns(uint) { 10 | uint i = 0; 11 | while (values[i] != value) { 12 | i++; 13 | } 14 | return i; 15 | } 16 | 17 | function removeByValue(uint256[] storage values, uint value) public { 18 | uint i = find(values, value); 19 | delete values[i]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | contract Migrations { 6 | address public owner; 7 | uint public last_completed_migration; 8 | 9 | constructor() { 10 | owner = msg.sender; 11 | } 12 | 13 | modifier restricted() { 14 | if (msg.sender == owner) _; 15 | } 16 | 17 | function setCompleted(uint completed) public restricted { 18 | last_completed_migration = completed; 19 | } 20 | 21 | function upgrade(address new_address) public restricted { 22 | Migrations upgraded = Migrations(new_address); 23 | upgraded.setCompleted(last_completed_migration); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gnft", 3 | "version": "1.0.0", 4 | "description": "gNFT smart contracts, including ERC1155 templates.", 5 | "scripts": { 6 | "compile": "truffle compile", 7 | "test": "truffle test", 8 | "lint": "eslint .", 9 | "lint:contracts": "solhint contracts/*.sol" 10 | }, 11 | "author": "support@DRepublic.io", 12 | "license": "ISC", 13 | "dependencies": { 14 | "openzeppelin-solidity": "~4.1.0", 15 | "truffle": "^5.1.30", 16 | "truffle-assertions": "^0.9.2", 17 | "truffle-contract-size": "^2.0.1", 18 | "truffle-flattener": "^1.4.2", 19 | "truffle-hdwallet-provider": "1.0.17", 20 | "truffle-privatekey-provider": "^1.5.0" 21 | }, 22 | "engines": { 23 | "node": "^14.17.x", 24 | "yarn": "~1.22.10" 25 | }, 26 | "devDependencies": { 27 | "eslint": "^7.22.0", 28 | "truffle-plugin-verify": "^0.5.8" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /migrations/2_deploy_contracts.js: -------------------------------------------------------------------------------- 1 | const GameNFTs = artifacts.require("GameNFTs.sol"); 2 | const Arrays = artifacts.require("../contracts/utils/Arrays.sol"); 3 | const GenericAttribute = artifacts.require("../contracts/attributes/GenericAttribute.sol"); 4 | const UpgradableAttribute = artifacts.require("../contracts/attributes/UpgradableAttribute.sol"); 5 | const TransferableAttribute = artifacts.require("../contracts/attributes/TransferableAttribute.sol"); 6 | const EvolutiveAttribute = artifacts.require("../contracts/attributes/EvolutiveAttribute.sol"); 7 | 8 | module.exports = async (deployer) => { 9 | let proxyRegistryAddress = "0xf57b2c51ded3a29e6891aba85459d600256cf317"; 10 | 11 | await deployer.deploy(GameNFTs, "gNFT", "OGN", 12 | "https://nfts-api.drepublic.io/api/nfts/{id}", proxyRegistryAddress, {gas: 5000000}); 13 | 14 | // deploy nft attributes 15 | await deployer.deploy(Arrays); 16 | await deployer.link(Arrays, GenericAttribute); 17 | await deployer.deploy(GenericAttribute); 18 | await deployer.link(Arrays, UpgradableAttribute); 19 | await deployer.deploy(UpgradableAttribute); 20 | await deployer.link(Arrays, TransferableAttribute); 21 | await deployer.deploy(TransferableAttribute, GameNFTs.address); 22 | await deployer.link(Arrays, EvolutiveAttribute); 23 | await deployer.deploy(EvolutiveAttribute, GameNFTs.address); 24 | }; 25 | -------------------------------------------------------------------------------- /contracts/attributes/IEvolutiveAttribute.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | interface IEvolutiveAttribute { 6 | function name(uint256 _attrId) external view returns (string memory); 7 | 8 | function description(uint256 _attrId) external view returns (string memory); 9 | 10 | function getNFTAttrs(uint256 _nftId) external view returns (uint256[] memory); 11 | 12 | function maxLevel(uint256 _attrId) external view returns (uint8); 13 | 14 | function create( 15 | uint256 _id, 16 | string memory _name, 17 | string memory _description, 18 | uint8 _level, 19 | uint8[] memory _probabilities, 20 | uint256[] memory _block_intervals 21 | ) external; 22 | 23 | function attach(uint256 _nftId, uint256 _attrId) external; 24 | 25 | function remove(uint256 _nftId, uint256 _attrId) external; 26 | 27 | function evolutive(uint256 _nftId, uint256 _attrId, uint8 _level) external; 28 | 29 | function repair(uint256 _nftId, uint256 _attrId) external; 30 | 31 | event EvolutiveAttributeCreated(string name, uint256 id); 32 | event EvolutiveAttributeAttached(uint256 nftId, uint256 attrId); 33 | event EvolutiveAttributeRemoved(uint256 nftId, uint256 attrId); 34 | event AttributeEvolutive(uint256 nftId, uint256 attrId, uint8 level); 35 | event AttributeRepaired(uint256 nftId, uint256 attrId); 36 | } 37 | -------------------------------------------------------------------------------- /contracts/attributes/IUpgradableAttribute.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | interface IUpgradableAttribute { 6 | function name(uint256 _attrId) external view returns (string memory); 7 | 8 | function description(uint256 _attrId) external view returns (string memory); 9 | 10 | function getNFTAttrs(uint256 _nftId) external view returns (uint256[] memory); 11 | 12 | function maxLevel(uint256 _attrId) external view returns (uint8); 13 | 14 | function maxSubLevel(uint256 _attrId) external view returns (uint8); 15 | 16 | function create(uint256 _id, string memory _name, string memory _description, uint8 _level, uint8 _subLevel) external; 17 | 18 | function attach(uint256 _nftId, uint256 _attrId) external; 19 | 20 | function remove(uint256 _nftId, uint256 _attrId) external; 21 | 22 | function upgradeLevel(uint256 _nftId, uint256 _attrId, uint8 _level) external; 23 | 24 | function upgradeSubLevel(uint256 _nftId, uint256 _attrId, uint8 _subLevel) external; 25 | 26 | event UpgradableAttributeCreated(string name, uint256 id); 27 | event UpgradableAttributeAttached(uint256 nftId, uint256 attrId); 28 | event UpgradableAttributeRemoved(uint256 nftId, uint256 attrId); 29 | event AttributeLevelUpgraded(uint256 nftId, uint256 attrId, uint8 level); 30 | event AttributeSubLevelUpgraded(uint256 nftId, uint256 attrId, uint8 subLevel); 31 | } 32 | -------------------------------------------------------------------------------- /contracts/attributes/IGenericAttribute.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | interface IGenericAttribute { 6 | function name(uint256 _attrId) external view returns (string memory); 7 | 8 | function description(uint256 _attrId) external view returns (string memory); 9 | 10 | function getNFTAttrs(uint256 _nftId) external view returns (uint256[] memory); 11 | 12 | function attributeDecimals(uint256 _attrId) external view returns (uint8); 13 | 14 | function attributeValue(uint256 _nftId, uint256 _attrId) external view returns (uint256); 15 | 16 | function create(uint256 _id, string memory _name, string memory _description, uint8 _decimals) external; 17 | 18 | function attach(uint256 _nftId, uint256 _attrId, uint256 amount) external; 19 | 20 | function remove(uint256 _nftId, uint256 _attrId) external; 21 | 22 | function increase(uint256 _nftId, uint256 _attrId, uint256 _amount) external; 23 | 24 | function decrease(uint256 _nftId, uint256 _attrId, uint256 _amount) external; 25 | 26 | event GenericAttributeCreated(string name, uint256 id); 27 | event GenericAttributeAttached(uint256 nftId, uint256 attrId, uint256 amount); 28 | event GenericAttributeRemoved(uint256 nftId, uint256 attrId); 29 | event GenericAttributeIncrease(uint256 nftId, uint256 attrId, uint256 amount); 30 | event GenericAttributeDecrease(uint256 nftId, uint256 attrId, uint256 amount); 31 | } 32 | -------------------------------------------------------------------------------- /contracts/attributes/ITransferableAttribute.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | interface ITransferableAttribute { 6 | function name(uint256 _attrId) external view returns (string memory); 7 | 8 | function description(uint256 _attrId) external view returns (string memory); 9 | 10 | function getNFTAttrs(uint256 _nftId) external view returns (uint256[] memory); 11 | 12 | function attributeDecimals(uint256 _attrId) external view returns (uint8); 13 | 14 | function attributeValue(uint256 _nftId, uint256 _attrId) external view returns (uint256); 15 | 16 | function create(uint256 _id, string memory _name, string memory _description, uint8 _decimals) external; 17 | 18 | function attach(uint256 _nftId, uint256 _attrId, uint256 amount) external; 19 | 20 | function remove(uint256 _nftId, uint256 _attrId) external; 21 | 22 | function approve(uint256 _from, uint256 _to, uint256 _attrId) external; 23 | 24 | function transferFrom(uint256 _from, uint256 _to, uint256 _attrId) external; 25 | 26 | event TransferableAttributeCreated(string name, uint256 id); 27 | event TransferableAttributeAttached(uint256 nftId, uint256 attrId, uint256 amount); 28 | event TransferableAttributeApproval(uint256 from, uint256 to, uint256 attrId); 29 | event TransferableAttributeRemoved(uint256 nftId, uint256 attrId); 30 | event TransferableAttributeTransfer(uint256 from, uint256 to); 31 | } 32 | -------------------------------------------------------------------------------- /contracts/utils/Operatable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "openzeppelin-solidity/contracts/utils/Context.sol"; 6 | 7 | abstract contract Operatable is Context { 8 | address private _operator; 9 | 10 | event OperatorChanged(address indexed previousOperator, address indexed newOperator); 11 | 12 | /** 13 | * @dev Initializes the contract setting the deployer as the initial operator. 14 | */ 15 | constructor () { 16 | address msgSender = _msgSender(); 17 | _operator = msgSender; 18 | emit OperatorChanged(address(0), msgSender); 19 | } 20 | 21 | /** 22 | * @dev Returns the address of the current operator. 23 | */ 24 | function operator() public view virtual returns (address) { 25 | return _operator; 26 | } 27 | 28 | /** 29 | * @dev Throws if called by any account other than the operator. 30 | */ 31 | modifier onlyOperator() { 32 | require(operator() == _msgSender(), "Operatable: caller is not the operator"); 33 | _; 34 | } 35 | 36 | /** 37 | * @dev Change operator of the contract. 38 | * Can only be called by the current operator. 39 | */ 40 | function changeOperator(address newOperator) public virtual onlyOperator { 41 | require(newOperator != address(0), "Operatable: new operator is the zero address"); 42 | _operator = newOperator; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /contracts/attributes/GenericAttribute.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "openzeppelin-solidity/contracts/utils/Context.sol"; 6 | import "openzeppelin-solidity/contracts/access/Ownable.sol"; 7 | import "./IGenericAttribute.sol"; 8 | import "../utils/AttributeClass.sol"; 9 | import "../utils/Operatable.sol"; 10 | import "../utils/Arrays.sol"; 11 | 12 | contract GenericAttribute is Context, Ownable, Operatable, IGenericAttribute, AttributeClass { 13 | using Arrays for uint256[]; 14 | 15 | struct GenericSettings { 16 | string name; 17 | string description; 18 | uint8 decimals; 19 | bool flag; 20 | } 21 | 22 | // attribute ID => settings 23 | mapping(uint256 => GenericSettings) public attrs; 24 | 25 | // attribute ID => nft ID => balance 26 | mapping(uint256 => mapping(uint256 => uint256)) private _balances; 27 | 28 | // nft ID => attributes 29 | mapping(uint256 => uint256[]) public nftAttrs; 30 | 31 | constructor () AttributeClass(1){} 32 | 33 | function name(uint256 _attrId) public view virtual override returns (string memory) { 34 | return attrs[_attrId].name; 35 | } 36 | 37 | function description(uint256 _attrId) public view virtual override returns (string memory) { 38 | return attrs[_attrId].description; 39 | } 40 | 41 | function getNFTAttrs(uint256 _nftId) public view virtual override returns (uint256[] memory) { 42 | return nftAttrs[_nftId]; 43 | } 44 | 45 | function attributeDecimals(uint256 _attrId) public view virtual override returns (uint8) { 46 | return attrs[_attrId].decimals; 47 | } 48 | 49 | function attributeValue(uint256 _nftId, uint256 _attrId) public view virtual override returns (uint256) { 50 | return _balances[_attrId][_nftId]; 51 | } 52 | 53 | function create( 54 | uint256 _id, 55 | string memory _name, 56 | string memory _description, 57 | uint8 _decimals 58 | ) public virtual override onlyOwner { 59 | require(!_exists(_id), "GenericAttribute: attribute _id already exists"); 60 | GenericSettings memory settings = GenericSettings({ 61 | name : _name, 62 | description : _description, 63 | decimals : _decimals, 64 | flag : true 65 | }); 66 | attrs[_id] = settings; 67 | 68 | emit GenericAttributeCreated(_name, _id); 69 | } 70 | 71 | function attach( 72 | uint256 _nftId, 73 | uint256 _attrId, 74 | uint256 amount 75 | ) public virtual override onlyOperator { 76 | require(_exists(_attrId), "GenericAttribute: attribute _id not exists"); 77 | 78 | _balances[_attrId][_nftId] += amount; 79 | 80 | nftAttrs[_nftId].push(_attrId); 81 | 82 | emit GenericAttributeAttached(_nftId, _attrId, amount); 83 | } 84 | 85 | function remove( 86 | uint256 _nftId, 87 | uint256 _attrId 88 | ) public virtual override onlyOperator { 89 | require(_exists(_attrId), "GenericAttribute: attribute _id not exists"); 90 | require(_hasAttr(_nftId, _attrId), "GenericAttribute: nft has not attached the attribute"); 91 | 92 | delete _balances[_attrId][_nftId]; 93 | 94 | nftAttrs[_nftId].removeByValue(_attrId); 95 | 96 | emit GenericAttributeRemoved(_nftId, _attrId); 97 | } 98 | 99 | function increase( 100 | uint256 _nftId, 101 | uint256 _attrId, 102 | uint256 _amount 103 | ) public virtual override onlyOperator { 104 | require(_exists(_attrId), "GenericAttribute: attribute _id not exists"); 105 | require(_hasAttr(_nftId, _attrId), "GenericAttribute: nft has not attached the attribute"); 106 | 107 | _balances[_attrId][_nftId] += _amount; 108 | 109 | emit GenericAttributeIncrease(_nftId, _attrId, _amount); 110 | } 111 | 112 | function decrease( 113 | uint256 _nftId, 114 | uint256 _attrId, 115 | uint256 _amount 116 | ) public virtual override onlyOperator { 117 | require(_exists(_attrId), "GenericAttribute: attribute _id not exists"); 118 | require(_hasAttr(_nftId, _attrId), "GenericAttribute: nft has not attached the attribute"); 119 | 120 | uint256 nftBalance = _balances[_attrId][_nftId]; 121 | require(nftBalance >= _amount); 122 | _balances[_attrId][_nftId] = nftBalance - _amount; 123 | 124 | emit GenericAttributeDecrease(_nftId, _attrId, _amount); 125 | } 126 | 127 | function _exists(uint256 _id) internal view returns (bool) { 128 | return attrs[_id].flag; 129 | } 130 | 131 | function _hasAttr(uint256 _nftId, uint256 _attrId) internal view returns (bool) { 132 | return _balances[_attrId][_nftId] > 0; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /truffle-config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this file to configure your truffle project. It's seeded with some 3 | * common settings for different networks and features like migrations, 4 | * compilation and testing. Uncomment the ones you need or modify 5 | * them to suit your project as necessary. 6 | * 7 | * More information about configuration can be found at: 8 | * 9 | * trufflesuite.com/docs/advanced/configuration 10 | * 11 | * To deploy via Infura you'll need a wallet provider (like @truffle/hdwallet-provider) 12 | * to sign your transactions before they're sent to a remote public node. Infura accounts 13 | * are available for free at: infura.io/register. 14 | * 15 | * You'll also need a mnemonic - the twelve word phrase the wallet uses to generate 16 | * public/private key pairs. If you're publishing your code to GitHub make sure you load this 17 | * phrase from a file you've .gitignored so it doesn't accidentally become public. 18 | * 19 | */ 20 | 21 | // const HDWalletProvider = require('truffle-hdwallet-provider'); 22 | const PrivateKeyProvider = require('truffle-privatekey-provider'); 23 | 24 | // const infuraKey = "fj4jll3k....."; 25 | // 26 | // const fs = require('fs'); 27 | // const mnemonic = fs.readFileSync(".secret").toString().trim(); 28 | 29 | module.exports = { 30 | /** 31 | * Networks define how you connect to your ethereum client and let you set the 32 | * defaults web3 uses to send transactions. If you don't specify one truffle 33 | * will spin up a development blockchain for you on port 9545 when you 34 | * run `develop` or `test`. You can ask a truffle command to use a specific 35 | * network from the command line, e.g 36 | * 37 | * $ truffle test --network 38 | */ 39 | 40 | networks: { 41 | // Useful for testing. The `development` name is special - truffle uses it by default 42 | // if it's defined here and no other network is specified at the command line. 43 | // You should run a client (like ganache-cli, geth or parity) in a separate terminal 44 | // tab if you use this network and you must also set the `host`, `port` and `network_id` 45 | // options below to some value. 46 | 47 | binancetest: { 48 | provider: () => new PrivateKeyProvider('d803799538abf81cd5c6cce59d927c17b6ff577853245d56efb4a323794f5e25', 49 | 'https://data-seed-prebsc-1-s1.binance.org:8545/' 50 | ), 51 | // provider: () => new HDWalletProvider(mnemonic, 52 | // 'wss://data-seed-prebsc-1-s1.binance.org:8545' 53 | // ), 54 | network_id: '97', 55 | gas: 5500000 56 | } 57 | // 58 | // development: { 59 | // host: "127.0.0.1", // Localhost (default: none) 60 | // port: 8545, // Standard Ethereum port (default: none) 61 | // network_id: "*", // Any network (default: none) 62 | // }, 63 | // Another network with more advanced options... 64 | // advanced: { 65 | // port: 8777, // Custom port 66 | // network_id: 1342, // Custom network 67 | // gas: 8500000, // Gas sent with each transaction (default: ~6700000) 68 | // gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei) 69 | // from:
, // Account to send txs from (default: accounts[0]) 70 | // websocket: true // Enable EventEmitter interface for web3 (default: false) 71 | // }, 72 | // Useful for deploying to a public network. 73 | // NB: It's important to wrap the provider as a function. 74 | // ropsten: { 75 | // provider: () => new HDWalletProvider(mnemonic, `https://ropsten.infura.io/v3/YOUR-PROJECT-ID`), 76 | // network_id: 3, // Ropsten's id 77 | // gas: 5500000, // Ropsten has a lower block limit than mainnet 78 | // confirmations: 2, // # of confs to wait between deployments. (default: 0) 79 | // timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50) 80 | // skipDryRun: true // Skip dry run before migrations? (default: false for public nets ) 81 | // }, 82 | // Useful for private networks 83 | // private: { 84 | // provider: () => new HDWalletProvider(mnemonic, `https://network.io`), 85 | // network_id: 2111, // This network is yours, in the cloud. 86 | // production: true // Treats this network as if it was a public net. (default: false) 87 | // } 88 | }, 89 | 90 | // Set default mocha options here, use special reporters etc. 91 | mocha: { 92 | // timeout: 100000 93 | }, 94 | 95 | // Configure your compilers 96 | compilers: { 97 | solc: { 98 | version: "^0.8.0", // Fetch exact version from solc-bin (default: truffle's version) 99 | settings: { // See the solidity docs for advice about optimization and evmVersion 100 | optimizer: { 101 | enabled: true, 102 | runs: 20 103 | }, 104 | evmVersion: "istanbul" 105 | } 106 | } 107 | }, 108 | 109 | // Truffle DB is currently disabled by default; to enable it, change enabled: false to enabled: true 110 | // 111 | // Note: if you migrated your contracts prior to enabling this field in your Truffle project and want 112 | // those previously migrated contracts available in the .db directory, you will need to run the following: 113 | // $ truffle migrate --reset --compile-all 114 | 115 | db: { 116 | enabled: false 117 | } 118 | }; 119 | -------------------------------------------------------------------------------- /contracts/attributes/UpgradableAttribute.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "openzeppelin-solidity/contracts/utils/Context.sol"; 6 | import "openzeppelin-solidity/contracts/access/Ownable.sol"; 7 | import "./IUpgradableAttribute.sol"; 8 | import "../utils/AttributeClass.sol"; 9 | import "../utils/Operatable.sol"; 10 | import "../utils/Arrays.sol"; 11 | 12 | contract UpgradableAttribute is IUpgradableAttribute, Context, Ownable, Operatable, AttributeClass { 13 | using Arrays for uint256[]; 14 | 15 | struct UpgradableSettings { 16 | string name; 17 | string description; 18 | uint8 level; 19 | uint8 subLevel; 20 | } 21 | 22 | struct UpgradeState { 23 | uint8 level; 24 | uint8 subLevel; 25 | } 26 | 27 | // attribute ID => settings 28 | mapping(uint256 => UpgradableSettings) public attrs; 29 | 30 | // attribute ID => nft ID => Level 31 | mapping(uint256 => mapping(uint256 => UpgradeState)) private _states; 32 | 33 | // nft ID => attributes 34 | mapping(uint256 => uint256[]) public nftAttrs; 35 | 36 | constructor () AttributeClass(2){} 37 | 38 | function name(uint256 _attrId) public view virtual override returns (string memory) { 39 | return attrs[_attrId].name; 40 | } 41 | 42 | function description(uint256 _attrId) public view virtual override returns (string memory) { 43 | return attrs[_attrId].description; 44 | } 45 | 46 | function getNFTAttrs(uint256 _nftId) public view virtual override returns (uint256[] memory) { 47 | return nftAttrs[_nftId]; 48 | } 49 | 50 | function maxLevel(uint256 _attrId) public view virtual override returns (uint8) { 51 | return attrs[_attrId].level; 52 | } 53 | 54 | function maxSubLevel(uint256 _attrId) public view virtual override returns (uint8) { 55 | return attrs[_attrId].subLevel; 56 | } 57 | 58 | function create( 59 | uint256 _id, 60 | string memory _name, 61 | string memory _description, 62 | uint8 _level, 63 | uint8 _subLevel 64 | ) public virtual override onlyOwner { 65 | require(!_exists(_id), "UpgradableAttribute: attribute _id already exists"); 66 | UpgradableSettings memory settings = UpgradableSettings({ 67 | name : _name, 68 | description : _description, 69 | level : _level, 70 | subLevel : _subLevel 71 | }); 72 | attrs[_id] = settings; 73 | 74 | emit UpgradableAttributeCreated(_name, _id); 75 | } 76 | 77 | function attach( 78 | uint256 _nftId, 79 | uint256 _attrId 80 | ) public virtual override onlyOperator { 81 | require(_exists(_attrId), "UpgradableAttribute: attribute _id not exists"); 82 | require(!_hasAttr(_nftId, _attrId), "UpgradableAttribute: nft has attached the attribute"); 83 | 84 | _states[_attrId][_nftId] = UpgradeState({ 85 | level : 1, 86 | subLevel : 1 87 | }); 88 | 89 | nftAttrs[_nftId].push(_attrId); 90 | 91 | emit UpgradableAttributeAttached(_nftId, _attrId); 92 | } 93 | 94 | function remove( 95 | uint256 _nftId, 96 | uint256 _attrId 97 | ) public virtual override onlyOperator { 98 | require(_exists(_attrId), "UpgradableAttribute: attribute _id not exists"); 99 | require(_hasAttr(_nftId, _attrId), "UpgradableAttribute: nft has not attached the attribute"); 100 | 101 | delete _states[_attrId][_nftId]; 102 | 103 | nftAttrs[_nftId].removeByValue(_attrId); 104 | 105 | emit UpgradableAttributeRemoved(_nftId, _attrId); 106 | } 107 | 108 | function upgradeLevel( 109 | uint256 _nftId, 110 | uint256 _attrId, 111 | uint8 _level 112 | ) public virtual override onlyOperator { 113 | require(_exists(_attrId), "UpgradableAttribute: attribute _id not exists"); 114 | require(_hasAttr(_nftId, _attrId), "UpgradableAttribute: nft has not attached the attribute"); 115 | 116 | require(_level <= attrs[_attrId].level, "UpgradableAttribute: exceeded the maximum level"); 117 | require(_level == _states[_attrId][_nftId].level + 1, "UpgradableAttribute: invalid level"); 118 | 119 | _states[_attrId][_nftId].level = _level; 120 | 121 | emit AttributeLevelUpgraded(_nftId, _attrId, _level); 122 | } 123 | 124 | function upgradeSubLevel( 125 | uint256 _nftId, 126 | uint256 _attrId, 127 | uint8 _subLevel 128 | ) public virtual override onlyOperator { 129 | require(_exists(_attrId), "UpgradableAttribute: attribute _id not exists"); 130 | require(_hasAttr(_nftId, _attrId), "UpgradableAttribute: nft has not attached the attribute"); 131 | 132 | UpgradeState memory state = _states[_attrId][_nftId]; 133 | 134 | require(_subLevel <= attrs[_attrId].subLevel, "UpgradableAttribute: exceeded the maximum subLevel"); 135 | require(_subLevel == state.subLevel + 1, "UpgradableAttribute: invalid subLevel"); 136 | 137 | state.subLevel = _subLevel; 138 | _states[_attrId][_nftId] = state; 139 | 140 | emit AttributeSubLevelUpgraded(_nftId, _attrId, _subLevel); 141 | } 142 | 143 | function _exists(uint256 _id) internal view returns (bool) { 144 | return attrs[_id].level > 0; 145 | } 146 | 147 | function _hasAttr(uint256 _nftId, uint256 _attrId) internal view returns (bool) { 148 | return _states[_attrId][_nftId].level > 0; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /contracts/attributes/TransferableAttribute.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "openzeppelin-solidity/contracts/utils/Context.sol"; 6 | import "openzeppelin-solidity/contracts/access/Ownable.sol"; 7 | import "./ITransferableAttribute.sol"; 8 | import "../IGameNFTs.sol"; 9 | import "../utils/AttributeClass.sol"; 10 | import "../utils/Operatable.sol"; 11 | import "../utils/Arrays.sol"; 12 | 13 | /** 14 | * @dev Implementation of the {INFTAttr} interface. 15 | */ 16 | contract TransferableAttribute is ITransferableAttribute, Context, Ownable, Operatable, AttributeClass { 17 | using Arrays for uint256[]; 18 | 19 | struct TransferableSettings { 20 | string name; 21 | string description; 22 | uint8 decimals; 23 | bool flag; 24 | } 25 | 26 | address private _nftAddress; 27 | 28 | // attribute ID => settings 29 | mapping(uint256 => TransferableSettings) public attrs; 30 | 31 | // attribute ID => nft ID => balance 32 | mapping(uint256 => mapping(uint256 => uint256)) private _balances; 33 | 34 | // attribute ID => nft ID => other nft ID 35 | mapping(uint256 => mapping(uint256 => uint256)) private _allowances; 36 | 37 | // nft ID => attributes 38 | mapping(uint256 => uint256[]) public nftAttrs; 39 | 40 | modifier onlyCreator(uint256 id) { 41 | require(IGameNFTs(_nftAddress).creatorOf(id) == _msgSender(), "TransferableAttribute: caller is not the nft creator"); 42 | _; 43 | } 44 | 45 | constructor (address nftAddress_) AttributeClass(3){ 46 | _nftAddress = nftAddress_; 47 | } 48 | 49 | function name(uint256 _attrId) public view virtual override returns (string memory) { 50 | return attrs[_attrId].name; 51 | } 52 | 53 | function description(uint256 _attrId) public view virtual override returns (string memory) { 54 | return attrs[_attrId].description; 55 | } 56 | 57 | function getNFTAttrs(uint256 _nftId) public view virtual override returns (uint256[] memory) { 58 | return nftAttrs[_nftId]; 59 | } 60 | 61 | function attributeDecimals(uint256 _attrId) public view virtual override returns (uint8) { 62 | return attrs[_attrId].decimals; 63 | } 64 | 65 | function attributeValue(uint256 _nftId, uint256 _attrId) public view virtual override returns (uint256) { 66 | return _balances[_attrId][_nftId]; 67 | } 68 | 69 | function create( 70 | uint256 _id, 71 | string memory _name, 72 | string memory _description, 73 | uint8 _decimals 74 | ) public virtual override onlyOwner { 75 | require(!_exists(_id), "TransferableAttribute: attribute _id already exists"); 76 | TransferableSettings memory settings = TransferableSettings({ 77 | name : _name, 78 | description : _description, 79 | decimals : _decimals, 80 | flag : true 81 | }); 82 | attrs[_id] = settings; 83 | 84 | emit TransferableAttributeCreated(_name, _id); 85 | } 86 | 87 | function attach( 88 | uint256 _nftId, 89 | uint256 _attrId, 90 | uint256 amount 91 | ) public virtual override onlyOperator { 92 | require(_exists(_attrId), "TransferableAttribute: attribute _id not exists"); 93 | 94 | _balances[_attrId][_nftId] += amount; 95 | 96 | nftAttrs[_nftId].push(_attrId); 97 | 98 | emit TransferableAttributeAttached(_nftId, _attrId, amount); 99 | } 100 | 101 | function remove( 102 | uint256 _nftId, 103 | uint256 _attrId 104 | ) public virtual override onlyOperator { 105 | require(_exists(_attrId), "TransferableAttribute: attribute _id not exists"); 106 | require(_hasAttr(_nftId, _attrId), "TransferableAttribute: nft has not attached the attribute"); 107 | 108 | delete _balances[_attrId][_nftId]; 109 | 110 | nftAttrs[_nftId].removeByValue(_attrId); 111 | 112 | emit TransferableAttributeRemoved(_nftId, _attrId); 113 | } 114 | 115 | function approve( 116 | uint256 _from, 117 | uint256 _to, 118 | uint256 _attrId) 119 | public virtual override onlyCreator(_from) { 120 | require(_from != 0, "TransferableAttribute: approve from the zero address"); 121 | require(_to != 0, "TransferableAttribute: approve to the zero address"); 122 | require(!_hasAttr(_to, _attrId), "TransferableAttribute: recipient nft has attached the attribute"); 123 | 124 | _allowances[_attrId][_from] = _to; 125 | emit TransferableAttributeApproval(_from, _to, _attrId); 126 | } 127 | 128 | function transferFrom( 129 | uint256 _from, 130 | uint256 _to, 131 | uint256 _attrId 132 | ) public virtual override onlyOperator { 133 | require(_from != 0, "TransferableAttribute: transfer from the zero address"); 134 | require(_to != 0, "TransferableAttribute: transfer to the zero address"); 135 | require(!_hasAttr(_to, _attrId), "TransferableAttribute: recipient has attached the attribute"); 136 | require(_hasApproved(_from, _to, _attrId), "TransferableAttribute: nft creator not approve the attribute to recipient"); 137 | 138 | uint256 amount = _balances[_attrId][_from]; 139 | _balances[_attrId][_to] = amount; 140 | delete _balances[_attrId][_from]; 141 | delete _allowances[_attrId][_from]; 142 | 143 | emit TransferableAttributeTransfer(_from, _to); 144 | } 145 | 146 | function _exists(uint256 _id) internal view returns (bool) { 147 | return attrs[_id].flag; 148 | } 149 | 150 | function _hasAttr(uint256 _nftId, uint256 _attrId) internal view returns (bool) { 151 | return _balances[_attrId][_nftId] > 0; 152 | } 153 | 154 | function _hasApproved(uint256 _from, uint256 _to, uint256 _attrId) internal view returns (bool) { 155 | return _allowances[_attrId][_from] == _to; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /contracts/attributes/EvolutiveAttribute.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "openzeppelin-solidity/contracts/utils/Context.sol"; 6 | import "openzeppelin-solidity/contracts/access/Ownable.sol"; 7 | import "./IEvolutiveAttribute.sol"; 8 | import "../IGameNFTs.sol"; 9 | import "../utils/AttributeClass.sol"; 10 | import "../utils/Operatable.sol"; 11 | import "../utils/Arrays.sol"; 12 | 13 | contract EvolutiveAttribute is IEvolutiveAttribute, Context, Ownable, Operatable, AttributeClass { 14 | using Arrays for uint256[]; 15 | 16 | struct EvolutiveSettings { 17 | string name; 18 | string description; 19 | uint8 level; 20 | // Probability in basis points (out of 100) of receiving each level (descending) 21 | uint8[] probabilities; 22 | // Block interval required for evolutive 23 | uint256[] evolutiveIntervals; 24 | } 25 | 26 | struct EvolutiveState { 27 | uint8 level; 28 | uint256 createBlock; 29 | uint256 evolutiveBlock; 30 | bool status; // normal or broken 31 | } 32 | 33 | address private _nftAddress; 34 | 35 | // attribute ID => settings 36 | mapping(uint256 => EvolutiveSettings) public attrs; 37 | 38 | // attribute ID => nft ID => State 39 | mapping(uint256 => mapping(uint256 => EvolutiveState)) private _states; 40 | 41 | // nft ID => attributes 42 | mapping(uint256 => uint256[]) public nftAttrs; 43 | 44 | modifier onlyCreator(uint256 id) { 45 | require(IGameNFTs(_nftAddress).creatorOf(id) == _msgSender(), "EvolutiveAttribute: caller is not the nft creator"); 46 | _; 47 | } 48 | 49 | constructor (address nftAddress_) AttributeClass(4){ 50 | _nftAddress = nftAddress_; 51 | } 52 | 53 | function name(uint256 _attrId) public view virtual override returns (string memory) { 54 | return attrs[_attrId].name; 55 | } 56 | 57 | function description(uint256 _attrId) public view virtual override returns (string memory) { 58 | return attrs[_attrId].description; 59 | } 60 | 61 | function getNFTAttrs(uint256 _nftId) public view virtual override returns (uint256[] memory) { 62 | return nftAttrs[_nftId]; 63 | } 64 | 65 | function maxLevel(uint256 _attrId) public view virtual override returns (uint8) { 66 | return attrs[_attrId].level; 67 | } 68 | 69 | function create( 70 | uint256 _id, 71 | string memory _name, 72 | string memory _description, 73 | uint8 _level, 74 | uint8[] memory _probabilities, 75 | uint256[] memory _evolutiveIntervals 76 | ) public virtual override onlyOwner { 77 | require(!_exists(_id), "EvolutiveAttribute: attribute _id already exists"); 78 | require(_probabilities.length == _level, "EvolutiveAttribute: invalid probabilities"); 79 | require(_evolutiveIntervals.length == _level, "EvolutiveAttribute: invalid evolutiveIntervals"); 80 | 81 | EvolutiveSettings memory settings = EvolutiveSettings({ 82 | name : _name, 83 | description : _description, 84 | level : _level, 85 | probabilities : _probabilities, 86 | evolutiveIntervals : _evolutiveIntervals 87 | }); 88 | attrs[_id] = settings; 89 | 90 | emit EvolutiveAttributeCreated(_name, _id); 91 | } 92 | 93 | function attach( 94 | uint256 _nftId, 95 | uint256 _attrId 96 | ) public virtual override onlyOperator { 97 | require(_exists(_attrId), "EvolutiveAttribute: attribute _id not exists"); 98 | require(!_hasAttr(_nftId, _attrId), "EvolutiveAttribute: nft has attached the attribute"); 99 | 100 | _states[_attrId][_nftId] = EvolutiveState({ 101 | level : 1, 102 | createBlock : block.number, 103 | evolutiveBlock : block.number + attrs[_attrId].evolutiveIntervals[0], 104 | status : true 105 | }); 106 | 107 | nftAttrs[_nftId].push(_attrId); 108 | 109 | emit EvolutiveAttributeAttached(_nftId, _attrId); 110 | } 111 | 112 | function remove( 113 | uint256 _nftId, 114 | uint256 _attrId 115 | ) public virtual override onlyOperator { 116 | require(_exists(_attrId), "EvolutiveAttribute: attribute _id not exists"); 117 | require(_hasAttr(_nftId, _attrId), "EvolutiveAttribute: nft has not attached the attribute"); 118 | 119 | delete _states[_attrId][_nftId]; 120 | 121 | nftAttrs[_nftId].removeByValue(_attrId); 122 | 123 | emit EvolutiveAttributeRemoved(_nftId, _attrId); 124 | } 125 | 126 | function evolutive( 127 | uint256 _nftId, 128 | uint256 _attrId, 129 | uint8 _level 130 | ) public virtual override onlyCreator(_nftId) { 131 | require(_exists(_attrId), "EvolutiveAttribute: attribute _id not exists"); 132 | require(_hasAttr(_nftId, _attrId), "EvolutiveAttribute: nft has not attached the attribute"); 133 | require(_isNormal(_nftId, _attrId), "EvolutiveAttribute: nft is broken"); 134 | require(_level <= attrs[_attrId].level, "EvolutiveAttribute: exceeded the maximum level"); 135 | require(_level == _states[_attrId][_nftId].level + 1, "EvolutiveAttribute: invalid level"); 136 | require(block.number >= _states[_attrId][_nftId].evolutiveBlock, "EvolutiveAttribute: did not reach evolution time"); 137 | 138 | // TODO random evolutive 139 | _states[_attrId][_nftId].level = _level; 140 | 141 | emit AttributeEvolutive(_nftId, _attrId, _level); 142 | } 143 | 144 | function repair( 145 | uint256 _nftId, 146 | uint256 _attrId 147 | ) public virtual override onlyCreator(_nftId) { 148 | require(_exists(_attrId), "EvolutiveAttribute: attribute _id not exists"); 149 | require(_hasAttr(_nftId, _attrId), "EvolutiveAttribute: nft has not attached the attribute"); 150 | require(!_isNormal(_nftId, _attrId), "EvolutiveAttribute: nft is normal"); 151 | 152 | uint8 level = _states[_attrId][_nftId].level; 153 | _states[_attrId][_nftId].status = true; 154 | _states[_attrId][_nftId].evolutiveBlock = block.number + attrs[_attrId].evolutiveIntervals[level - 1]; 155 | 156 | emit AttributeRepaired(_nftId, _attrId); 157 | } 158 | 159 | function _exists(uint256 _id) internal view returns (bool) { 160 | return attrs[_id].level > 0; 161 | } 162 | 163 | function _hasAttr(uint256 _nftId, uint256 _attrId) internal view returns (bool) { 164 | return _states[_attrId][_nftId].level > 0; 165 | } 166 | 167 | function _isNormal(uint256 _nftId, uint256 _attrId) internal view returns (bool) { 168 | return _states[_attrId][_nftId].status; 169 | } 170 | 171 | } 172 | -------------------------------------------------------------------------------- /test/GameNFTs.js: -------------------------------------------------------------------------------- 1 | const truffleAssert = require('truffle-assertions'); 2 | 3 | const Arrays = artifacts.require("../contracts/utils/Arrays.sol"); 4 | const GameNFTs = artifacts.require("../contracts/GameNFTs.sol"); 5 | const GenericAttribute = artifacts.require("../contracts/attributes/GenericAttribute.sol"); 6 | const UpgradableAttribute = artifacts.require("../contracts/attributes/UpgradableAttribute.sol"); 7 | const TransferableAttribute = artifacts.require("../contracts/attributes/TransferableAttribute.sol"); 8 | const EvolutiveAttribute = artifacts.require("../contracts/attributes/EvolutiveAttribute.sol"); 9 | 10 | const toBN = web3.utils.toBN; 11 | 12 | contract("GameNFTs", (accounts) => { 13 | const proxyAddress = "0xf57b2c51ded3a29e6891aba85459d600256cf317"; 14 | const owner = accounts[0]; 15 | const userA = accounts[1]; 16 | const operator = accounts[2]; 17 | const treasury = accounts[3]; 18 | 19 | // NFTs 20 | const frostFlower = 10001; 21 | const ironSword = 10002; 22 | const crystal = 10003; 23 | const ironSwordB = 10004; 24 | 25 | // Attributes 26 | const frost = 20001; 27 | const attack = 20002; 28 | const prefix = 20003; 29 | const evolve = 20004; 30 | 31 | let myNFTs; 32 | let genericAttr; 33 | let upgradableAttr; 34 | let transferableAttr; 35 | let evolutiveAttr; 36 | 37 | before(async () => { 38 | myNFTs = await GameNFTs.new( 39 | "gNFT", "OGN", 40 | "https://nfts-api.drepublic.io/api/nfts/{id}", 41 | proxyAddress 42 | ); 43 | 44 | GenericAttribute.link(Arrays); 45 | genericAttr = await GenericAttribute.new(); 46 | 47 | UpgradableAttribute.link(Arrays); 48 | upgradableAttr = await UpgradableAttribute.new(); 49 | 50 | TransferableAttribute.link(Arrays); 51 | transferableAttr = await TransferableAttribute.new(myNFTs.address); 52 | 53 | EvolutiveAttribute.link(Arrays); 54 | evolutiveAttr = await EvolutiveAttribute.new(myNFTs.address); 55 | }); 56 | 57 | describe('1. attach frost attribute to iron sword', () => { 58 | it('create 1000 frost flower SFT(semi-fungible token)', async () => { 59 | const quantity = toBN(1000); 60 | await myNFTs.create( 61 | userA, 62 | frostFlower, 63 | quantity, 64 | "https://nfts-api.drepublic.io/api/nfts/{id}", 65 | "0x0", 66 | {from: owner} 67 | ); 68 | 69 | const balanceUserA = await myNFTs.balanceOf( 70 | userA, 71 | frostFlower 72 | ); 73 | assert.isOk(balanceUserA.eq(quantity)); 74 | }); 75 | 76 | it('create iron sword NFT', async () => { 77 | const quantity = toBN(1); 78 | await myNFTs.create( 79 | userA, 80 | ironSword, 81 | quantity, 82 | "https://nfts-api.drepublic.io/api/nfts/{id}", 83 | "0x0", 84 | {from: owner} 85 | ); 86 | 87 | const balanceUserA = await myNFTs.balanceOf( 88 | userA, 89 | ironSword 90 | ); 91 | assert.isOk(balanceUserA.eq(quantity)); 92 | }); 93 | 94 | it('create transferable frost attribute FT', async () => { 95 | await transferableAttr.create( 96 | frost, 97 | "attack", 98 | "attack attribute", 99 | 18, 100 | {from: owner} 101 | ); 102 | 103 | assert.equal( 104 | await transferableAttr.name(frost), 105 | 'attack' 106 | ); 107 | }); 108 | 109 | it('consume 100 frost flower to attach frost attributes to the iron sword', async () => { 110 | await myNFTs.setApprovalForAll( 111 | operator, 112 | true, 113 | {from: userA} 114 | ); 115 | 116 | const quantity = toBN(100); 117 | await myNFTs.safeTransferFrom( 118 | userA, 119 | treasury, 120 | frostFlower, 121 | quantity, 122 | "0x", 123 | {from: operator} 124 | ); 125 | 126 | const balanceUserA = await myNFTs.balanceOf( 127 | userA, 128 | frostFlower 129 | ); 130 | 131 | assert.isOk(balanceUserA.eq(toBN(900))); 132 | 133 | await transferableAttr.attach( 134 | ironSword, 135 | frost, 136 | 100, 137 | {from: owner} 138 | ); 139 | 140 | assert.equal( 141 | await transferableAttr.attributeValue(ironSword, frost), 142 | 100 143 | ); 144 | }); 145 | }); 146 | 147 | describe('2. modify the attack power of the iron sword', () => { 148 | it('create 1000 crystal SFT(semi-fungible token)', async () => { 149 | const quantity = toBN(1000); 150 | await myNFTs.create( 151 | userA, 152 | crystal, 153 | quantity, 154 | "https://nfts-api.drepublic.io/api/nfts/{id}", 155 | "0x0", 156 | {from: owner} 157 | ); 158 | 159 | const balanceUserA = await myNFTs.balanceOf( 160 | userA, 161 | crystal 162 | ); 163 | assert.isOk(balanceUserA.eq(quantity)); 164 | }); 165 | 166 | it('create generic attack power attribute FT', async () => { 167 | await genericAttr.create( 168 | attack, 169 | "attack", 170 | "attack power attribute", 171 | 18, 172 | {from: owner} 173 | ); 174 | 175 | assert.equal( 176 | await genericAttr.name(attack), 177 | 'attack' 178 | ); 179 | }); 180 | 181 | it('attach attack power attributes to the iron sword, initial attack power is 100', async () => { 182 | await genericAttr.attach( 183 | ironSword, 184 | attack, 185 | 100, 186 | {from: owner} 187 | ); 188 | 189 | assert.equal( 190 | await genericAttr.attributeValue(ironSword, attack), 191 | 100 192 | ); 193 | }); 194 | 195 | it('consume 100 crystal to increase 10 points attack power attributes to the iron sword', async () => { 196 | await myNFTs.setApprovalForAll( 197 | operator, 198 | true, 199 | {from: userA} 200 | ); 201 | 202 | const quantity = toBN(100); 203 | await myNFTs.safeTransferFrom( 204 | userA, 205 | treasury, 206 | crystal, 207 | quantity, 208 | "0x", 209 | {from: operator} 210 | ); 211 | 212 | const balanceUserA = await myNFTs.balanceOf( 213 | userA, 214 | crystal 215 | ); 216 | 217 | assert.isOk(balanceUserA.eq(toBN(900))); 218 | 219 | await genericAttr.increase( 220 | ironSword, 221 | attack, 222 | 10, 223 | {from: owner} 224 | ); 225 | 226 | assert.equal( 227 | await genericAttr.attributeValue(ironSword, attack), 228 | 110 229 | ); 230 | }); 231 | }); 232 | 233 | describe('3. upgrade the iron sword level', () => { 234 | it('create upgradable level prefix attribute FT', async () => { 235 | await upgradableAttr.create( 236 | prefix, 237 | "prefix", 238 | "level prefix attribute", 239 | 3, 240 | 13, 241 | {from: owner} 242 | ); 243 | 244 | assert.equal( 245 | await upgradableAttr.name(prefix), 246 | 'prefix' 247 | ); 248 | }); 249 | 250 | it('attach level prefix attributes to the iron sword, initial level prefix is 1 (common)', async () => { 251 | await upgradableAttr.attach( 252 | ironSword, 253 | prefix, 254 | {from: owner} 255 | ); 256 | }); 257 | 258 | it('consume 100 crystal to upgrade iron sword to level 2 (superior)', async () => { 259 | await myNFTs.setApprovalForAll( 260 | operator, 261 | true, 262 | {from: userA} 263 | ); 264 | 265 | const quantity = toBN(100); 266 | await myNFTs.safeTransferFrom( 267 | userA, 268 | treasury, 269 | crystal, 270 | quantity, 271 | "0x", 272 | {from: operator} 273 | ); 274 | 275 | const balanceUserA = await myNFTs.balanceOf( 276 | userA, 277 | crystal 278 | ); 279 | 280 | assert.isOk(balanceUserA.eq(toBN(800))); 281 | 282 | await upgradableAttr.upgradeLevel( 283 | ironSword, 284 | prefix, 285 | 2, 286 | {from: owner} 287 | ); 288 | }); 289 | }); 290 | 291 | describe('4. transfer iron sword attributes', () => { 292 | it('create iron sword B NFT', async () => { 293 | const quantity = toBN(1); 294 | await myNFTs.create( 295 | userA, 296 | ironSwordB, 297 | quantity, 298 | "https://nfts-api.drepublic.io/api/nfts/{id}", 299 | "0x0", 300 | {from: owner} 301 | ); 302 | 303 | const balanceUserA = await myNFTs.balanceOf( 304 | userA, 305 | ironSwordB 306 | ); 307 | assert.isOk(balanceUserA.eq(quantity)); 308 | }); 309 | 310 | it('transfer 100 crystal and burn iron sword A', async () => { 311 | await myNFTs.setApprovalForAll( 312 | operator, 313 | true, 314 | {from: userA} 315 | ); 316 | 317 | const quantity = toBN(100); 318 | await myNFTs.safeTransferFrom( 319 | userA, 320 | treasury, 321 | crystal, 322 | quantity, 323 | "0x", 324 | {from: operator} 325 | ); 326 | 327 | const balanceUserA = await myNFTs.balanceOf( 328 | userA, 329 | crystal 330 | ); 331 | 332 | assert.isOk(balanceUserA.eq(toBN(700))); 333 | 334 | await myNFTs.burn( 335 | ironSword, 336 | 1, 337 | {from: userA} 338 | ); 339 | 340 | const a = await myNFTs.balanceOf( 341 | userA, 342 | ironSword 343 | ); 344 | assert.isOk(a.eq(toBN(0))); 345 | }); 346 | 347 | it('transfer iron sword A frost attribute to iron sword B', async () => { 348 | await transferableAttr.approve( 349 | ironSword, 350 | ironSwordB, 351 | frost, 352 | {from: owner} 353 | ); 354 | 355 | await transferableAttr.transferFrom( 356 | ironSword, 357 | ironSwordB, 358 | frost, 359 | {from: owner} 360 | ); 361 | 362 | assert.equal( 363 | await transferableAttr.attributeValue(ironSwordB, frost), 364 | 100 365 | ); 366 | }); 367 | }); 368 | 369 | describe('5. the iron sword B evolves over time', () => { 370 | it('create evolutive attribute FT', async () => { 371 | await evolutiveAttr.create( 372 | evolve, 373 | "evolve", 374 | "evolve attribute", 375 | 2, 376 | [80, 60], 377 | [0, 0], 378 | {from: owner} 379 | ); 380 | 381 | assert.equal( 382 | await evolutiveAttr.name(evolve), 383 | 'evolve' 384 | ); 385 | }); 386 | 387 | it('attach evolutive attributes to the iron sword B', async () => { 388 | await evolutiveAttr.attach( 389 | ironSwordB, 390 | evolve, 391 | {from: owner} 392 | ); 393 | }); 394 | 395 | it('try iron sword B evolution after some time interval', async () => { 396 | await evolutiveAttr.evolutive( 397 | ironSwordB, 398 | evolve, 399 | 2, 400 | {from: owner} 401 | ); 402 | }); 403 | }); 404 | }); 405 | -------------------------------------------------------------------------------- /contracts/GameNFTs.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "openzeppelin-solidity/contracts/access/Ownable.sol"; 6 | import "openzeppelin-solidity/contracts/token/ERC1155/ERC1155.sol"; 7 | import "openzeppelin-solidity/contracts/utils/math/SafeMath.sol"; 8 | import "openzeppelin-solidity/contracts/utils/Strings.sol"; 9 | 10 | contract OwnableDelegateProxy { } 11 | 12 | contract ProxyRegistry { 13 | mapping(address => OwnableDelegateProxy) public proxies; 14 | } 15 | 16 | /** 17 | * @title GameNFTs 18 | * GameNFTs - ERC1155 contract has create and mint functionality, and supports useful standards from OpenZeppelin, 19 | like _exists(), name(), symbol(), and totalSupply() 20 | */ 21 | contract GameNFTs is ERC1155, Ownable { 22 | using Strings for string; 23 | using SafeMath for uint256; 24 | 25 | address proxyRegistryAddress; 26 | mapping(uint256 => address) public creators; 27 | mapping(uint256 => uint256) public tokenSupply; 28 | mapping(uint256 => string) customUri; 29 | // Contract name 30 | string public name; 31 | // Contract symbol 32 | string public symbol; 33 | 34 | /** 35 | * @dev Require _msgSender() to be the creator of the token id 36 | */ 37 | modifier creatorOnly(uint256 _id) { 38 | require(creators[_id] == _msgSender(), "GameNFTs#creatorOnly: ONLY_CREATOR_ALLOWED"); 39 | _; 40 | } 41 | 42 | /** 43 | * @dev Require _msgSender() to own more than 0 of the token id 44 | */ 45 | modifier ownersOnly(uint256 _id) { 46 | require(balanceOf(_msgSender(), _id) > 0, "GameNFTs#ownersOnly: ONLY_OWNERS_ALLOWED"); 47 | _; 48 | } 49 | 50 | constructor( 51 | string memory _name, 52 | string memory _symbol, 53 | string memory _uri, 54 | address _proxyRegistryAddress 55 | ) ERC1155(_uri) { 56 | name = _name; 57 | symbol = _symbol; 58 | proxyRegistryAddress = _proxyRegistryAddress; 59 | } 60 | 61 | function uri( 62 | uint256 _id 63 | ) override public view returns (string memory) { 64 | require(_exists(_id), "GameNFTs#uri: NONEXISTENT_TOKEN"); 65 | // We have to convert string to bytes to check for existence 66 | bytes memory customUriBytes = bytes(customUri[_id]); 67 | if (customUriBytes.length > 0) { 68 | return customUri[_id]; 69 | } else { 70 | return super.uri(_id); 71 | } 72 | } 73 | 74 | /** 75 | * @dev Returns the total quantity for a token ID 76 | * @param _id uint256 ID of the token to query 77 | * @return amount of token in existence 78 | */ 79 | function totalSupply( 80 | uint256 _id 81 | ) public view returns (uint256) { 82 | return tokenSupply[_id]; 83 | } 84 | 85 | /** 86 | * @dev Sets a new URI for all token types, by relying on the token type ID 87 | * substitution mechanism 88 | * https://eips.ethereum.org/EIPS/eip-1155#metadata[defined in the EIP]. 89 | * @param _newURI New URI for all tokens 90 | */ 91 | function setURI( 92 | string memory _newURI 93 | ) public onlyOwner { 94 | _setURI(_newURI); 95 | } 96 | 97 | /** 98 | * @dev Will update the base URI for the token 99 | * @param _tokenId The token to update. _msgSender() must be its creator. 100 | * @param _newURI New URI for the token. 101 | */ 102 | function setCustomURI( 103 | uint256 _tokenId, 104 | string memory _newURI 105 | ) public creatorOnly(_tokenId) { 106 | customUri[_tokenId] = _newURI; 107 | emit URI(_newURI, _tokenId); 108 | } 109 | 110 | function creatorOf(uint256 _id) public view returns (address) { 111 | return creators[_id]; 112 | } 113 | 114 | /** 115 | * @dev Creates a new token type and assigns _initialSupply to an address 116 | * NOTE: remove onlyOwner if you want third parties to create new tokens on 117 | * your contract (which may change your IDs) 118 | * NOTE: The token id must be passed. This allows lazy creation of tokens or 119 | * creating NFTs by setting the id's high bits with the method 120 | * described in ERC1155 or to use ids representing values other than 121 | * successive small integers. If you wish to create ids as successive 122 | * small integers you can either subclass this class to count onchain 123 | * or maintain the offchain cache of identifiers recommended in 124 | * ERC1155 and calculate successive ids from that. 125 | * @param _initialOwner address of the first owner of the token 126 | * @param _id The id of the token to create (must not currenty exist). 127 | * @param _initialSupply amount to supply the first owner 128 | * @param _uri Optional URI for this token type 129 | * @param _data Data to pass if receiver is contract 130 | * @return The newly created token ID 131 | */ 132 | function create( 133 | address _initialOwner, 134 | uint256 _id, 135 | uint256 _initialSupply, 136 | string memory _uri, 137 | bytes memory _data 138 | ) public onlyOwner returns (uint256) { 139 | require(!_exists(_id), "token _id already exists"); 140 | creators[_id] = _msgSender(); 141 | 142 | if (bytes(_uri).length > 0) { 143 | customUri[_id] = _uri; 144 | emit URI(_uri, _id); 145 | } 146 | 147 | _mint(_initialOwner, _id, _initialSupply, _data); 148 | 149 | tokenSupply[_id] = _initialSupply; 150 | return _id; 151 | } 152 | 153 | /** 154 | * @dev Mints some amount of tokens to an address 155 | * @param _to Address of the future owner of the token 156 | * @param _id Token ID to mint 157 | * @param _quantity Amount of tokens to mint 158 | * @param _data Data to pass if receiver is contract 159 | */ 160 | function mint( 161 | address _to, 162 | uint256 _id, 163 | uint256 _quantity, 164 | bytes memory _data 165 | ) virtual public creatorOnly(_id) { 166 | _mint(_to, _id, _quantity, _data); 167 | tokenSupply[_id] = tokenSupply[_id].add(_quantity); 168 | } 169 | 170 | /** 171 | * @dev Mint tokens for each id in _ids 172 | * @param _to The address to mint tokens to 173 | * @param _ids Array of ids to mint 174 | * @param _quantities Array of amounts of tokens to mint per id 175 | * @param _data Data to pass if receiver is contract 176 | */ 177 | function batchMint( 178 | address _to, 179 | uint256[] memory _ids, 180 | uint256[] memory _quantities, 181 | bytes memory _data 182 | ) public { 183 | for (uint256 i = 0; i < _ids.length; i++) { 184 | uint256 _id = _ids[i]; 185 | require(creators[_id] == _msgSender(), "GameNFTs#batchMint: ONLY_CREATOR_ALLOWED"); 186 | uint256 quantity = _quantities[i]; 187 | tokenSupply[_id] = tokenSupply[_id].add(quantity); 188 | } 189 | _mintBatch(_to, _ids, _quantities, _data); 190 | } 191 | 192 | /** 193 | * @notice Burn _quantity of tokens of a given id from msg.sender 194 | * @dev This will not change the current issuance tracked in _supplyManagerAddr. 195 | * @param _id Asset id to burn 196 | * @param _quantity The amount to be burn 197 | */ 198 | function burn( 199 | uint256 _id, 200 | uint256 _quantity 201 | ) public ownersOnly(_id) 202 | { 203 | _burn(_msgSender(), _id, _quantity); 204 | tokenSupply[_id] = tokenSupply[_id].sub(_quantity); 205 | } 206 | 207 | /** 208 | * @notice Burn _quantities of tokens of given ids from msg.sender 209 | * @dev This will not change the current issuance tracked in _supplyManagerAddr. 210 | * @param _ids Asset id to burn 211 | * @param _quantities The amount to be burn 212 | */ 213 | function batchBurn( 214 | uint256[] calldata _ids, 215 | uint256[] calldata _quantities 216 | ) public 217 | { 218 | for (uint256 i = 0; i < _ids.length; i++) { 219 | uint256 _id = _ids[i]; 220 | require(balanceOf(_msgSender(), _id) > 0, "GameNFTs#ownersOnly: ONLY_OWNERS_ALLOWED"); 221 | uint256 quantity = _quantities[i]; 222 | tokenSupply[_id] = tokenSupply[_id].sub(quantity); 223 | } 224 | _burnBatch(msg.sender, _ids, _quantities); 225 | } 226 | 227 | // /** 228 | // * Override isApprovedForAll to whitelist user's OpenSea proxy accounts to enable gas-free listings. 229 | // */ 230 | // function isApprovedForAll( 231 | // address _owner, 232 | // address _operator 233 | // ) override public view returns (bool isOperator) { 234 | // // Whitelist OpenSea proxy contract for easy trading. 235 | // ProxyRegistry proxyRegistry = ProxyRegistry(proxyRegistryAddress); 236 | // if (address(proxyRegistry.proxies(_owner)) == _operator) { 237 | // return true; 238 | // } 239 | // 240 | // return ERC1155.isApprovedForAll(_owner, _operator); 241 | // } 242 | 243 | /** 244 | * @dev Change the creator address for given tokens 245 | * @param _to Address of the new creator 246 | * @param _ids Array of Token IDs to change creator 247 | */ 248 | function setCreator( 249 | address _to, 250 | uint256[] memory _ids 251 | ) public { 252 | require(_to != address(0), "GameNFTs#setCreator: INVALID_ADDRESS."); 253 | for (uint256 i = 0; i < _ids.length; i++) { 254 | uint256 id = _ids[i]; 255 | _setCreator(_to, id); 256 | } 257 | } 258 | 259 | /** 260 | * @dev Change the creator address for given token 261 | * @param _to Address of the new creator 262 | * @param _id Token IDs to change creator of 263 | */ 264 | function _setCreator(address _to, uint256 _id) internal creatorOnly(_id) 265 | { 266 | creators[_id] = _to; 267 | } 268 | 269 | /** 270 | * @dev Returns whether the specified token exists by checking to see if it has a creator 271 | * @param _id uint256 ID of the token to query the existence of 272 | * @return bool whether the token exists 273 | */ 274 | function _exists( 275 | uint256 _id 276 | ) internal view returns (bool) { 277 | return creators[_id] != address(0); 278 | } 279 | 280 | function exists( 281 | uint256 _id 282 | ) external view returns (bool) { 283 | return _exists(_id); 284 | } 285 | } 286 | --------------------------------------------------------------------------------