├── .jshintrc ├── migrations ├── 1_initial_migration.js ├── 2_deploy_pol_timestamp.js ├── 3_deploy_pol_rewardtiers.js └── 4_deploy_pol_contract.js ├── developers ├── audits │ └── mythx │ │ ├── POL-v2-Solidity-v0.6.12-deepscan_9823b22a94aad68372545a72.pdf │ │ ├── POL-v2-Solidity-v0.6.12-quickscan_cca1c1fe81878c6066bc9f61.pdf │ │ └── POL-v2-Solidity-v0.6.12-standardscan_3008b3401800cb81836daef1.pdf └── uml │ └── @startuml.umldoc ├── .gitignore ├── contracts ├── Migrations.sol ├── ISparkleRewardTiers.sol ├── ISparkleTimestamp.sol ├── SparkleRewardTiers.sol ├── SparkleTimestamp.sol └── SparkleLoyalty.sol ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── test ├── helpers │ └── truffle-time-helpers.js ├── TestSparkleLoyalty.js ├── TestSparkleRewardTiers.js └── TestSparkleLoyalty-Tier0.js ├── package.json ├── truffle-config.js └── README.md /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esversion": 8 3 | } 4 | -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | const Migrations = artifacts.require("Migrations"); 2 | 3 | module.exports = function(deployer) { 4 | deployer.deploy(Migrations); 5 | }; 6 | -------------------------------------------------------------------------------- /developers/audits/mythx/POL-v2-Solidity-v0.6.12-deepscan_9823b22a94aad68372545a72.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sparkleloyalty/Sparkle-Proof-Of-Loyalty/HEAD/developers/audits/mythx/POL-v2-Solidity-v0.6.12-deepscan_9823b22a94aad68372545a72.pdf -------------------------------------------------------------------------------- /developers/audits/mythx/POL-v2-Solidity-v0.6.12-quickscan_cca1c1fe81878c6066bc9f61.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sparkleloyalty/Sparkle-Proof-Of-Loyalty/HEAD/developers/audits/mythx/POL-v2-Solidity-v0.6.12-quickscan_cca1c1fe81878c6066bc9f61.pdf -------------------------------------------------------------------------------- /developers/audits/mythx/POL-v2-Solidity-v0.6.12-standardscan_3008b3401800cb81836daef1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sparkleloyalty/Sparkle-Proof-Of-Loyalty/HEAD/developers/audits/mythx/POL-v2-Solidity-v0.6.12-standardscan_3008b3401800cb81836daef1.pdf -------------------------------------------------------------------------------- /migrations/2_deploy_pol_timestamp.js: -------------------------------------------------------------------------------- 1 | const SparkleTimestamp = artifacts.require('./SparkleTimestamp'); 2 | 3 | module.exports = function(deployer, network, accounts) { 4 | return deployer.then(() => { 5 | return deployer.deploy(SparkleTimestamp); 6 | }); 7 | }; -------------------------------------------------------------------------------- /migrations/3_deploy_pol_rewardtiers.js: -------------------------------------------------------------------------------- 1 | const SparkleRewardTiers = artifacts.require('./SparkleRewardTiers'); 2 | 3 | module.exports = function(deployer, network, accounts) { 4 | return deployer.then(() => { 5 | return deployer.deploy(SparkleRewardTiers); 6 | }); 7 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignored files 2 | #--------------- 3 | .DS_Store 4 | .env 5 | *.sh 6 | *.txt 7 | *.flat 8 | 9 | # Ignore folders 10 | #---------------- 11 | Documents/ 12 | build/ 13 | scripts/ 14 | .ganachedb/ 15 | 16 | # Ignore node_modules but include OpenZeppelin 17 | #---------------------------------------------- 18 | !node_modules/ 19 | node_modules/* 20 | !node_modules/openzeppelin-solidity/ 21 | node_modules/openzeppelin-solidity/* 22 | !node_modules/openzeppelin-solidity/contracts/ 23 | -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.6.12; 4 | 5 | contract Migrations { 6 | address public owner; 7 | uint public last_completed_migration; 8 | 9 | constructor() public { 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 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /migrations/4_deploy_pol_contract.js: -------------------------------------------------------------------------------- 1 | const SparkleLoyalty = artifacts.require('./SparkleLoyalty'); 2 | const SparkleTimestamp = artifacts.require('./SparkleTimestamp'); 3 | const SparkleRewardTiers = artifacts.require('./SparkleRewardTiers'); 4 | 5 | const onchainSparkleTokenAddressGanache = '0x14d8d4e089a4ae60f315be178434c651d11f9b9a'; // GanacheCLI 6 | const onchainSparkleTokenAddressRopten = '0xb0550ae71eFec2e163f9aDb540b32641511b0a88'; // Ropsten 7 | const onchainSparkleTokenAddressMainnet = '0x0'; // Mainnet 8 | 9 | module.exports = async function(deployer, network, accounts) { 10 | return deployer 11 | .then(() => { 12 | return deployer.deploy(SparkleRewardTiers, {overwrite: false}); 13 | }) 14 | .then(() => { 15 | return deployer.deploy(SparkleTimestamp, {overwrite: false}); 16 | }).then(() => { 17 | const tokenAddress = 0x0; 18 | if(network == 'ropsten') { 19 | tokenAddress = onchainSparkleTokenAddressRopten; 20 | } 21 | else if(network == 'mainnet') { 22 | tokenAddress = onchainSparkleTokenAddressMainnet; 23 | } 24 | else { 25 | tokenAddress = onchainSparkleTokenAddressGanache; 26 | } 27 | 28 | const tokenAddress = onchainSparkleTokenAddress; 29 | const treasuryAddress = accounts[1]; 30 | const collectionAddress = accounts[4]; 31 | const timestampAddress = SparkleTimestamp.address; 32 | const tiersAddress = SparkleRewardTiers.address; 33 | 34 | return deployer.deploy(SparkleLoyalty, tokenAddress, treasuryAddress, collectionAddress, tiersAddress, timestampAddress); 35 | }).then(() => { 36 | SparkleRewardTiers.deployed({overwrite: false }) 37 | .then(function (rti) { 38 | // rti.setContractAddress(SparkleLoyalty.address, {from: accounts[0]}); 39 | SparkleTimestamp.deployed({ overwrite: false }) 40 | .then(function (tsi) { 41 | tsi.setContractAddress(SparkleLoyalty.address, {from: accounts[0]}); 42 | tsi.setTimePeriod(60*3, {from: accounts[0]}); 43 | }); 44 | }); 45 | }); 46 | }; -------------------------------------------------------------------------------- /test/helpers/truffle-time-helpers.js: -------------------------------------------------------------------------------- 1 | 2 | advanceTime = (time) => { 3 | return new Promise((resolve, reject) => { 4 | web3.currentProvider.send({ 5 | jsonrpc: '2.0', 6 | method: 'evm_increaseTime', 7 | params: [time], 8 | id: new Date().getTime() 9 | }, (err, result) => { 10 | if (err) { 11 | return reject(err); 12 | } 13 | return resolve(result); 14 | }); 15 | }); 16 | }; 17 | 18 | advanceBlock = () => { 19 | return new Promise((resolve, reject) => { 20 | web3.currentProvider.send({ 21 | jsonrpc: '2.0', 22 | method: 'evm_mine', 23 | id: new Date().getTime() 24 | }, (err, result) => { 25 | if (err) { 26 | return reject(err); 27 | } 28 | const newBlockHash = web3.eth.getBlock('latest').hash; 29 | 30 | return resolve(newBlockHash); 31 | }); 32 | }); 33 | }; 34 | 35 | takeSnapshot = () => { 36 | return new Promise((resolve, reject) => { 37 | web3.currentProvider.send({ 38 | jsonrpc: '2.0', 39 | method: 'evm_snapshot', 40 | id: new Date().getTime() 41 | }, (err, snapshotId) => { 42 | if (err) { 43 | return reject(err); 44 | } 45 | return resolve(snapshotId); 46 | }); 47 | }); 48 | }; 49 | 50 | revertToSnapShot = (id) => { 51 | return new Promise((resolve, reject) => { 52 | web3.currentProvider.send({ 53 | jsonrpc: '2.0', 54 | method: 'evm_revert', 55 | params: [id], 56 | id: new Date().getTime() 57 | }, (err, result) => { 58 | if (err) { 59 | return reject(err); 60 | } 61 | return resolve(result); 62 | }); 63 | }); 64 | }; 65 | 66 | advanceTimeAndBlock = async (time) => { 67 | await advanceTime(time); 68 | await advanceBlock(); 69 | return Promise.resolve(web3.eth.getBlock('latest')); 70 | }; 71 | 72 | module.exports = { 73 | advanceTime, 74 | advanceBlock, 75 | advanceTimeAndBlock, 76 | takeSnapshot, 77 | revertToSnapShot 78 | }; 79 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sparkle-pol", 3 | "version": "1.0.0", 4 | "description": "SparkleLoyalty, ProofOfLoyalty Contract Project", 5 | "directories": { 6 | "test": "test" 7 | }, 8 | "scripts": { 9 | "ganache:cli": "ganache-cli -d -m 'eternal logic alarm impose play scissors climb must ecology belt jungle salt' -e 1000000000 -a 5 --db ./.ganachedb/ -i 31621", 10 | "ganache:cli:verbose": "ganache-cli -d -m 'eternal logic alarm impose play scissors climb must ecology belt jungle salt' -e 1000000000 -a 5 --db ./.ganachedb/ -i 31621 -v", 11 | "truffle:con": "truffle console", 12 | "test:all": "npm run test:rewardtiers && npm run test:timestamp && npm run test:loyalty", 13 | "test:loyalty": "truffle test ./test/TestSparkleLoyalty.js && exit 1", 14 | "test:timestamp": "truffle test ./test/TestSparkleTimestamp.js && exit 1", 15 | "test:rewardtiers": "truffle test ./test/TestSparkleRewardTiers.js && exit 1", 16 | "test:alltierrewards": "npm run test:tier0rewards && npm run test:tier1rewards && npm run test:tier2rewards && npm run test:tier3rewards", 17 | "test:tier0rewards": "truffle test ./test/TestSparkleLoyalty-Tier0.js", 18 | "test:tier1rewards": "truffle test ./test/TestSparkleLoyalty-Tier1.js", 19 | "test:tier2rewards": "truffle test ./test/TestSparkleLoyalty-Tier2.js", 20 | "test:tier3rewards": "truffle test ./test/TestSparkleLoyalty-Tier3.js", 21 | "audit:mythx:v6:quick": "mythx --format json --yes analyze --wait --mode quick --create-group --group-name SparkleLoyaltyQuickV6 --solc-version 0.6.12 --include SparkleTimestamp --include SparkleRewardTiers --include SparkleLoyalty --swc-blacklist 102,107,116,123", 22 | "audit:mythx:v6:standard": "mythx --format json --yes analyze --wait --mode standard --create-group --group-name SparkleLoyaltyStandardV6 --solc-version 0.6.12 --include SparkleTimestamp --include SparkleRewardTiers --include SparkleLoyalty --swc-blacklist 102,107,116,123", 23 | "audit:mythx:v6:deep": "mythx --format json --yes analyze --wait --mode deep --create-group --group-name SparkleLoyaltyDeepV6 --solc-version 0.6.12 --include SparkleTimestamp --include SparkleRewardTiers --include SparkleLoyalty --swc-blacklist 102,107,116,123" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+ssh://git@bitbucket.org/sparkledevelopers/sparkle-pol.git" 28 | }, 29 | "keywords": [], 30 | "author": "SparkleLoyalty Inc. (c) 2018 - 2020", 31 | "license": "MIT", 32 | "homepage": "https://bitbucket.org/sparkledevelopers/sparkle-pol#readme", 33 | "dependencies": { 34 | "@openzeppelin/contracts": "^3.1.0", 35 | "web3-provider-engine": "^15.0.12" 36 | }, 37 | "devDependencies": { 38 | "@babel/core": "^7.10.4", 39 | "@truffle/hdwallet-provider": "^1.0.37", 40 | "chai": "^4.2.0", 41 | "dotenv": "^8.2.0", 42 | "truffle-assertions": "^0.9.2" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /truffle-config.js: -------------------------------------------------------------------------------- 1 | const HDWalletProvider = require('@truffle/hdwallet-provider'); 2 | const NonceTrackerSubprovider = require('web3-provider-engine/subproviders/nonce-tracker'); 3 | 4 | // Using dotenv 5 | require('dotenv').config(); 6 | 7 | /** 8 | * @title module.exports 9 | * @dev Truffle configuration sections 10 | * 11 | * Network: 12 | * - development: Deployment path on port 8545 (geth/ganache/rpc) 13 | * - ganachecli: Deployment path on port 7545 (ganache-cli) 14 | * - ropten: Deployment on the Ethereum ropsten test network (powered-by infura) 15 | * - mainnet: Deployment on the Ethereum main network (powered-by infura) 16 | * 17 | * rpc: 18 | * - host: server to connect to for rpc connection 19 | * - port: server port used to accept rpc connections 20 | * 21 | * Solc 22 | * - optimizer: should the optimizer be used (true/false) 23 | * - runs: number of times the optimizer shold be run 24 | */ 25 | module.exports = { 26 | networks: { 27 | development: { 28 | host: "localhost", // Localhost (default: none) 29 | port: 8545, // Standard Ethereum port (default: none) 30 | network_id: "*", // Any network (default: none) 31 | // websockets: true 32 | // gas: 100000000, // Updated gas amount (Closed to mainnet block gas) 33 | }, 34 | ganache: { 35 | host: "localhost", // Localhost (default: none) 36 | port: 7545, // Standard Ethereum port (default: none) 37 | network_id: "*", // Any network (default: none) 38 | // websockets: true 39 | }, 40 | ganachecli: { 41 | host: "localhost", // Localhost (default: none) 42 | port: 8545, // Standard Ethereum port (default: none) 43 | network_id: "*", // Any network (default: none) 44 | // websockets: true 45 | // gas: 100000000, // Updated gas amount (Closed to mainnet block gas) 46 | }, 47 | ropsten: { 48 | provider: () => new HDWalletProvider(process.env.INFURA_MNEMONIC, `https://ropsten.infura.io/v3/${process.env.INFURA_PROJECTID}`), // infura Ropsten provider 49 | network_id: "3", // Network id is 3 for ropsten 50 | // websockets: true 51 | }, 52 | ropsten2: { 53 | provider: () => { 54 | var wallet = new HDWalletProvider(process.env.INFURA_MNEMONIC, `https://ropsten.infura.io/v3/${process.env.INFURA_PROJECTID}`); // infura Ropsten provider 55 | var nonceTracker = new NonceTrackerSubprovider(); 56 | wallet.engine._providers.unshift(nonceTracker); 57 | nonceTracker.setEngine(wallet.engine); 58 | return wallet; 59 | }, 60 | network_id: "3", // Network id is 3 for ropsten 61 | // websockets: true 62 | }, 63 | // mainnet: { 64 | // provider: `https://mainnet.infura.io/v3/${process.env.INFURA_PROJECTID}`, // infura Mainnet provider 65 | // network_id: "1", // Network id is 3 for ropsten 66 | // }, 67 | }, 68 | rpc: { 69 | host: "127.0.0.1", 70 | port: 8080 71 | }, 72 | // Set default mocha options here, use special reporters etc. 73 | mocha: { 74 | usecolors: true, 75 | timeout: 0 76 | }, 77 | // Configure your compilers 78 | compilers: { 79 | solc: { 80 | version: "0.6.12", // Fetch exact version from solc-bin (default: truffle's version) 81 | docker: true, // Use "0.5.1" you've installed locally with docker (default: false) 82 | settings: { // See the solidity docs for advice about optimization and evmVersion 83 | optimizer: { 84 | enabled: true, 85 | runs: 200 86 | }, 87 | evmVersion: "petersburg" 88 | } 89 | } 90 | } 91 | }; 92 | -------------------------------------------------------------------------------- /developers/uml/@startuml.umldoc: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | ' -- classes -- 4 | 5 | 6 | interface IERC20 { 7 | ' -- inheritance -- 8 | 9 | ' -- usingFor -- 10 | 11 | ' -- vars -- 12 | 13 | ' -- methods -- 14 | +🔍totalSupply() 15 | +🔍balanceOf() 16 | +🔍allowance() 17 | +transfer() 18 | +approve() 19 | +transferFrom() 20 | 21 | } 22 | 23 | 24 | abstract SafeMath { 25 | ' -- inheritance -- 26 | 27 | ' -- usingFor -- 28 | 29 | ' -- vars -- 30 | 31 | ' -- methods -- 32 | #🔍mul() 33 | #🔍div() 34 | #🔍sub() 35 | #🔍add() 36 | #🔍mod() 37 | 38 | } 39 | 40 | 41 | class ERC20 { 42 | ' -- inheritance -- 43 | {abstract}IERC20 44 | 45 | ' -- usingFor -- 46 | {abstract}📚SafeMath for [[uint256]] 47 | 48 | ' -- vars -- 49 | -[[mapping address=>uint256 ]] _balances 50 | -[[mapping address=>mapping address=>uint256 ]] _allowed 51 | -[[uint256]] _totalSupply 52 | 53 | ' -- methods -- 54 | +🔍totalSupply() 55 | +🔍balanceOf() 56 | +🔍allowance() 57 | +transfer() 58 | +approve() 59 | +transferFrom() 60 | +increaseAllowance() 61 | +decreaseAllowance() 62 | #_transfer() 63 | #_mint() 64 | #_burn() 65 | #_burnFrom() 66 | 67 | } 68 | 69 | 70 | class ERC20Detailed { 71 | ' -- inheritance -- 72 | {abstract}IERC20 73 | 74 | ' -- usingFor -- 75 | 76 | ' -- vars -- 77 | -[[string]] _name 78 | -[[string]] _symbol 79 | -[[uint8]] _decimals 80 | 81 | ' -- methods -- 82 | +**__constructor__**() 83 | +🔍name() 84 | +🔍symbol() 85 | +🔍decimals() 86 | 87 | } 88 | 89 | 90 | class Ownable { 91 | ' -- inheritance -- 92 | 93 | ' -- usingFor -- 94 | 95 | ' -- vars -- 96 | -[[address]] _owner 97 | 98 | ' -- methods -- 99 | #**__constructor__**() 100 | +🔍owner() 101 | +🔍isOwner() 102 | +renounceOwnership() 103 | +transferOwnership() 104 | #_transferOwnership() 105 | 106 | } 107 | 108 | 109 | class Sparkle { 110 | ' -- inheritance -- 111 | {abstract}Ownable 112 | {abstract}ERC20 113 | {abstract}ERC20Detailed 114 | 115 | ' -- usingFor -- 116 | 117 | ' -- vars -- 118 | +[[string]] _tokenName 119 | +[[string]] _tokenSymbol 120 | +[[uint8]] _tokenDecimals 121 | +[[uint256]] _tokenMaxSupply 122 | 123 | ' -- methods -- 124 | +**__constructor__**() 125 | 126 | } 127 | 128 | 129 | class VerifyTime { 130 | ' -- inheritance -- 131 | {abstract}Ownable 132 | {abstract}ReentrancyGuard 133 | 134 | ' -- usingFor -- 135 | {abstract}📚SafeMath for [[uint256]] 136 | 137 | ' -- vars -- 138 | -[[address]] contractAddress 139 | +[[mapping address=>mapping address=>ProofOfTime ]] checkTimestamp 140 | 141 | ' -- methods -- 142 | +**__constructor__**() 143 | +setContractAddress() 144 | +setTimestamp() 145 | +checkTimestamp() 146 | +resetTimestamp() 147 | +removeTimestamp() 148 | 149 | } 150 | 151 | 152 | class loyaltySettings { 153 | ' -- inheritance -- 154 | {abstract}Ownable 155 | {abstract}ReentrancyGuard 156 | {abstract}ERC20 157 | 158 | ' -- usingFor -- 159 | {abstract}📚SafeMath for [[uint256]] 160 | 161 | ' -- vars -- 162 | +[[uint256]] currentMiners 163 | -[[address]] loyaltyfaucet 164 | +[[mapping address=>ProofOfLoyalty ]] loyaltyTimestamp 165 | +[[mapping address=>storageDump ]] timestampRemoved 166 | 167 | ' -- methods -- 168 | +**__constructor__**() 169 | +setfaucetAddress() 170 | +💰loyaltyBonus1() 171 | +💰loyaltyBonus2() 172 | +verifyBlockLoyalty() 173 | #dailyCounter() 174 | +claimReward() 175 | +withdrawLoyalty() 176 | +depositLoyalty() 177 | 178 | } 179 | ' -- inheritance / usingFor -- 180 | ERC20 --[#DarkGoldenRod]|> IERC20 181 | ERC20 ..[#DarkOliveGreen]|> SafeMath : //for uint256// 182 | ERC20Detailed --[#DarkGoldenRod]|> IERC20 183 | Sparkle --[#DarkGoldenRod]|> Ownable 184 | Sparkle --[#DarkGoldenRod]|> ERC20 185 | Sparkle --[#DarkGoldenRod]|> ERC20Detailed 186 | VerifyTime --[#DarkGoldenRod]|> Ownable 187 | VerifyTime --[#DarkGoldenRod]|> ReentrancyGuard 188 | VerifyTime ..[#DarkOliveGreen]|> SafeMath : //for uint256// 189 | loyaltySettings --[#DarkGoldenRod]|> Ownable 190 | loyaltySettings --[#DarkGoldenRod]|> ReentrancyGuard 191 | loyaltySettings --[#DarkGoldenRod]|> ERC20 192 | loyaltySettings ..[#DarkOliveGreen]|> SafeMath : //for uint256// 193 | 194 | @enduml -------------------------------------------------------------------------------- /contracts/ISparkleRewardTiers.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | 3 | /// SWC-103: Floating Pragma 4 | pragma solidity 0.6.12; 5 | 6 | // import '../node_modules/openzeppelin-solidity/contracts/math/SafeMath.sol'; 7 | // import '../node_modules/openzeppelin-solidity/contracts/ownership/Ownable.sol'; 8 | // import '../node_modules/openzeppelin-solidity/contracts/lifecycle/Pausable.sol'; 9 | // import '../node_modules/openzeppelin-solidity/contracts/utils/ReentrancyGuard.sol'; 10 | 11 | /** 12 | * @title A contract for managing reward tiers 13 | * @author SparkleLoyalty Inc. (c) 2019-2020 14 | */ 15 | // interface ISparkleRewardTiers is Ownable, Pausable, ReentrancyGuard { 16 | interface ISparkleRewardTiers { 17 | 18 | /** 19 | * @dev Add a new reward tier to the contract for future proofing 20 | * @param _index of the new reward tier to add 21 | * @param _rate of the added reward tier 22 | * @param _price of the added reward tier 23 | * @param _enabled status of the added reward tier 24 | * @notice Test(s) Need rewrite 25 | */ 26 | function addTier(uint256 _index, uint256 _rate, uint256 _price, bool _enabled) 27 | external 28 | // view 29 | // onlyOwner 30 | // whenNotPaused 31 | // nonReentrant 32 | returns(bool); 33 | 34 | /** 35 | * @dev Update an existing reward tier with new values 36 | * @param _index of reward tier to update 37 | * @param _rate of the reward tier 38 | * @param _price of the reward tier 39 | * @param _enabled status of the reward tier 40 | * @return (bool) indicating success/failure 41 | * @notice Test(s) Need rewrite 42 | */ 43 | function updateTier(uint256 _index, uint256 _rate, uint256 _price, bool _enabled) 44 | external 45 | // view 46 | // onlyOwner 47 | // whenNotPaused 48 | // nonReentrant 49 | returns(bool); 50 | 51 | /** 52 | * @dev Remove an existing reward tier from list of tiers 53 | * @param _index of reward tier to remove 54 | * @notice Test(s) Need rewrite 55 | */ 56 | function deleteTier(uint256 _index) 57 | external 58 | // view 59 | // onlyOwner 60 | // whenNotPaused 61 | // nonReentrant 62 | returns(bool); 63 | 64 | /** 65 | * @dev Get the rate value of specified tier 66 | * @param _index of tier to query 67 | * @return specified reward tier rate 68 | * @notice Test(s) Need rewrite 69 | */ 70 | function getRate(uint256 _index) 71 | external 72 | // view 73 | // whenNotPaused 74 | returns(uint256); 75 | 76 | /** 77 | * @dev Get price of tier 78 | * @param _index of tier to query 79 | * @return uint256 indicating tier price 80 | * @notice Test(s) Need rewrite 81 | */ 82 | function getPrice(uint256 _index) 83 | external 84 | // view 85 | // whenNotPaused 86 | returns(uint256); 87 | 88 | /** 89 | * @dev Get the enabled status of tier 90 | * @param _index of tier to query 91 | * @return bool indicating status of tier 92 | * @notice Test(s) Need rewrite 93 | */ 94 | function getEnabled(uint256 _index) 95 | external 96 | // view 97 | // whenNotPaused 98 | returns(bool); 99 | 100 | /** 101 | * @dev Withdraw ether that has been sent directly to the contract 102 | * @return bool indicating withdraw success 103 | * @notice Test(s) Need rewrite 104 | */ 105 | function withdrawEth() 106 | external 107 | // onlyOwner 108 | // whenNotPaused 109 | // nonReentrant 110 | returns(bool); 111 | 112 | /** 113 | * @dev Event triggered when a reward tier is deleted 114 | * @param _index of tier to deleted 115 | */ 116 | event TierDeleted(uint256 _index); 117 | 118 | /** 119 | * @dev Event triggered when a reward tier is updated 120 | * @param _index of the updated tier 121 | * @param _rate of updated tier 122 | * @param _price of updated tier 123 | * @param _enabled status of updated tier 124 | */ 125 | event TierUpdated(uint256 _index, uint256 _rate, uint256 _price, bool _enabled); 126 | 127 | /** 128 | * @dev Event triggered when a new reward tier is added 129 | * @param _index of the tier added 130 | * @param _rate of added tier 131 | * @param _price of added tier 132 | * @param _enabled status of added tier 133 | */ 134 | event TierAdded(uint256 _index, uint256 _rate, uint256 _price, bool _enabled); 135 | 136 | } -------------------------------------------------------------------------------- /contracts/ISparkleTimestamp.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | 3 | /// SWC-103: Floating Pragma 4 | pragma solidity 0.6.12; 5 | 6 | // import "../node_modules/openzeppelin-solidity/contracts/math/SafeMath.sol"; 7 | // import "../node_modules/openzeppelin-solidity/contracts/ownership/Ownable.sol"; 8 | // import "../node_modules/openzeppelin-solidity/contracts/lifecycle/Pausable.sol"; 9 | // import "../node_modules/openzeppelin-solidity/contracts/utils/ReentrancyGuard.sol"; 10 | 11 | /** 12 | * @dev Sparkle Timestamp Contract 13 | * @author SparkleMobile Inc. (c) 2019-2020 14 | */ 15 | interface ISparkleTimestamp { 16 | 17 | /** 18 | * @dev Add new reward timestamp for address 19 | * @param _rewardAddress being added to timestamp collection 20 | */ 21 | function addTimestamp(address _rewardAddress) 22 | external 23 | returns(bool); 24 | 25 | /** 26 | * @dev Reset timestamp maturity for loyalty address 27 | * @param _rewardAddress to have reward period reset 28 | */ 29 | function resetTimestamp(address _rewardAddress) 30 | external 31 | returns(bool); 32 | 33 | /** 34 | * @dev Zero/delete existing loyalty timestamp entry 35 | * @param _rewardAddress being requested for timestamp deletion 36 | * @notice Test(s) not passed 37 | */ 38 | function deleteTimestamp(address _rewardAddress) 39 | external 40 | returns(bool); 41 | 42 | /** 43 | * @dev Get address confirmation for loyalty address 44 | * @param _rewardAddress being queried for address information 45 | */ 46 | function getAddress(address _rewardAddress) 47 | external 48 | returns(address); 49 | 50 | /** 51 | * @dev Get timestamp of initial joined timestamp for loyalty address 52 | * @param _rewardAddress being queried for timestamp information 53 | */ 54 | function getJoinedTimestamp(address _rewardAddress) 55 | external 56 | returns(uint256); 57 | 58 | /** 59 | * @dev Get timestamp of last deposit for loyalty address 60 | * @param _rewardAddress being queried for timestamp information 61 | */ 62 | function getDepositTimestamp(address _rewardAddress) 63 | external 64 | returns(uint256); 65 | 66 | /** 67 | * @dev Get timestamp of reward maturity for loyalty address 68 | * @param _rewardAddress being queried for timestamp information 69 | */ 70 | function getRewardTimestamp(address _rewardAddress) 71 | external 72 | returns(uint256); 73 | 74 | /** 75 | * @dev Determine if address specified has a timestamp record 76 | * @param _rewardAddress being queried for timestamp existance 77 | */ 78 | function hasTimestamp(address _rewardAddress) 79 | external 80 | returns(bool); 81 | 82 | /** 83 | * @dev Calculate time remaining in seconds until this address' reward matures 84 | * @param _rewardAddress to query remaining time before reward matures 85 | */ 86 | function getTimeRemaining(address _rewardAddress) 87 | external 88 | returns(uint256, bool, uint256); 89 | 90 | /** 91 | * @dev Determine if reward is mature for address 92 | * @param _rewardAddress Address requesting addition in to loyalty timestamp collection 93 | */ 94 | function isRewardReady(address _rewardAddress) 95 | external 96 | returns(bool); 97 | 98 | /** 99 | * @dev Change the stored loyalty controller contract address 100 | * @param _newAddress of new loyalty controller contract address 101 | */ 102 | function setContractAddress(address _newAddress) 103 | external; 104 | 105 | /** 106 | * @dev Return the stored authorized controller address 107 | * @return Address of loyalty controller contract 108 | */ 109 | function getContractAddress() 110 | external 111 | returns(address); 112 | 113 | /** 114 | * @dev Change the stored loyalty time period 115 | * @param _newTimePeriod of new reward period (in seconds) 116 | */ 117 | function setTimePeriod(uint256 _newTimePeriod) 118 | external; 119 | 120 | /** 121 | * @dev Return the current loyalty timer period 122 | * @return Current stored value of loyalty time period 123 | */ 124 | function getTimePeriod() 125 | external 126 | returns(uint256); 127 | 128 | /** 129 | * @dev Event signal: Reset timestamp 130 | */ 131 | event ResetTimestamp(address _rewardAddress); 132 | 133 | /** 134 | * @dev Event signal: Loyalty contract address waws changed 135 | */ 136 | event ContractAddressChanged(address indexed _previousAddress, address indexed _newAddress); 137 | 138 | /** 139 | * @dev Event signal: Loyalty reward time period was changed 140 | */ 141 | event TimePeriodChanged( uint256 indexed _previousTimePeriod, uint256 indexed _newTimePeriod); 142 | 143 | /** 144 | * @dev Event signal: Loyalty reward timestamp was added 145 | */ 146 | event TimestampAdded( address indexed _newTimestampAddress ); 147 | 148 | /** 149 | * @dev Event signal: Loyalty reward timestamp was removed 150 | */ 151 | event TimestampDeleted( address indexed _newTimestampAddress ); 152 | 153 | /** 154 | * @dev Event signal: Timestamp for address was reset 155 | */ 156 | event TimestampReset(address _rewardAddress); 157 | 158 | } -------------------------------------------------------------------------------- /contracts/SparkleRewardTiers.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | 3 | /// SWC-103: Floating Pragma 4 | pragma solidity 0.6.12; 5 | 6 | import '@openzeppelin/contracts/math/SafeMath.sol'; 7 | import '@openzeppelin/contracts/access/Ownable.sol'; 8 | import '@openzeppelin/contracts/utils/Pausable.sol'; 9 | import '@openzeppelin/contracts/utils/ReentrancyGuard.sol'; 10 | import './ISparkleRewardTiers.sol'; 11 | 12 | /** 13 | * @title A contract for managing reward tiers 14 | * @author SparkleLoyalty Inc. (c) 2019-2020 15 | */ 16 | contract SparkleRewardTiers is ISparkleRewardTiers, Ownable, Pausable, ReentrancyGuard { 17 | 18 | /** 19 | * @dev Ensure math safety through SafeMath 20 | */ 21 | using SafeMath for uint256; 22 | 23 | /** 24 | * @dev Data structure declaring a loyalty tier 25 | * @param _rate apr for reward tier 26 | * @param _price to select reward tier 27 | * @param _enabled availability for reward tier 28 | */ 29 | struct Tier { 30 | uint256 _rate; 31 | uint256 _price; 32 | bool _enabled; 33 | } 34 | 35 | // tiers mapping of available reward tiers 36 | mapping(uint256 => Tier) private g_tiers; 37 | 38 | /** 39 | * @dev Sparkle loyalty tier rewards contract 40 | * @notice Timestamp support for SparklePOL contract 41 | */ 42 | constructor() 43 | public 44 | Ownable() 45 | Pausable() 46 | ReentrancyGuard() 47 | { 48 | Tier memory tier0; 49 | tier0._rate = uint256(1.00000000 * 10e7); 50 | tier0._price = 0 ether; 51 | tier0._enabled = true; 52 | /// Initialize default reward tier 53 | g_tiers[0] = tier0; 54 | 55 | Tier memory tier1; 56 | tier1._rate = uint256(1.10000000 * 10e7); 57 | tier1._price = 0.10 ether; 58 | tier1._enabled = true; 59 | /// Initialize reward tier 1 60 | g_tiers[1] = tier1; 61 | 62 | Tier memory tier2; 63 | tier2._rate = uint256(1.20000000 * 10e7); 64 | tier2._price = 0.20 ether; 65 | tier2._enabled = true; 66 | /// Initialize reward tier 2 67 | g_tiers[2] = tier2; 68 | 69 | Tier memory tier3; 70 | tier3._rate = uint256(1.30000000 * 10e7); 71 | tier3._price = 0.30 ether; 72 | tier3._enabled = true; 73 | /// Initialize reward tier 3 74 | g_tiers[3] = tier3; 75 | } 76 | 77 | /** 78 | * @dev Add a new reward tier to the contract for future proofing 79 | * @param _index of the new reward tier to add 80 | * @param _rate of the added reward tier 81 | * @param _price of the added reward tier 82 | * @param _enabled status of the added reward tier 83 | * @notice Test(s) Need rewrite 84 | */ 85 | function addTier(uint256 _index, uint256 _rate, uint256 _price, bool _enabled) 86 | public 87 | onlyOwner 88 | whenNotPaused 89 | nonReentrant 90 | override 91 | returns(bool) 92 | { 93 | /// Validate calling address (msg.sender) 94 | require(msg.sender != address(0x0), 'Invalid {From}'); 95 | /// Validate that tier does not already exist 96 | require(g_tiers[_index]._enabled == false, 'Tier exists'); 97 | Tier memory newTier; 98 | /// Initialize structure to specified data 99 | newTier._rate = _rate; 100 | newTier._price = _price; 101 | newTier._enabled = _enabled; 102 | /// Insert tier into collection 103 | g_tiers[_index] = newTier; 104 | /// Emit event log to the block chain for future web3 use 105 | emit TierAdded(_index, _rate, _price, _enabled); 106 | /// Return success 107 | return true; 108 | } 109 | 110 | /** 111 | * @dev Update an existing reward tier with new values 112 | * @param _index of reward tier to update 113 | * @param _rate of the reward tier 114 | * @param _price of the reward tier 115 | * @param _enabled status of the reward tier 116 | * @return (bool) indicating success/failure 117 | * @notice Test(s) Need rewrite 118 | */ 119 | function updateTier(uint256 _index, uint256 _rate, uint256 _price, bool _enabled) 120 | public 121 | onlyOwner 122 | whenNotPaused 123 | nonReentrant 124 | override 125 | returns(bool) 126 | { 127 | /// Validate calling address (msg.sender) 128 | require(msg.sender != address(0x0), 'Invalid {From}'); 129 | require(g_tiers[_index]._rate > 0, 'Invalid tier'); 130 | /// Validate that reward and ether values 131 | require(_rate > 0, 'Invalid rate'); 132 | require(_price > 0, 'Invalid Price'); 133 | /// Update the specified tier with specified data 134 | g_tiers[_index]._rate = _rate; 135 | g_tiers[_index]._price = _price; 136 | g_tiers[_index]._enabled = _enabled; 137 | /// Emit event log to the block chain for future web3 use 138 | emit TierUpdated(_index, _rate, _price, _enabled); 139 | /// Return success 140 | return true; 141 | } 142 | 143 | /** 144 | * @dev Remove an existing reward tier from list of tiers 145 | * @param _index of reward tier to remove 146 | * @notice Test(s) Need rewrite 147 | */ 148 | function deleteTier(uint256 _index) 149 | public 150 | onlyOwner 151 | whenNotPaused 152 | nonReentrant 153 | override 154 | returns(bool) 155 | { 156 | /// Validate calling address (msg.sender) 157 | require(msg.sender != address(0x0), 'Invalid {From}'); 158 | /// Validate tier delete does not delete system tiers 0-2 159 | require(_index >= 4, 'Invalid request'); 160 | /// Zero out the spcified tier's data 161 | delete g_tiers[_index]; 162 | /// Emit event log to the block chain for future web3 use 163 | emit TierDeleted(_index); 164 | /// Return success 165 | return true; 166 | } 167 | 168 | /** 169 | * @dev Get the rate value of specified tier 170 | * @param _index of tier to query 171 | * @return specified reward tier rate 172 | * @notice Test(s) Need rewrite 173 | */ 174 | function getRate(uint256 _index) 175 | public 176 | whenNotPaused 177 | override 178 | returns(uint256) 179 | { 180 | /// Return reward rate for specified tier 181 | return g_tiers[_index]._rate; 182 | } 183 | 184 | /** 185 | * @dev Get price of tier 186 | * @param _index of tier to query 187 | * @return uint256 indicating tier price 188 | * @notice Test(s) Need rewrite 189 | */ 190 | function getPrice(uint256 _index) 191 | public 192 | whenNotPaused 193 | override 194 | returns(uint256) 195 | { 196 | /// Return reward purchase price in ether for tier 197 | return g_tiers[_index]._price; 198 | } 199 | 200 | /** 201 | * @dev Get the enabled status of tier 202 | * @param _index of tier to query 203 | * @return bool indicating status of tier 204 | * @notice Test(s) Need rewrite 205 | */ 206 | function getEnabled(uint256 _index) 207 | public 208 | whenNotPaused 209 | override 210 | returns(bool) 211 | { 212 | /// Return reward tier enabled status for specified tier 213 | return g_tiers[_index]._enabled; 214 | } 215 | 216 | /** 217 | * @dev Withdraw ether that has been sent directly to the contract 218 | * @return bool indicating withdraw success 219 | * @notice Test(s) Need rewrite 220 | */ 221 | function withdrawEth() 222 | public 223 | onlyOwner 224 | whenNotPaused 225 | nonReentrant 226 | override 227 | returns(bool) 228 | { 229 | /// Validate calling address (msg.sender) 230 | require(msg.sender != address(0x0), 'Invalid {From}'); 231 | /// Validate that this contract is storing ether 232 | require(address(this).balance >= 0, 'No ether'); 233 | /// Transfer the ether to owner address 234 | msg.sender.transfer(address(this).balance); 235 | return true; 236 | } 237 | 238 | /** 239 | * @dev Event triggered when a reward tier is deleted 240 | * @param _index of tier to deleted 241 | */ 242 | event TierDeleted(uint256 _index); 243 | 244 | /** 245 | * @dev Event triggered when a reward tier is updated 246 | * @param _index of the updated tier 247 | * @param _rate of updated tier 248 | * @param _price of updated tier 249 | * @param _enabled status of updated tier 250 | */ 251 | event TierUpdated(uint256 _index, uint256 _rate, uint256 _price, bool _enabled); 252 | 253 | /** 254 | * @dev Event triggered when a new reward tier is added 255 | * @param _index of the tier added 256 | * @param _rate of added tier 257 | * @param _price of added tier 258 | * @param _enabled status of added tier 259 | */ 260 | event TierAdded(uint256 _index, uint256 _rate, uint256 _price, bool _enabled); 261 | 262 | } -------------------------------------------------------------------------------- /contracts/SparkleTimestamp.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | 3 | /// SWC-103: Floating Pragma 4 | pragma solidity 0.6.12; 5 | 6 | import "@openzeppelin/contracts/math/SafeMath.sol"; 7 | import "@openzeppelin/contracts/access/Ownable.sol"; 8 | import "@openzeppelin/contracts/utils/Pausable.sol"; 9 | import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; 10 | import './ISparkleTimestamp.sol'; 11 | 12 | /** 13 | * @dev Sparkle Timestamp Contract 14 | * @author SparkleMobile Inc. (c) 2019-2020 15 | */ 16 | contract SparkleTimestamp is ISparkleTimestamp, Ownable, Pausable, ReentrancyGuard { 17 | /** 18 | * @dev Ensure math safety through SafeMath 19 | */ 20 | using SafeMath for uint256; 21 | 22 | /** 23 | * @dev Timestamp object for tacking block.timestamp ooc(out-of-contract) 24 | * @param _address Address of the owner address of this record 25 | * @param _joined block.timestamp of initial joining time 26 | * @param _deposit block.timestamp of reward address' deposit (uint256) 27 | * @param _reward block.timestamp + loyaltyTimePeriod precalculation (uint256) 28 | */ 29 | struct Timestamp { 30 | address _address; 31 | uint256 _joined; 32 | uint256 _deposit; 33 | uint256 _reward; 34 | } 35 | 36 | /** 37 | * @dev Internal address for authorized loyalty contract 38 | */ 39 | address private contractAddress; 40 | 41 | /** 42 | * @dev Internal time period of reward maturity for all address' 43 | */ 44 | uint256 private timePeriod; 45 | 46 | /** 47 | * @dev Internal loyalty timestamp mapping to authorized calling loyalty contracts 48 | */ 49 | mapping(address => mapping(address => Timestamp)) private g_timestamps; 50 | 51 | /** 52 | * @dev SparkleTimestamp contract .cTor 53 | */ 54 | constructor() 55 | public 56 | Ownable() 57 | Pausable() 58 | ReentrancyGuard() 59 | { 60 | /// Initialize contract address to 0x0 61 | contractAddress = address(0x0); 62 | /// Initilize time period to 24 hours (86400 seconds) 63 | timePeriod = 60 * 60 * 24; 64 | } 65 | 66 | /** 67 | * @dev Add new reward timestamp for address 68 | * @param _rewardAddress being added to timestamp collection 69 | */ 70 | function addTimestamp(address _rewardAddress) 71 | external 72 | whenNotPaused 73 | nonReentrant 74 | override 75 | returns(bool) 76 | { 77 | /// Validate calling address (msg.sender) 78 | require(msg.sender != address(0x0), 'Invalid {From}a'); 79 | /// Validate caller is valid controller contract 80 | require(msg.sender == address(contractAddress), 'Unauthorized {From}'); 81 | /// Validate specified address (_rewardAddress) 82 | require(_rewardAddress != address(0x0), 'Invalid reward address'); 83 | /// Validate specified address does not have a timestamp 84 | require(g_timestamps[msg.sender][_rewardAddress]._address == address(0x0), 'Timestamp exists'); 85 | /// Initialize timestamp structure with loyalty users data 86 | g_timestamps[msg.sender][_rewardAddress]._address = address(_rewardAddress); 87 | g_timestamps[msg.sender][_rewardAddress]._deposit = block.timestamp; 88 | g_timestamps[msg.sender][_rewardAddress]._joined = block.timestamp; 89 | /// Calculate the time in the future reward will mature 90 | g_timestamps[msg.sender][_rewardAddress]._reward = timePeriod.add(block.timestamp); 91 | /// Emit event log to the block chain for future web3 use 92 | emit TimestampAdded(_rewardAddress); 93 | /// Return success 94 | return true; 95 | } 96 | 97 | /** 98 | * @dev Reset timestamp maturity for loyalty address 99 | * @param _rewardAddress to have reward period reset 100 | */ 101 | function resetTimestamp(address _rewardAddress) 102 | external 103 | whenNotPaused 104 | nonReentrant 105 | override 106 | returns(bool) 107 | { 108 | /// Validate calling address (msg.sender) 109 | require(msg.sender != address(0x0), 'Invalid {from}b'); 110 | require(msg.sender == address(contractAddress), 'Unauthorized {From}'); 111 | /// Validate specified address (_rewardAddress) 112 | require(_rewardAddress != address(0x0), 'Invalid reward address'); 113 | /// Validate specified address has a timestamp 114 | require(g_timestamps[msg.sender][_rewardAddress]._address == address(_rewardAddress), 'Invalid timestamp'); 115 | /// Re-initialize timestamp structure with updated time data 116 | g_timestamps[msg.sender][_rewardAddress]._deposit = block.timestamp; 117 | g_timestamps[msg.sender][_rewardAddress]._reward = uint256(block.timestamp).add(timePeriod); 118 | /// Return success 119 | return true; 120 | } 121 | 122 | /** 123 | * @dev Zero/delete existing loyalty timestamp entry 124 | * @param _rewardAddress being requested for timestamp deletion 125 | * @notice Test(s) not passed 126 | */ 127 | function deleteTimestamp(address _rewardAddress) 128 | external 129 | whenNotPaused 130 | nonReentrant 131 | override 132 | returns(bool) 133 | { 134 | /// Validate calling address (msg.sender) 135 | require(msg.sender != address(0), 'Invalid {from}c'); 136 | /// Validate caller is valid controller contract 137 | require(msg.sender == address(contractAddress), 'Unauthorized {From}'); 138 | /// Validate specified address (_rewardAddress) 139 | require(_rewardAddress != address(0), "Invalid reward address "); 140 | /// Validate specified address has a timestamp 141 | if(g_timestamps[msg.sender][_rewardAddress]._address != address(_rewardAddress)) { 142 | emit TimestampDeleted( false ); 143 | return false; 144 | } 145 | 146 | // Zero out address as delete does nothing with structure elements 147 | Timestamp storage ts = g_timestamps[msg.sender][_rewardAddress]; 148 | ts._address = address(0x0); 149 | ts._deposit = 0; 150 | ts._reward = 0; 151 | /// Return success 152 | emit TimestampDeleted( true ); 153 | return true; 154 | } 155 | 156 | /** 157 | * @dev Get address confirmation for loyalty address 158 | * @param _rewardAddress being queried for address information 159 | */ 160 | function getAddress(address _rewardAddress) 161 | external 162 | whenNotPaused 163 | override 164 | returns(address) 165 | { 166 | /// Validate calling address (msg.sender) 167 | require(msg.sender != address(0), 'Invalid {from}d'); 168 | /// Validate caller is valid controller contract 169 | require(msg.sender == address(contractAddress), 'Unauthorized {From}'); 170 | /// Validate specified address (_rewardAddress) 171 | require(_rewardAddress != address(0), 'Invalid reward address'); 172 | /// Validate specified address has a timestamp 173 | require(g_timestamps[msg.sender][_rewardAddress]._address == address(_rewardAddress), 'No timestamp b'); 174 | /// Return address indicating success 175 | return address(g_timestamps[msg.sender][_rewardAddress]._address); 176 | } 177 | 178 | /** 179 | * @dev Get timestamp of initial joined timestamp for loyalty address 180 | * @param _rewardAddress being queried for timestamp information 181 | */ 182 | function getJoinedTimestamp(address _rewardAddress) 183 | external 184 | whenNotPaused 185 | override 186 | returns(uint256) 187 | { 188 | /// Validate calling address (msg.sender) 189 | require(msg.sender != address(0), 'Invalid {from}e'); 190 | /// Validate caller is valid controller contract 191 | require(msg.sender == address(contractAddress), 'Unauthorized {From}'); 192 | /// Validate specified address (_rewardAddress) 193 | require(_rewardAddress != address(0), 'Invalid reward address'); 194 | /// Validate specified address has a timestamp 195 | require(g_timestamps[msg.sender][_rewardAddress]._address == address(_rewardAddress), 'No timestamp c'); 196 | /// Return deposit timestamp indicating success 197 | return g_timestamps[msg.sender][_rewardAddress]._joined; 198 | } 199 | 200 | /** 201 | * @dev Get timestamp of last deposit for loyalty address 202 | * @param _rewardAddress being queried for timestamp information 203 | */ 204 | function getDepositTimestamp(address _rewardAddress) 205 | external 206 | whenNotPaused 207 | override 208 | returns(uint256) 209 | { 210 | /// Validate calling address (msg.sender) 211 | require(msg.sender != address(0), 'Invalid {from}e'); 212 | /// Validate caller is valid controller contract 213 | require(msg.sender == address(contractAddress), 'Unauthorized {From}'); 214 | /// Validate specified address (_rewardAddress) 215 | require(_rewardAddress != address(0), 'Invalid reward address'); 216 | /// Validate specified address has a timestamp 217 | require(g_timestamps[msg.sender][_rewardAddress]._address == address(_rewardAddress), 'No timestamp d'); 218 | /// Return deposit timestamp indicating success 219 | return g_timestamps[msg.sender][_rewardAddress]._deposit; 220 | } 221 | 222 | /** 223 | * @dev Get timestamp of reward maturity for loyalty address 224 | * @param _rewardAddress being queried for timestamp information 225 | */ 226 | function getRewardTimestamp(address _rewardAddress) 227 | external 228 | whenNotPaused 229 | override 230 | returns(uint256) 231 | { 232 | /// Validate calling address (msg.sender) 233 | require(msg.sender != address(0), 'Invalid {from}f'); 234 | /// Validate caller is valid controller contract 235 | require(msg.sender == address(contractAddress), 'Unauthorized {From}'); 236 | /// Validate specified address (_rewardAddress) 237 | require(_rewardAddress != address(0), 'Invalid reward address'); 238 | /// Return reward timestamp indicating success 239 | return g_timestamps[msg.sender][_rewardAddress]._reward; 240 | } 241 | 242 | 243 | /** 244 | * @dev Determine if address specified has a timestamp record 245 | * @param _rewardAddress being queried for timestamp existance 246 | */ 247 | function hasTimestamp(address _rewardAddress) 248 | external 249 | whenNotPaused 250 | override 251 | returns(bool) 252 | { 253 | /// Validate calling address (msg.sender) 254 | require(msg.sender != address(0), 'Invalid {from}g'); 255 | /// Validate caller is valid controller contract 256 | require(msg.sender == address(contractAddress), 'Unauthorized {From}'); 257 | /// Validate specified address (_rewardAddress) 258 | require(_rewardAddress != address(0), 'Invalid reward address'); 259 | /// Determine if timestamp record matches reward address 260 | // if(g_timestamps[msg.sender][_rewardAddress]._address == address(_rewardAddress)) { 261 | // /// yes, then return success 262 | // return true; 263 | // } 264 | if(g_timestamps[msg.sender][_rewardAddress]._address != address(_rewardAddress)) 265 | { 266 | emit TimestampHasTimestamp(false); 267 | return false; 268 | } 269 | 270 | /// Return success 271 | emit TimestampHasTimestamp(true); 272 | return true; 273 | } 274 | 275 | /** 276 | * @dev Calculate time remaining in seconds until this address' reward matures 277 | * @param _rewardAddress to query remaining time before reward matures 278 | */ 279 | function getTimeRemaining(address _rewardAddress) 280 | external 281 | whenNotPaused 282 | override 283 | returns(uint256, bool, uint256) 284 | { 285 | /// Validate calling address (msg.sender) 286 | require(msg.sender != address(0), 'Invalid {from}h'); 287 | /// Validate caller is valid controller contract 288 | require(msg.sender == address(contractAddress), 'Unauthorized {From}'); 289 | /// Validate specified address (_rewardAddress) 290 | require(_rewardAddress != address(0), 'Invalid reward address'); 291 | /// Validate specified address has a timestamp 292 | require(g_timestamps[msg.sender][_rewardAddress]._address == address(_rewardAddress), 'No timestamp f'); 293 | /// Deterimine if reward address timestamp record has matured 294 | if(g_timestamps[msg.sender][_rewardAddress]._reward > block.timestamp) { 295 | /// No, then return indicating remaining time and false to indicate failure 296 | // return (g_timestamps[msg.sender][_rewardAddress]._reward - block.timestamp, false, g_timestamps[msg.sender][_rewardAddress]._deposit); 297 | return (g_timestamps[msg.sender][_rewardAddress]._reward - block.timestamp, false, g_timestamps[msg.sender][_rewardAddress]._joined); 298 | } 299 | 300 | /// Return indicating time since reward maturing and true to indicate success 301 | // return (block.timestamp - g_timestamps[msg.sender][_rewardAddress]._reward, true, g_timestamps[msg.sender][_rewardAddress]._deposit); 302 | return (block.timestamp - g_timestamps[msg.sender][_rewardAddress]._reward, true, g_timestamps[msg.sender][_rewardAddress]._joined); 303 | } 304 | 305 | /** 306 | * @dev Determine if reward is mature for address 307 | * @param _rewardAddress Address requesting addition in to loyalty timestamp collection 308 | */ 309 | function isRewardReady(address _rewardAddress) 310 | external 311 | whenNotPaused 312 | override 313 | returns(bool) 314 | { 315 | /// Validate calling address (msg.sender) 316 | require(msg.sender != address(0), 'Invalid {from}i'); 317 | /// Validate caller is valid controller contract 318 | require(msg.sender == address(contractAddress), 'Unauthorized {From}'); 319 | /// Validate specified address (_rewardAddress) 320 | require(_rewardAddress != address(0), 'Invalid reward address'); 321 | /// Validate specified address has a timestamp 322 | require(g_timestamps[msg.sender][_rewardAddress]._address == address(_rewardAddress), 'No timestamp g'); 323 | /// Deterimine if reward address timestamp record has matured 324 | if(g_timestamps[msg.sender][_rewardAddress]._reward > block.timestamp) { 325 | /// No, then return false to indicate failure 326 | return false; 327 | } 328 | 329 | /// Return success 330 | return true; 331 | } 332 | 333 | /** 334 | * @dev Change the stored loyalty controller contract address 335 | * @param _newAddress of new loyalty controller contract address 336 | */ 337 | function setContractAddress(address _newAddress) 338 | external 339 | onlyOwner 340 | nonReentrant 341 | override 342 | { 343 | /// Validate calling address (msg.sender) 344 | require(msg.sender != address(0), 'Invalid {from}j'); 345 | /// Validate specified address (_newAddress) 346 | require(_newAddress != address(0), 'Invalid contract address'); 347 | address currentAddress = contractAddress; 348 | /// Set current address to new controller contract address 349 | contractAddress = _newAddress; 350 | /// Emit event log to the block chain for future web3 use 351 | emit ContractAddressChanged(currentAddress, _newAddress); 352 | } 353 | 354 | /** 355 | * @dev Return the stored authorized controller address 356 | * @return Address of loyalty controller contract 357 | */ 358 | function getContractAddress() 359 | external 360 | whenNotPaused 361 | override 362 | returns(address) 363 | { 364 | /// Return current controller contract address 365 | return address(contractAddress); 366 | } 367 | 368 | /** 369 | * @dev Change the stored loyalty time period 370 | * @param _newTimePeriod of new reward period (in seconds) 371 | */ 372 | function setTimePeriod(uint256 _newTimePeriod) 373 | external 374 | onlyOwner 375 | nonReentrant 376 | override 377 | { 378 | /// Validate calling address (msg.sender) 379 | require(msg.sender != address(0), 'Invalid {from}k'); 380 | /// Validate specified time period 381 | require(_newTimePeriod >= 60 seconds, 'Time period < 60s'); 382 | uint256 currentTimePeriod = timePeriod; 383 | timePeriod = _newTimePeriod; 384 | /// Emit event log to the block chain for future web3 use 385 | emit TimePeriodChanged(currentTimePeriod, _newTimePeriod); 386 | } 387 | 388 | /** 389 | * @dev Return the current loyalty timer period 390 | * @return Current stored value of loyalty time period 391 | */ 392 | function getTimePeriod() 393 | external 394 | whenNotPaused 395 | override 396 | returns(uint256) 397 | { 398 | /// Return current time period 399 | return timePeriod; 400 | } 401 | 402 | /** 403 | * @dev Event signal: Reset timestamp 404 | */ 405 | event ResetTimestamp(address _rewardAddress); 406 | 407 | /** 408 | * @dev Event signal: Loyalty contract address waws changed 409 | */ 410 | event ContractAddressChanged(address indexed _previousAddress, address indexed _newAddress); 411 | 412 | /** 413 | * @dev Event signal: Loyalty reward time period was changed 414 | */ 415 | event TimePeriodChanged( uint256 indexed _previousTimePeriod, uint256 indexed _newTimePeriod); 416 | 417 | /** 418 | * @dev Event signal: Loyalty reward timestamp was added 419 | */ 420 | event TimestampAdded( address indexed _newTimestampAddress ); 421 | 422 | /** 423 | * @dev Event signal: Loyalty reward timestamp was removed 424 | */ 425 | event TimestampDeleted( bool indexed _timestampDeleted ); 426 | 427 | /** 428 | * @dev Event signal: Timestamp for address was reset 429 | */ 430 | event TimestampReset(address _rewardAddress); 431 | 432 | /** 433 | * @dev Event signal: Current hasTimestamp value 434 | */ 435 | event TimestampHasTimestamp(bool _hasTimestamp); 436 | 437 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sparkle Proof Of Loyalty 2 | 3 | >The SparkleLoyalty program is built on Ethereum and is comprised of a set specialized contracts that provide the functionality required by the program and should be considered compatible with any [ERC-20 Token Standard](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md). 4 | 5 | * Development/Testing environment: Truffle 6 | * MainNet Contract([Link]()): Currently not available 7 | * Ropsten Contract([Link]()): Currently not available 8 | 9 | ### Definition 10 | 11 | >**Proof Of loyalty** - As a consensus mechanism SparkleLoyalty provides rewards to users through a series of smart contracts that deter service abuse through the use of verified and trusted addressing. The SparkeLoyalty program rewards users for simply holding thier Sparkle for a predetermined time period currently set at ~24h. 12 | 13 | 14 | ### Sparkle Token Information 15 | 16 | >Sparkle token (SPRKL) is an ERC20 token built on the Ethereum network. The Sparkle token use case is similar to other digital assets and can be bought, sold and traded. Sparkle's use case however diverges from this standard expectation through it's main purpose being to support the SparkleLoyalty program and all program users. Sparkle token was built using the [OpenZeppelin](https://github.com/OpenZeppelin/openzeppelin-contracts) secure contract library for contract development. 17 | 18 | | **Field** | **Type** | **Value** | 19 | | :--------------- | :-------- | :------------------------------------------------------ | 20 | | Name | string | Sparkle | 21 | | Symbol | string | SPRKL | 22 | | Decimals | uint256 | 8 | 23 | | Total Supply | uint256 | 70,000,000 | 24 | | Mintable | Boolean | False | 25 | 26 | **Note** Sparkle (SPRKL) was designed with a finite supply of tokens and as such there will never be more than 70 million tokens in circulation. 27 | 28 | 29 | ### Sparkle Token Metrics 30 | 31 | >Through various campaigns and give-aways, Sparkle token (SPRKL) has been distributed to introduce new users to the SparkleLoyalty program. Following is a brief table of known Sparkle token current distribution metrics. 32 | 33 | | **Field** | **Type** | **Value** | 34 | | :--------------- | :-------- | :------------------------------------------------------ | 35 | | AirDrop | uint256 | TBD | 36 | | Campaigns | uint256 | TBD | 37 | | Give-Aways | uint256 | TBD | 38 | | In Circulation | uint256 | 2,347,450 SPRKL | 39 | 40 | **Note** The list is not exaustive and represents a generalized view at the time of writing. 41 | 42 | 43 | ### SparkleLoyalty Information 44 | 45 | >SparkleLoyalty is a loyalty reward program designed and developed from the ground up to provide users a secure and robust program that rewards them for simply holding their Sparkle tokens. The SparkleLoyalty program operates similar to a traditional savings account in that it provides users with a consistant rate of return on their deposits that start out at 5%. Unlike a traditional savings account however, SparkleLoyalty program users get to choose the rate in which they claim thier rewards and in addition may choose to purchase a reward upgrade in the form of a Tier1(10%), Tier2(20%), or Tier3(30%) rate boost. 46 | 47 | **Note** Rewards are distributed in a, "first come, first served," basis gactored bythe user's claiming and loyalty withdraw habits. 48 | 49 | 50 | #### Loyalty Treasury Metrics 51 | 52 | >Following are the current SparkleLoyalty operations and development treasury address information. 53 | 54 | * SparkleLoyalty Treasury: [Loyalty Address](https://etherscan.io/token/0x4b7ad3a56810032782afce12d7d27122bdb96eff?a=0xa90c682f511b384706e592a8cad9121f1c17de86) 55 | * SparkleLoyalty DevTreasury: [Treasury Address](https://etherscan.io/token/0x4b7ad3a56810032782afce12d7d27122bdb96eff?a=0xbea52413e26c38b51cbcb0d3661a25f2097f8574) 56 | 57 | 58 | #### Loyalty Reward Structure 59 | 60 | >The following tables are provided as a guide to new and existing users when desciding to join or remain in the SparkleLoyalty program. The following tables are a reflection of the current program settings at the time of writing. 61 | 62 | 63 | #### Deposit Specifications 64 | 65 | >Following are the minimum/maximum allowed deposit values per address to join the SparkleLoyalty program. 66 | 67 | | **Field** | **Type** | **Value** | 68 | | :--------------- | :-------- | :----------------------------------------------------- | 69 | | Minimum deposit | uint256 | 1,000 SPRKL | 70 | | Maximum deposit | uint256 | 250,000 SPRKL | 71 | 72 | 73 | #### Maturity Specifications 74 | 75 | >Following are the maturity and minimum/maximum allowed days a SparkleLoyalty prgram user may earn on thier deposit over a given period of time. 76 | 77 | | **Field** | **Type** | **Value** | 78 | | :--------------- | :-------- | :----------------------------------------------------- | 79 | | Maturity Period | uint256 | 86400 seconds (~24h) | 80 | | Min. Period | uint256 | 86400 seconds (~24) | 81 | | Max. Period | uint256 | 31,536,000 seconds (~365d) | 82 | 83 | 84 | #### Reward/Bonus Specification 85 | 86 | >Following are the rates of return available to SparkleLoyalty program members including the available rate boost tiers and thier current pricing. 87 | 88 | | **Field** | **Type** | **Value** | **Price** | 89 | | :------------ | :-------- | :----------- | :---------------------------------------- | 90 | | Base Rate | uint256 | 15% APR | Free (Default) | 91 | | Tier1 Rate | uint256 | +10% Bonus | 0.10 Ethereum | 92 | | Tier2 Rate | uint256 | +20% Bonus | 0.20 Ethereum | 93 | | Tier3 Rate | uint256 | +30% Bonus | 0.30 Ethereum | 94 | 95 | **NOTICE** Users of the SparkleLoyalty program acknowledge that potential risks and/or the loss of tokens may occur through their use of the SparkleLoyalty platform. By joining users of the SparkleLoyalty program agree to not hold SparkleLoyalty Inc, and any and all subsiduaries therein owned wholy or in part liable. 96 | 97 | 98 | ### Security Considerations & Specifications 99 | 100 | #### Considerations 101 | 102 | >There are three main considerations when using a timestamp to execute a critical function in a contract, especially when actions involve fund transfer. 103 | 104 | ##### 1. Timestamp Manipulation 105 | 106 | >On the Etereum network a malicious miner can manulate the blockchains block timestamp by up to ~15 seconds. While not specific to the SparkleLoyalty program the following snippet of code demonstrates how a malicious miner could manipulate the blocktime stamp to thier advantage. 107 | 108 | 109 | ``` 110 | uint256 constant private salt = block.timestamp; 111 | 112 | function random(uint Max) constant private returns (uint256 result){ 113 | //get the best seed for randomness 114 | uint256 x = salt * 100/Max; 115 | uint256 y = salt * block.number/(salt % 5) ; 116 | uint256 seed = block.number/3 + (salt % 300) + Last_Payout + y; 117 | uint256 h = uint256(block.blockhash(seed)); 118 | 119 | return uint256((h / x)) % Max + 1; //random number between 1 and Max 120 | } 121 | ``` 122 | 123 | >When a contract uses the timestamp to seed a random number, the miner can actually post a timestamp within 15 seconds of the block being validated, effectively allowing the miner to precompute an option more favorable to them. Timestamps are not random and should not be used in that context. The consensus is the block timestamp should never be used as a source of randomness but rather a source of accuracy. 124 | 125 | 126 | ##### 2. The 15-second Rule 127 | 128 | >The Ethereum [Yellow Paper](https://ethereum.github.io/yellowpaper/paper.pdf) does not specify a constraint on how many blocks can drift in time. However it does implement protections against publishing a block to the Ethereum network before the current block timestamp. Ethereum protocol implementations such as [Geth](https://github.com/ethereum/go-ethereum/blob/4e474c74dc2ac1d26b339c32064d0bac98775e77/consensus/ethash/consensus.go#L45) and [Parity](https://github.com/OpenEthereum/open-ethereum/blob/73db5dda8c0109bb6bc1392624875078f973be14/ethcore/src/verification/verification.rs#L296-L307) both reject blocks as described previous but also reject blocks with a timestamp more than 15 seconds into the future. 129 | 130 | **Note**: If the resolution of your time-dependent event can vary by 15 seconds, then it may be safe to use a block.timestamp. 131 | 132 | 133 | ##### 3. Avoid using block.number like a block timestamp 134 | 135 | >It is possible to estimate a time delta using the block.number property and [average block time](https://etherscan.io/chart/blocktime), however this is not future proof as block times may change (such as [fork reorganizations and the difficulty bomb](https://github.com/ethereum/EIPs/issues/649)). In a sale spanning days, the 15-second rule allows one to achieve a more reliable estimate of time. 136 | 137 | See [SWC-116](https://swcregistry.io/docs/SWC-116) 138 | 139 | 140 | #### Response Expectations 141 | 142 | ##### Contract Pause 143 | >SparkleLoyalty and the contracts that comprise the program have been designed so they can be paused at any time by SparkleLoyalty Inc. staff. When a serious issue or malicious attack is detected the first action will be to pause the SparkleLoyalty program immediately to prevent any further losses or damange from occurring. 144 | 145 | **Note** When paused, all functions and features of the SparkleLoyalty program will be unavailable to users. This includes the ability to claim any additional rewards or withdraw existing loyalty rewards until the SparkleLoyalty program has been re-started or a suitable alternative solution has been implemented. 146 | 147 | 148 | ##### Contract Re-Deployment 149 | >In the extreme case where the SparkleLoyalty program cannot be restarted due to a malicious attack the staff at SparkleLoyalty Inc. reserve the right to re-deploy an updated loyalty contract to the Ethereum block chain and implement a process in which users can transfer theri loyalty balances to the new program and continue operation of the loyalty reward program. 150 | 151 | **Note** Our objective is to provide users with the most secure experience when earning loyalty rewards and while this is our driving goal this is not always possible. Should there be some kind of problem with the SparkleLoyalty program we will do our best to corredct the issues as soon as possible with the least amount of distruption to users as possible. 152 | 153 | 154 | ### Developers 155 | 156 | #### Getting Started 157 | 158 | >Getting started with the SparkleLoyalty program developers will need the following software tools. 159 | 160 | ##### Requirements 161 | 162 | * [NodeJS](https://nodejs.org/en/) (v10.11.0) 163 | * [Docker](https://www.docker.com/) (v19.03.8) 164 | * [TruffleSuite](https://www.trufflesuite.com/) (v5.1.23) 165 | * [Ganache-Cli](https://www.npmjs.com/package/ganache-cli) (v6.9.1) 166 | 167 | 168 | ##### NodeJS Packages 169 | 170 | * [OpenZeppelin](https://openzeppelin.com/contracts/) (v2.0) 171 | * [Babel/Core](https://www.npmjs.com/package/@babel/core) (v7.10.4) 172 | * [Truffle/HDWallet](https://www.npmjs.com/package/@truffle/hdwallet-provider/v/1.0.37) (v1.0.37) 173 | * [Truffle-Assertions](https://www.npmjs.com/package/truffle-assertions) (v0.9.2) 174 | * [Chai](https://www.npmjs.com/package/chai) (v4.2.0) 175 | * [DotEnv](https://www.npmjs.com/package/dotenv) (v8.2.0) 176 | 177 | 178 | ##### Install NodeJS 179 | 180 | >Please see the link provided above to download and install the NodeJS version appropriate for your development environment and operating system. 181 | 182 | **Note** It is beyond the scope of this document to provide the NodeJS installation process as there are many tutorials and content already available that describe the process in detail. 183 | 184 | 185 | ##### Install Docker 186 | 187 | >Please see the link provided above to download and install the Docker version appropriate for your development environment and operating system. 188 | 189 | **Note** It is beyond the scope of this document to provide the Docker or DockerDesktop installation process as there are many tutorials and content already available that describe the process in detail. 190 | 191 | 192 | ##### Install TruffleSuite 193 | 194 | >Developers interested in working with the SparkleLoyalty program repository will need to install the TruffleSuite set of smart contract tools including contract deployment and contract unit testing. To install TruffleSuite please follow the following step. 195 | 196 | ``` 197 | $ npm install -g truffle@5.1.23 198 | ``` 199 | 200 | ##### Install Ganache-CLI 201 | 202 | >Development of the SparkleLoyalty program was initially performed and deployed to a local Ethereum blockchain provided by Ganache-CLI. To install Ganach-CLI please follow the following step. 203 | 204 | ``` 205 | $ npm install -g ganache-cli 206 | ``` 207 | 208 | ##### Clone The SparkeLoyalty Repository 209 | 210 | >To begin developing with the SparkleLoyalty program developers will need to clone the source code repository for the project into a folder or directory on their computer to work from. To clone the SparkleLoyalty repository please create a working folder on your computer. 211 | 212 | >Once a working folder has been created please navigate into the newly created directory perform one of the following commands from the command line of your development enviroment to clone the repository. 213 | 214 | Using SSH: 215 | ``` 216 | $ git clone git@github.com:Sparklemobile/Sparkle-Proof-Of-Loyalty.git 217 | ``` 218 | 219 | Using: HTTPS 220 | 221 | ``` 222 | $ git clone https://github.com/Sparklemobile/Sparkle-Proof-Of-Loyalty.git 223 | ``` 224 | 225 | 226 | ##### Initialize Local NodeJS Project 227 | 228 | >Now that the SparkleLoyalty program source code has been cloned or downloaded the next step is to initalize the project. This step will install any required dependancies required that were not installed in a previous step. To initialize the newly download SparkleLoyalty repository please enter the following command from the command line of your development environment. 229 | 230 | ``` 231 | $ npm init 232 | ``` 233 | 234 | 235 | ##### Starting Your Local Blockchain 236 | 237 | >Before the contracts are compiled and migrated, a local Ethereum blockchain will be required to deploy the compiled contracts to. This step is being performed now as it seems most logical to have the running development blockchain up before the contracts are compiled and migrated. To start the Ganache-CLI local Ethereum blockchain please enter the following command from the command line of your development environment. 238 | 239 | ``` 240 | $ npm run ganache:cli 241 | ``` 242 | 243 | **Note** Starting the Ganache-CLI local blockchain is not required if the intention is to just compile the contracts in the project. 244 | 245 | **Note** The Ganache-CLI local blockchain has been configured to preserve it's data and to be persistant between executions. This means that the local server may be shutdown and restarted while preserving the blockchain state. 246 | 247 | 248 | ##### Compile SparkleLoyalty 249 | 250 | >The next step is to compile the SparkleLoyalty program contracts. Currently everything neede to accomplish this task should be installed and ready. To compile the SparkleLoyalty program contract please enter the following command from the command line of your development enviroment. 251 | 252 | ``` 253 | $ truffle compile --all 254 | ``` 255 | 256 | 257 | ##### Migrate SparkleLoyalty To Local Blockchain 258 | 259 | >After the SparkleLoyalty program contracts have been successfully compiled the next step is to deploy or migrate them to the local Ethereum blockchain provided by Ganache-CLI as outlined above. To migrate the compiled SparkleLoyalty contracts to the local blockchain enter the following command from the command line of your development environment. 260 | 261 | ``` 262 | $ truffle migrate --reset 263 | ``` 264 | 265 | **Note** The use of the --reset parameter is intentional and forces Truffle to migrate or deploy all contracts again. Without this parameter Truffle will attempt tp reuse contracts that have already been deployed to the blockchain it has recorded in it's migration file. The use of this parameter here is expressly to force Truffle to migrate all contracts regardless of being modified or not. 266 | 267 | 268 | ##### Run The Tests 269 | 270 | >For developers interested in ensuring they have a working version of the SparkleLoyalty program should consider running the provided tests. Running the provided test not only familiarlize teh developer with the functionality of the project but also ensure that eveything is working as expected out of the box. 271 | 272 | >A number of solidity contract tests have been provided that cover most if not all of the expected funcationality of the SparkleLoyalty program to ensure that the expected behaviour is intact. To run the tests simple enter the following command from the command line of your development environment. 273 | 274 | **Note** For successfull operation of the provided tests users must transfer tokens into the correct account addresses created by ganache-cli. It is left up to the reader to transfer the approriate tokens to the TREASURY and USER1 accounts addresses as specified in the respective test file. For the tests to function it is expected that USER1 address have at least a 1000*10e7 TOKEN balance. The TREASURY address should have at least a 5000*10e7 TOKEN balance. 275 | 276 | **Note** Additionally the provided tests assume the Sparkle Token Contract has been previously deployed to the local blockchain or testnet. This project does not include nor migrate the Sparkle Token contract to the target blockchain. Readers interested in executing the tests are required to change the 'onchainSparkleToken' variable to point to their deployed Sparkle Token instance address before performing the tests. 277 | 278 | 279 | Test: All: 280 | ``` 281 | $ npm run test:all 282 | ``` 283 | 284 | Test: RewardTiers: 285 | ``` 286 | $ npm run test:rewardtiers 287 | ``` 288 | 289 | Test: Timestamp: 290 | ``` 291 | $ npm run test:timestamp 292 | ``` 293 | 294 | Test: Loyalty: 295 | ``` 296 | $ npm run test:loyalty 297 | ``` 298 | 299 | 300 | **Note** Tests relying on specific timing expectations occasionally fail the testing sequence. This is due to fluctuations in time and load on the system running the local blockchain. Often re-running the tests or running the specific test on its own fixes this problem. 301 | 302 | 303 | #### MythX Audits 304 | 305 | >The provided npm scripts that perform MythX Smart Contract Auditing are bound to information not provided in this repository. Readers interested in executing the provided MythX audits will be required to create their own MythX account, setup a MythX api/secret and configure thier development environmemnt to use this information. Once configured the MythX audit scripts should execute without issue. 306 | 307 | 308 | ### Additional Developer Information 309 | 310 | >Following are some additional points of information developers interested in SparkleLoyalty program development. 311 | 312 | #### Basic (UML) Diagram For Textual Description 313 | 314 | >The following [(UML)](https://plantuml.com/) diagram visualy demonstrates the inter-contract relationship of the SparkleLoyalty program and how the various contracts work together to provide functionality to the SparkleLoyalty program and its operational life cycle. 315 | 316 | *NEEDS UPDATE* 317 | 318 | 319 | #### Known Issues 320 | 321 | >Known issues can be found on the SparkleLoyalty Program's GitHub Issue tracker as well as viewed publicly using pre-audit reports compiled by [Chainsecurity](https://securify.chainsecurity.com/) 322 | 323 | 324 | #### When Making Pull Requests a Test File is Required 325 | 326 | >When submitting a pull request that attempts to fix an existing bug or is meant to demonstrate the existence of a bug please provide a fully operational test that demonstrates and recreates the issue beineg described. 327 | 328 | **Note** Pull requests submitted not accompanied by a working test will be ignored and/or deleted. 329 | 330 | 331 | ### Looking For More Information 332 | 333 | >To learn more about the SparkleLoyalty program please join our [telegram](https://t.me/Sparklemobile) and speak with any of our admins regarding how to get started earning rewards. 334 | 335 | 336 | ### Sources 337 | * [The-15-second-rule](https://consensys.github.io/smart-contract-best-practices/recommendations/#the-15-second-rule) 338 | * [SWCregistry ](https://swcregistry.io/) 339 | * [Build Highly Secure, Decentralized Application](https://books.google.ca/books/about/Advanced_Blockchain_Development.html?id=lOiZDwAAQBAJ&printsec=frontcover&source=kp_read_button&redir_esc=y#v=twopage&q&f=false) 340 | -------------------------------------------------------------------------------- /test/TestSparkleLoyalty.js: -------------------------------------------------------------------------------- 1 | /// Obtain artifacts for timestamp contact 2 | const SparkleLoyalty = artifacts.require('./SparkleLoyalty'); 3 | const SparkleTimestamp = artifacts.require('./SparkleTimestamp'); 4 | const SparkleRewardTiers = artifacts.require('./SparkleRewardTiers'); 5 | /// Include assertion libraries to support tests 6 | const assert = require('chai').assert; 7 | const truffleAssert = require('truffle-assertions'); 8 | /// Set SparkleToken onchain address 9 | const onchainSparkleToken = '0x14d8d4e089a4ae60f315be178434c651d11f9b9a'; 10 | /// Initialize reference to BN object 11 | const BN = web3.utils.BN; 12 | 13 | contract('SparkleLoyalty - Test coverage', async accounts => { 14 | let st, pol, tsi; 15 | 16 | it('Initialize contract(s)', async () => { 17 | /// Set account literals 18 | OWNER = accounts[0]; 19 | SUDOPOL = accounts[1]; 20 | USER1 = accounts[2]; 21 | USER2 = accounts[3]; 22 | SPARKLE = onchainSparkleToken; 23 | /// Initialize contract(s) 24 | pol = await SparkleLoyalty.deployed({ overwrite: true }); 25 | tsi = await SparkleTimestamp.deployed({ overwrite: true }); 26 | rti = await SparkleRewardTiers.deployed({ overwrite: true }); 27 | st = new web3.eth.Contract([{ "constant": true, "inputs": [], "name": "name", "outputs": [{ "name": "", "type": "string" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [{ "name": "spender", "type": "address" }, { "name": "value", "type": "uint256" }], "name": "approve", "outputs": [{ "name": "", "type": "bool" }], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": true, "inputs": [], "name": "_tokenMaxSupply", "outputs": [{ "name": "", "type": "uint256" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [], "name": "totalSupply", "outputs": [{ "name": "", "type": "uint256" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [{ "name": "from", "type": "address" }, { "name": "to", "type": "address" }, { "name": "value", "type": "uint256" }], "name": "transferFrom", "outputs": [{ "name": "", "type": "bool" }], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": true, "inputs": [], "name": "_tokenDecimals", "outputs": [{ "name": "", "type": "uint8" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [], "name": "decimals", "outputs": [{ "name": "", "type": "uint8" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [{ "name": "spender", "type": "address" }, { "name": "addedValue", "type": "uint256" }], "name": "increaseAllowance", "outputs": [{ "name": "", "type": "bool" }], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": true, "inputs": [{ "name": "owner", "type": "address" }], "name": "balanceOf", "outputs": [{ "name": "", "type": "uint256" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [], "name": "renounceOwnership", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": true, "inputs": [], "name": "owner", "outputs": [{ "name": "", "type": "address" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [], "name": "isOwner", "outputs": [{ "name": "", "type": "bool" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [], "name": "symbol", "outputs": [{ "name": "", "type": "string" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [], "name": "_tokenName", "outputs": [{ "name": "", "type": "string" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [{ "name": "spender", "type": "address" }, { "name": "subtractedValue", "type": "uint256" }], "name": "decreaseAllowance", "outputs": [{ "name": "", "type": "bool" }], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": false, "inputs": [{ "name": "to", "type": "address" }, { "name": "value", "type": "uint256" }], "name": "transfer", "outputs": [{ "name": "", "type": "bool" }], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": true, "inputs": [{ "name": "owner", "type": "address" }, { "name": "spender", "type": "address" }], "name": "allowance", "outputs": [{ "name": "", "type": "uint256" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [], "name": "_tokenVersion", "outputs": [{ "name": "", "type": "string" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [], "name": "_tokenSymbol", "outputs": [{ "name": "", "type": "string" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [{ "name": "newOwner", "type": "address" }], "name": "transferOwnership", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], "payable": false, "stateMutability": "nonpayable", "type": "constructor" }, { "anonymous": false, "inputs": [{ "indexed": true, "name": "from", "type": "address" }, { "indexed": true, "name": "to", "type": "address" }, { "indexed": false, "name": "value", "type": "uint256" }], "name": "Transfer", "type": "event" }, { "anonymous": false, "inputs": [{ "indexed": true, "name": "owner", "type": "address" }, { "indexed": true, "name": "spender", "type": "address" }, { "indexed": false, "name": "value", "type": "uint256" }], "name": "Approval", "type": "event" }, { "anonymous": false, "inputs": [{ "indexed": true, "name": "previousOwner", "type": "address" }, { "indexed": true, "name": "newOwner", "type": "address" }], "name": "OwnershipTransferred", "type": "event" }], onchainSparkleToken); 28 | // Set up additional literals 29 | POLADDR = pol.address; 30 | TSIADDR = tsi.address; 31 | RTIADDR = rti.address; 32 | /// Set pol contract address as timestamp controller address 33 | await tsi.setContractAddress(pol.address, { from: OWNER }); 34 | /// Set the reward time period to 86400 seconds (~24h) 35 | await tsi.setTimePeriod(60 * 60 * 24, { from: OWNER }); 36 | /// Return success if controller address changes were made 37 | return assert(await tsi.getContractAddress.call({ from: OWNER }) == pol.address && await tsi.getTimePeriod.call() == 86400); 38 | }); 39 | 40 | /** 41 | * @dev Token contract address testing block 42 | */ 43 | describe('Testing getTokenAddress()/SetTokenAddress() operations', async () => { 44 | 45 | /** 46 | * @dev Test getTokenAddress() 47 | */ 48 | it("getTokenAddress() passed", async () => { 49 | assert(await pol.getTokenAddress.call() == st._address); 50 | }); 51 | 52 | /** 53 | * @dev Test setTokenAddress(0x0, {from: OWNER }) 54 | */ 55 | it("setTokenAddress(0x0, {from: OWNER}) failed", async () => { 56 | await pol.setTokenAddress(0x0, { from: OWNER }) 57 | .then((response) => { 58 | /// Should not make it here, return failure 59 | return assert(false); 60 | }) 61 | .catch((error) => { 62 | let err = new String(error); 63 | assert(err.includes('Error: invalid address')); 64 | }); 65 | }); 66 | 67 | /** 68 | * @dev Test setTokenAddress(SUDOPOL, {from: 0x0}) 69 | */ 70 | it("setTokenAddress(SUDOPOL, {from: 0x0}) failed", async () => { 71 | await pol.setTokenAddress(SUDOPOL, {from: 0x0}) 72 | .then((response) => { 73 | /// Should not get here, return failure 74 | return assert(false); 75 | }) 76 | .catch((error) => { 77 | let err = new String(error); 78 | return assert(err.includes('from not found')); 79 | }); 80 | }); 81 | 82 | /** 83 | * @dev setTokenAddress(SUDOPOL, {from: USER1}) 84 | */ 85 | it('setTokenAddress(SUDOPOL, {from: USER1}) failed', async () => { 86 | await pol.setTokenAddress(SUDOPOL, { from: USER1 }) 87 | .then((response) => { 88 | /// Should not make it here, return failure 89 | return assert(false); 90 | }) 91 | .catch((error) => { 92 | let err = new String(error); 93 | return assert(err.includes('revert')); 94 | }) 95 | }); 96 | 97 | /** 98 | * @dev setTokenAddress(SUDOPOL, {from: OWNER }) 99 | */ 100 | it('setTokenAddress(SUDOPOL, {from: OWNER}) passed', async () => { 101 | let currentAddress = await pol.getTokenAddress.call(); 102 | await pol.setTokenAddress(SUDOPOL, { from: OWNER }); 103 | let newAddress = await pol.getTokenAddress.call(); 104 | assert(currentAddress != newAddress && newAddress == SUDOPOL); 105 | }); 106 | 107 | }) 108 | 109 | /** 110 | * @dev Timestamp contract address testing block 111 | */ 112 | describe('Testing getTimestampAddress()/setTimestampAddress() operations', async () => { 113 | 114 | /** 115 | * @dev Test getTimestampAddress() 116 | */ 117 | it("getTimestampAddress() passed", async () => { 118 | assert.equal(await pol.getTimestampAddress.call(), tsi.address, "Incorrect timestamp address"); 119 | }); 120 | 121 | /** 122 | * @dev Testing setTimestampAddress(0x0, { from: OWNER }) 123 | */ 124 | it("setTimestampAddress(0x0, {from: OWNER}) failed", async () => { 125 | await pol.setTimestampAddress(0x0, { from: OWNER }) 126 | .then((response) => { 127 | /// Should not get here, return failure 128 | return assert(false); 129 | }) 130 | .catch((error) => { 131 | let err = new String(error); 132 | return assert(err.includes('Error: invalid address')); 133 | }); 134 | }); 135 | 136 | /** 137 | * @dev Testing setTimestampAddress(SUDOPOL, { from: 0x0 }) 138 | */ 139 | it("setTimestampAddress(SUDOPOL, {from: 0x0}) failed", async () => { 140 | await pol.setTimestampAddress(SUDOPOL, { from: 0x0 }) 141 | .then((response) => { 142 | /// Should not get here, return failure 143 | return assert(false); 144 | }) 145 | .catch((error) => { 146 | let err = new String(error); 147 | return assert(err.includes('from not found')); 148 | }); 149 | }); 150 | 151 | /** 152 | * @dev Testing setTimestampAddress(SUDOPOL, { from: USER1 }) 153 | */ 154 | it('setTimestampAddress(SUDOPOL, {from: USER1}) failed', async () => { 155 | await pol.setTimestampAddress(SUDOPOL, { from: USER1 }). 156 | then((response) => { 157 | /// Should not get here, return failure 158 | return assert(false); 159 | }) 160 | .catch((error) => { 161 | let err = new String(error); 162 | return assert(err.includes('revert')); 163 | }) 164 | }); 165 | 166 | /** 167 | * @dev Testing setTimestampAddress(SUDOPOL, { from: OWNER }) 168 | */ 169 | it('setTimestampAddress(SUDOPOL, {from: OWNER}) passed', async () => { 170 | let currentAddress = await pol.getTimestampAddress.call(); 171 | await pol.setTimestampAddress(SUDOPOL, { from: OWNER }); 172 | let newAddress = await pol.getTimestampAddress.call(); 173 | assert(currentAddress != newAddress && newAddress == SUDOPOL); 174 | }); 175 | 176 | }); 177 | 178 | /** 179 | * @dev Treasury address testing block 180 | */ 181 | describe('Testing getTreasuryAddress()/setTreasuryAddress() operations', async () => { 182 | 183 | /** 184 | * @dev Test getTreasuryAddress() 185 | */ 186 | it("getTreasuryAddress() passed", async () => { 187 | assert.equal(await pol.getTreasuryAddress.call(), accounts[1], "Incorrect treasury address"); 188 | }); 189 | 190 | /** 191 | * @dev Testing setTreasuryAddress(0x0, { from: OWNER }) 192 | */ 193 | it("setTreasuryAddress(0x0, {from: OWNER}) failed", async () => { 194 | await pol.setTreasuryAddress(0x0, { 195 | from: OWNER 196 | }) 197 | .then((response) => { 198 | /// Should not get here, return failure 199 | return assert(false); 200 | }) 201 | .catch((error) => { 202 | let err = new String(error); 203 | return assert(err.includes('Error: invalid address')); 204 | }); 205 | }); 206 | 207 | /** 208 | * @dev Testing setTreasuryAddress(SUDOPOL, { from: 0x0 }) 209 | */ 210 | it("setTreasuryAddress(SUDOPOL, {from: 0x0}) failed", async () => { 211 | await pol.setTreasuryAddress(SUDOPOL, { 212 | from: 0x0 213 | }) 214 | .then((response) => { 215 | /// Should not get here, return failure 216 | return assert(false); 217 | }) 218 | .catch((error) => { 219 | let err = new String(error); 220 | return assert(err.includes('from not found')); 221 | }); 222 | }); 223 | 224 | /** 225 | * @dev Testing setTreasuryAddress(SUDOPOL, { from: USER1 }) 226 | */ 227 | it('setTreasuryAddress(SUDOPOL, {from: USER1}) failed', async () => { 228 | await pol.setTreasuryAddress(SUDOPOL, { 229 | from: USER1 230 | }). 231 | then((response) => { 232 | /// Should not get here, return failure 233 | return assert(false); 234 | }) 235 | .catch((error) => { 236 | let err = new String(error); 237 | return assert(err.includes('revert')); 238 | }) 239 | }); 240 | 241 | /** 242 | * @dev Testing setTreasuryAddress(SUDOPOL, { from: OWNER }) 243 | */ 244 | it('setTreasuryAddress(USER2, {from: OWNER}) passed', async () => { 245 | let currentAddress = await pol.getTreasuryAddress.call(); 246 | await pol.setTreasuryAddress(USER2, { from: OWNER }); 247 | let newAddress = await pol.getTreasuryAddress.call(); 248 | // console.log('currentAddress:', currentAddress); 249 | // console.log('newAddress:', newAddress); 250 | // console.log('SUDOPOL:', USER2); 251 | assert(currentAddress != newAddress && newAddress == USER2); 252 | }); 253 | 254 | }); 255 | 256 | /** 257 | * @dev Collection address testing block 258 | */ 259 | describe('Testing getCollectionAddress()/setCollectionAddress() operations', async () => { 260 | 261 | /** 262 | * @dev Testing getCollectionAddress() 263 | */ 264 | it("getCollectionAddress() passed", async () => { 265 | assert.equal(await pol.getCollectionAddress.call(), accounts[4], "Incorrect collection address"); 266 | }); 267 | 268 | /** 269 | * @dev Testing setCollectionAddress(0x0, { from: OWNER }) 270 | */ 271 | it("setCollectionAddress(0x0, {from: OWNER}) failed", async () => { 272 | await pol.setCollectionAddress(0x0, { from: OWNER }) 273 | .then((response) => { 274 | /// Should not get here, return failure 275 | return assert(false); 276 | }) 277 | .catch((error) => { 278 | let err = new String(error); 279 | return assert(err.includes('Error: invalid address')); 280 | }); 281 | }); 282 | 283 | /** 284 | * @dev Testing setCollectionAddress(SUDOPOL, { from: 0x0 }) 285 | */ 286 | it("setCollectionAddress(SUDOPOL, {from: 0x0}) failed", async () => { 287 | await pol.setCollectionAddress(SUDOPOL, { from: 0x0 }) 288 | .then((response) => { 289 | /// Should not get here, return failure 290 | return assert(false); 291 | }) 292 | .catch((error) => { 293 | let err = new String(error); 294 | return assert(err.includes('from not found')); 295 | }); 296 | }); 297 | 298 | /** 299 | * @dev Testing setCollectionAddress(SUDOPOL, { from: USER1 }) 300 | */ 301 | it('setCollectionAddress(SUDOPOL, {from: USER1}) failed', async () => { 302 | await pol.setCollectionAddress(SUDOPOL, { from: USER1 }). 303 | then((response) => { 304 | /// Should not get here, return failure 305 | return assert(false); 306 | }) 307 | .catch((error) => { 308 | let err = new String(error); 309 | return assert(err.includes('revert')); 310 | }) 311 | }); 312 | 313 | /** 314 | * @dev Testing setCollectionAddress(SUDOPOL, { from: OWNER }) 315 | */ 316 | it('setCollectionAddress(SUDOPOL, {from: OWNER}) passed', async () => { 317 | let currentAddress = await pol.getCollectionAddress.call(); 318 | await pol.setCollectionAddress(SUDOPOL, { from: OWNER }); 319 | let newAddress = await pol.getCollectionAddress.call(); 320 | assert(currentAddress != newAddress && newAddress == SUDOPOL); 321 | }); 322 | 323 | }); 324 | 325 | /** 326 | * @dev Reward tiers address testing block 327 | */ 328 | describe('Testing getRewardTiersAddress()/setRewardTiersAddress() operations', async () => { 329 | 330 | /** 331 | * @dev Testing getRewardTiersAddress() 332 | */ 333 | it("getRewardTiersAddress() passed", async () => { 334 | assert.equal(await pol.getRewardTiersAddress.call(), RTIADDR, "Incorrect collection address"); 335 | }); 336 | 337 | /** 338 | * @dev Testing setCollectionAddress(0x0, { from: OWNER }) 339 | */ 340 | it("setRewardTiersAddress(0x0, {from: OWNER}) failed", async () => { 341 | await pol.setRewardTiersAddress(0x0, { from: OWNER }) 342 | .then((response) => { 343 | /// Should not get here, return failure 344 | return assert(false); 345 | }) 346 | .catch((error) => { 347 | let err = new String(error); 348 | return assert(err.includes('Error: invalid address')); 349 | }); 350 | }); 351 | 352 | /** 353 | * @dev Testing setCollectionAddress(SUDOPOL, { from: 0x0 }) 354 | */ 355 | it("setRewardTiersAddress(SUDOPOL, {from: 0x0}) failed", async () => { 356 | await pol.setRewardTiersAddress(SUDOPOL, { from: 0x0 }) 357 | .then((response) => { 358 | /// Should not get here, return failure 359 | return assert(false); 360 | }) 361 | .catch((error) => { 362 | let err = new String(error); 363 | return assert(err.includes('from not found')); 364 | }); 365 | }); 366 | 367 | /** 368 | * @dev Testing setCollectionAddress(SUDOPOL, { from: USER1 }) 369 | */ 370 | it('setRewardTiersAddress(SUDOPOL, {from: USER1}) failed', async () => { 371 | await pol.setRewardTiersAddress(SUDOPOL, { from: USER1 }) 372 | .then((response) => { 373 | /// Should not get here, return failure 374 | return assert(false); 375 | }) 376 | .catch((error) => { 377 | let err = new String(error); 378 | return assert(err.includes('revert')); 379 | }) 380 | }); 381 | 382 | /** 383 | * @dev Testing setRewardTiersAddress(SUDOPOL, { from: OWNER }) 384 | */ 385 | it('setRewardTiersAddress(SUDOPOL, {from: OWNER}) passed', async () => { 386 | let currentAddress = await pol.getRewardTiersAddress.call(); 387 | await pol.setRewardTiersAddress(SUDOPOL, { from: OWNER }); 388 | let newAddress = await pol.getRewardTiersAddress.call(); 389 | assert(currentAddress != newAddress && newAddress == SUDOPOL); 390 | }); 391 | 392 | }); 393 | 394 | /** 395 | * @dev Minimum deposit values testing block 396 | */ 397 | describe('getMinProof()/setMinProof() operations', async () => { 398 | 399 | it("getMinProof() passed", async () => { 400 | let minDeposit = new BN(await pol.getMinProof.call({ from: accounts[3] })); 401 | let testAmount = new BN(1000 * 10e7); 402 | assert.equal(minDeposit.toString(), testAmount.toString()); 403 | }); 404 | 405 | it("setMinProof(0x0, {from: OWNER}) failed", async () => { 406 | await pol.setMinProof(0x0, { from: OWNER }) 407 | .then((response) => { 408 | /// Should not get here, return failure 409 | return assert(false); 410 | }) 411 | .catch((error) => { 412 | let err = new String(error); 413 | return assert(err.includes('revert')); 414 | }); 415 | }); 416 | 417 | it("setMinProof(2000, {from: 0x0}) failed", async () => { 418 | await pol.setMinProof(2000, { from: 0x0 }). 419 | then((response) => { 420 | /// Should not get here, return failure 421 | return assert(false); 422 | }) 423 | .catch((error) => { 424 | let err = new String(error); 425 | return assert(err.includes('from not found')); 426 | }); 427 | }); 428 | 429 | it("setMinProof(2000, {from: USER1}) failed", async () => { 430 | await pol.setMinProof(2000, { from: USER1 }) 431 | .then((response) => { 432 | /// Should not get here, return failure 433 | return assert(false); 434 | }) 435 | .catch((error) => { 436 | let err = new String(error); 437 | return assert(err.includes('revert')); 438 | }); 439 | }); 440 | 441 | it("setMinProof(2000, {from: OWNER}) passed", async () => { 442 | let currentMin = await pol.getMinProof.call(); 443 | await pol.setMinProof(2000, { from: OWNER }); 444 | let newMin = await pol.getMinProof.call(); 445 | assert(currentMin != newMin && newMin / 10e7 == 2000); 446 | }); 447 | 448 | }); 449 | 450 | /** 451 | * @dev Maximum deposit values testing block 452 | */ 453 | describe('Testing getMaxProof()/setMaxProof() operations', async () => { 454 | 455 | it("getMaxProof() passed", async () => { 456 | let maxDeposit = new BN(await pol.getMaxProof()); 457 | var testAmount = new BN(250000 * 10e7); 458 | assert.equal(maxDeposit.toString(), testAmount.toString()); 459 | }); 460 | 461 | it("setMaxProof(0x0, {from: OWNER}) failed", async () => { 462 | await pol.setMaxProof(0x0, { from: OWNER }) 463 | .then((response) => { 464 | /// Should not get here, return failure 465 | return assert(false); 466 | }) 467 | .catch((error) => { 468 | let err = new String(error); 469 | return assert(err.includes('Invalid amount')); 470 | }); 471 | }); 472 | 473 | it("setMaxProof(2000, {from: 0x0}) failed", async () => { 474 | await pol.setMaxProof(2000, { from: 0x0 }) 475 | .then((response) => { 476 | /// Should not get here, return failure 477 | return assert(false); 478 | }) 479 | .catch((error) => { 480 | let err = new String(error); 481 | return assert(err.includes('from not found')); 482 | }); 483 | }); 484 | 485 | it("setMaxProof(2000, {from: USER1}) failed", async () => { 486 | await pol.setMaxProof(2000, { from: USER1 }) 487 | .then((response) => { 488 | /// Should not get here, return failure 489 | return assert(false); 490 | }) 491 | .catch((error) => { 492 | let err = new String(error); 493 | return assert(err.includes('revert')); 494 | }); 495 | }); 496 | 497 | it("setMaxProof(2000, {from: owner}) passed", async () => { 498 | let currentMax = await pol.getMaxProof.call(); 499 | await pol.setMaxProof(2000, { from: OWNER }); 500 | let newMax = await pol.getMaxProof.call(); 501 | assert(currentMax != newMax && newMax / 10e7 == 2000); 502 | }); 503 | 504 | it("setMaxProof(500000, {from: owner}) passed", async () => { 505 | let currentMax = await pol.getMaxProof.call(); 506 | await pol.setMaxProof(500000, { from: OWNER }); 507 | let newMax = await pol.getMaxProof.call(); 508 | assert(currentMax != newMax && newMax / 10e7 == 500000); 509 | }); 510 | }); 511 | 512 | /** 513 | * @dev Various tests for remaining functions not under other coverage 514 | */ 515 | describe('Testing getSentGasAmount()/setSentGasAmount() operations', async () => { 516 | 517 | /** 518 | * @dev Testing getSetGasAmount() 519 | */ 520 | it('getSentGasAmount() passed', async () => { 521 | assert.equal(await pol.getSentGasAmount.call(), 25317, "Invalid sent gas amount returned"); 522 | }); 523 | 524 | /** 525 | * @dev Testing setCollectionAddress(42, { from: 0x0 }) 526 | */ 527 | it("setSentGasAmount(42, {from: 0x0}) failed", async () => { 528 | await pol.setSentGasAmount(42, { from: 0x0 }) 529 | .then((response) => { 530 | // Should not get here, return failure 531 | return assert(false); 532 | }) 533 | .catch((error) => { 534 | let err = new String(error); 535 | return assert(err.includes('from not found')); 536 | }); 537 | }); 538 | 539 | /** 540 | * @dev Testing setSentGasAmount(42, { from: USER1 }) 541 | */ 542 | it('setSentGasAmount(42, {from: USER1}) failed', async () => { 543 | await pol.setSentGasAmount(42, { from: USER1 }) 544 | .then((response) => { 545 | /// Should not get here, return failure 546 | return assert(false); 547 | }) 548 | .catch((error) => { 549 | let err = new String(error); 550 | return assert(err.includes('caller is not the owner')); 551 | }) 552 | }); 553 | 554 | /** 555 | * @dev Testing setRewardTiersAddress(SUDOPOL, { from: OWNER }) 556 | */ 557 | it('setSentGasAmount(42, {from: OWNER}) passed', async () => { 558 | await pol.setSentGasAmount(42, { from: OWNER }) 559 | .then((tx) => { 560 | truffleAssert.eventEmitted(tx, 'GasSentChanged', (ev) => { 561 | return (ev[0] == 42); 562 | }); 563 | }) 564 | .catch((err) => { 565 | return assert(false); 566 | }); 567 | }); 568 | 569 | /** 570 | * @dev Testing getSetGasAmount() 571 | */ 572 | it('getSentGasAmount() passed', async () => { 573 | assert.equal(await pol.getSentGasAmount.call(), 42, "Invalid sent gas amount returned"); 574 | }); 575 | 576 | }); 577 | 578 | }); 579 | -------------------------------------------------------------------------------- /test/TestSparkleRewardTiers.js: -------------------------------------------------------------------------------- 1 | /// Obtain artifacts for reward tier contract 2 | const SparkleRewardTiers = artifacts.require('./SparkleRewardTiers'); 3 | /// Include assertion libraries to support tests 4 | const assert = require('chai').assert; 5 | const truffleAssert = require('truffle-assertions'); 6 | 7 | /** 8 | * @title System reward tier0 tests 9 | * @author MrBitKoin (SparkleLoyalty Inc.) (c) 2019-2020 10 | */ 11 | contract('SparkleRewardTiers - Tier0 thru Tier3 test coverage', async accounts => { 12 | let rti; 13 | let tier1eth, tier2eth, tier3eth, tier4eth; 14 | 15 | /** 16 | * @dev Initialize contracts used throughout this set of tests 17 | */ 18 | it('Initialize contract', async () => { 19 | /// Set account literals 20 | OWNER = accounts[0]; 21 | SUDOPOL = accounts[1]; 22 | USER1 = accounts[2]; 23 | USER2 = accounts[3]; 24 | /// Initialize contract 25 | rti = await SparkleRewardTiers.deployed({ overwrite: true }); 26 | // Set price comparison variables 27 | tier0eth = web3.utils.toWei('0.00', 'ether'); 28 | tier1eth = web3.utils.toWei('0.10', 'ether'); 29 | tier2eth = web3.utils.toWei('0.20', 'ether'); 30 | tier3eth = web3.utils.toWei('0.30', 'ether'); 31 | tier4eth = web3.utils.toWei('0.40', 'ether'); 32 | // Return success 33 | return assert(true); 34 | }) 35 | 36 | /** 37 | * @dev Tier0 testing block 38 | */ 39 | describe('Testing operations for tier0 (5%)', async () => { 40 | 41 | /** 42 | * @dev Test getEnabled(tier0) 43 | */ 44 | it('getEnabled(Tier0) is enabled', async () => { 45 | await rti.getEnabled.call(0) 46 | .then((response) => { 47 | /// Attempt to get enabled status of tier0 48 | assert.equal(response, true, 'Not enabled'); 49 | }) 50 | .catch((error) => { 51 | return assert(false); 52 | }) 53 | }); 54 | 55 | /** 56 | * @dev Test getRate(tier0) 57 | */ 58 | it('getRate(Tier0) should return 1.0', async () => { 59 | /// Attempt to get current tier0 rate 60 | let rate = await rti.getRate.call(0); 61 | /// Return success if expected address is returned 62 | assert.equal(rate.toString(), '100000000', 'Not enabled'); 63 | }); 64 | 65 | /** 66 | * @dev Test getPrice(tier0) 67 | */ 68 | it('getPrice(Tier0) should return 0eth', async () => { 69 | /// Return success if expected address is returned 70 | assert.equal(await rti.getPrice.call(0), tier0eth, 'Not enabled'); 71 | }); 72 | 73 | /** 74 | * @dev Test updateTier(Tier0, 2, 0.1eth, false) 75 | */ 76 | it('updateTier(Tier0, 200000000, tier1eth, false, {from: 0x0 }) should fail', async () => { 77 | /// Attemp to update Tier0 data 78 | await rti.updateTier(0, 200000000, tier1eth, false, { from: 0x0 }) 79 | .then(() => { 80 | /// Should not get here, return failure 81 | return assert(false); 82 | }) 83 | .catch((error) => { 84 | let err = new String(error); 85 | /// Return success if expected event emitted 86 | return assert(err.includes('from not found')); 87 | }); 88 | }); 89 | 90 | /** 91 | * @dev Test updateTier(Tier0, 2, 0.1eth, false) 92 | */ 93 | it('updateTier(Tier0, 200000000, tier1eth, false, { from: USER1 }) should fail', async () => { 94 | /// Attemp to update Tier0 data 95 | await rti.updateTier.call(0, 200000000, tier1eth, false, { from: USER1 }) 96 | .then((response) => { 97 | /// Should not get here, return failure 98 | console.log('response:', response); 99 | return assert(false); 100 | }) 101 | .catch((error) => { 102 | let err = new String(error); 103 | /// Return success if expected event emitted 104 | return assert(err.includes('revert')); 105 | }); 106 | }); 107 | 108 | /** 109 | * @dev Test updateTier(Tier0, 2, 0.1eth, false) 110 | */ 111 | it('updateTier(Tier0, 200000000, tier1eth, false, { from: OWNER }) should pass', async () => { 112 | /// Attemp to update Tier0 data 113 | let tx = await rti.updateTier(0, 200000000, tier1eth, false, {from: OWNER }); 114 | truffleAssert.eventEmitted(tx, 'TierUpdated', (event) => { 115 | /// Return success if expected event emitted 116 | return event[0].toString() === '0' && event[1] == (2.0 * 10e7) && event[2] == tier1eth && event[3] === false; 117 | }) 118 | }); 119 | 120 | /** 121 | * @dev Test deleteTier(Tier0) 122 | */ 123 | it('deleteTier(Tier0, { from: 0x0 }) should fail', async () => { 124 | /// Attemps to delete Tier0 record 125 | await rti.deleteTier.call(0, { from: 0x0 }) 126 | .then((response) => { 127 | /// Should not make it here, return failure 128 | return false; 129 | }) 130 | .catch((error) => { 131 | let err = new String(error); 132 | /// Return success if expected value is returned 133 | return err.includes('Invalid request'); 134 | }); 135 | }); 136 | 137 | /** 138 | * @dev Test deleteTier(Tier0) 139 | */ 140 | it('deleteTier(Tier0, { from: USER1 }) should fail', async () => { 141 | /// Attemps to delete Tier0 record 142 | await rti.deleteTier.call(0, { from: USER1 }) 143 | .then((response) => { 144 | /// Should not make it here, return failure 145 | return false; 146 | }) 147 | .catch((error) => { 148 | let err = new String(error); 149 | /// Return success if expected value is returned 150 | return err.includes('Invalid request'); 151 | }); 152 | }); 153 | 154 | /** 155 | * @dev Test deleteTier(Tier0) 156 | */ 157 | it('deleteTier(Tier0, { from: OWNER }) should fail', async () => { 158 | /// Attemps to delete Tier0 record 159 | await rti.deleteTier.call(0, { from: OWNER }) 160 | .then((response) => { 161 | /// Should not make it here, return failure 162 | return false; 163 | }) 164 | .catch((error) => { 165 | let err = new String(error); 166 | /// Return success if expected value is returned 167 | return err.includes('Invalid request'); 168 | }); 169 | }); 170 | 171 | }); 172 | 173 | /** 174 | * @dev Tier1 testing block 175 | */ 176 | describe('Testing operations for Tier1 (10%)', async () => { 177 | 178 | /** 179 | * @dev Test getEnabled(tier1) 180 | */ 181 | it('getEnabled(Tier1) is enabled', async () => { 182 | /// Attempt to get enabled status of tier0 183 | assert.equal(await rti.getEnabled.call(1), true, 'Not enabled'); 184 | }); 185 | 186 | /** 187 | * @dev Test getRate(tier1) 188 | */ 189 | it('getRate(Tier1) should return 1.1', async () => { 190 | /// Attempt to get current tier0 rate 191 | let rate = await rti.getRate.call(1); 192 | /// Return success if expected address is returned 193 | assert.equal(rate.toString(), '110000000', 'Not enabled'); 194 | }); 195 | 196 | /** 197 | * @dev Test getPrice(tier1) 198 | */ 199 | it('getPrice(Tier1) should return 0.10eth', async () => { 200 | /// Return success if expected address is returned 201 | assert.equal(await rti.getPrice.call(1), tier1eth, 'Not enabled'); 202 | }); 203 | 204 | /** 205 | * @dev Test updateTier(Tier1, 2, 0.1eth, false) 206 | */ 207 | it('updateTier(Tier1, 200000000, tier1eth, false, {from: 0x0 }) should fail', async () => { 208 | /// Attemp to update Tier0 data 209 | await rti.updateTier(1, 200000000, tier1eth, false, { from: 0x0 }) 210 | .then((response) => { 211 | /// Should not get here, return failure 212 | return assert(false); 213 | }) 214 | .catch((error) => { 215 | let err = new String(error); 216 | /// Return success if expected event emitted 217 | return assert(err.includes('from not found')); 218 | }); 219 | }); 220 | 221 | /** 222 | * @dev Test updateTier(Tier1, 2, 0.1eth, false) 223 | */ 224 | it('updateTier(Tier1, 200000000, tier1eth, false, { from: USER1 }) should fail', async () => { 225 | /// Attemp to update Tier0 data 226 | await rti.updateTier(1, 200000000, tier1eth, false, { from: USER1 }) 227 | .then((response) => { 228 | /// Should not get here, return failure 229 | return assert(false); 230 | }) 231 | .catch((error) => { 232 | let err = new String(error); 233 | /// Return success if expected event emitted 234 | return assert(err.includes('revert')); 235 | }); 236 | }); 237 | 238 | /** 239 | * @dev Test updateTier(Tier1, 2, 0.1eth, false) 240 | */ 241 | it('updateTier(Tier1, 200000000, tier1eth, false, { from: OWNER }) should pass', async () => { 242 | /// Attemp to update Tier0 data 243 | let tx = await rti.updateTier(1, 200000000, tier1eth, false, { from: OWNER }); 244 | truffleAssert.eventEmitted(tx, 'TierUpdated', (event) => { 245 | /// Return success if expected event emitted 246 | return event[0].toString() === '1' && event[1] == (2.0 * 10e7) && event[2] == tier1eth && event[3] === false; 247 | }) 248 | }); 249 | 250 | /** 251 | * @dev Test deleteTier(Tier1) 252 | */ 253 | it('deleteTier(Tier1, { from: 0x0 }) should fail', async () => { 254 | /// Attemps to delete Tier0 record 255 | await rti.deleteTier(1, { from: 0x0 }) 256 | .then((response) => { 257 | /// Should not make it here, return failure 258 | return false; 259 | }) 260 | .catch((error) => { 261 | let err = new String(error); 262 | /// Return success if expected value is returned 263 | return err.includes('Invalid request'); 264 | }); 265 | }); 266 | 267 | /** 268 | * @dev Test deleteTier(Tier1) 269 | */ 270 | it('deleteTier(Tier1, { from: USER1 }) should fail', async () => { 271 | /// Attemps to delete Tier0 record 272 | await rti.deleteTier(1, { from: USER1 }) 273 | .then((response) => { 274 | /// Should not make it here, return failure 275 | return false; 276 | }) 277 | .catch((error) => { 278 | let err = new String(error); 279 | /// Return success if expected value is returned 280 | return err.includes('Invalid request'); 281 | }); 282 | }); 283 | 284 | /** 285 | * @dev Test deleteTier(Tier1) 286 | */ 287 | it('deleteTier(Tier1, { from: OWNER }) should fail', async () => { 288 | /// Attemps to delete Tier0 record 289 | await rti.deleteTier(1, { from: OWNER }) 290 | .then((response) => { 291 | /// Should not make it here, return failure 292 | return false; 293 | }) 294 | .catch((error) => { 295 | let err = new String(error); 296 | /// Return success if expected value is returned 297 | return err.includes('Invalid request'); 298 | }); 299 | }); 300 | 301 | }); 302 | 303 | /** 304 | * @dev Tier2 testing block 305 | */ 306 | describe('Testing operations for Tier2 (20%)', async () => { 307 | /** 308 | * @dev Test getEnabled(tier2) 309 | */ 310 | it('getEnabled(Tier2) is enabled', async () => { 311 | /// Attempt to get enabled status of tier2 312 | assert.equal(await rti.getEnabled.call(2), true, 'Not enabled'); 313 | }); 314 | 315 | /** 316 | * @dev Test getRate(tier2) 317 | */ 318 | it('getRate(Tier1) should return 1.2', async () => { 319 | /// Attempt to get current tier2 rate 320 | let rate = await rti.getRate.call(2); 321 | /// Return success if expected address is returned 322 | assert.equal(rate.toString(), '120000000', 'Not enabled'); 323 | }); 324 | 325 | /** 326 | * @dev Test getPrice(tier2) 327 | */ 328 | it('getPrice(Tier2) should return 0.20eth', async () => { 329 | /// Return success if expected address is returned 330 | assert.equal(await rti.getPrice.call(2), tier2eth, 'Not enabled'); 331 | }); 332 | 333 | /** 334 | * @dev Test updateTier(Tier2, 2, 0.1eth, false) 335 | */ 336 | it('updateTier(Tier2, 200000000, tier1eth, false, {from: 0x0 }) should fail', async () => { 337 | /// Attemp to update Tier2 data 338 | await rti.updateTier(2, 200000000, tier1eth, false, { from: 0x0 }) 339 | .then((response) => { 340 | /// Should not get here, return failure 341 | return assert(false); 342 | }) 343 | .catch((error) => { 344 | let err = new String(error); 345 | return assert(err.includes('from not found')); 346 | }); 347 | }); 348 | 349 | /** 350 | * @dev Test updateTier(Tier2, 2, 0.1eth, false) 351 | */ 352 | it('updateTier(Tier2, 200000000, tier1eth, false, { from: USER1 }) should fail', async () => { 353 | /// Attemp to update Tier2 data 354 | await rti.updateTier(2, 200000000, tier1eth, false, { from: USER1 }) 355 | .then((response) => { 356 | /// SHould not get here, return failure 357 | return assert(false); 358 | }) 359 | .catch((error) => { 360 | let err = new String(error); 361 | /// Return success if expected event emitted 362 | return assert(err.includes('revert')); 363 | }); 364 | }); 365 | 366 | /** 367 | * @dev Test updateTier(Tier2, 2, 0.1eth, false) 368 | */ 369 | it('updateTier(Tier2, 200000000, tier1eth, false, { from: OWNER }) should pass', async () => { 370 | /// Attemp to update Tier2 data 371 | let tx = await rti.updateTier(2, 200000000, tier1eth, false, { from: OWNER }); 372 | truffleAssert.eventEmitted(tx, 'TierUpdated', (event) => { 373 | /// Return success if expected event emitted 374 | return event[0].toString() === '2' && event[1] == (2.0 * 10e7) && event[2] == tier1eth && event[3] === false; 375 | }) 376 | }); 377 | 378 | /** 379 | * @dev Test deleteTier(Tier2) 380 | */ 381 | it('deleteTier(Tier2, { from: 0x0 }) should fail', async () => { 382 | /// Attemps to delete Tier2 record 383 | await rti.deleteTier(2, { from: 0x0 }) 384 | .then((response) => { 385 | /// Should not make it here, return failure 386 | return false; 387 | }) 388 | .catch((error) => { 389 | let err = new String(error); 390 | /// Return success if expected value is returned 391 | return err.includes('Invalid request'); 392 | }); 393 | }); 394 | 395 | /** 396 | * @dev Test deleteTier(Tier2) 397 | */ 398 | it('deleteTier(Tier2, { from: USER1 }) should fail', async () => { 399 | /// Attemps to delete Tier2 record 400 | await rti.deleteTier(2, { from: USER1 }) 401 | .then((response) => { 402 | /// Should not make it here, return failure 403 | return false; 404 | }) 405 | .catch((error) => { 406 | let err = new String(error); 407 | /// Return success if expected value is returned 408 | return err.includes('Invalid request'); 409 | }); 410 | }); 411 | 412 | /** 413 | * @dev Test deleteTier(Tier2) 414 | */ 415 | it('deleteTier(Tier2, { from: OWNER }) should fail', async () => { 416 | /// Attemps to delete Tier2 record 417 | await rti.deleteTier(1, { from: OWNER }) 418 | .then((response) => { 419 | /// Should not make it here, return failure 420 | return false; 421 | }) 422 | .catch((error) => { 423 | let err = new String(error); 424 | /// Return success if expected value is returned 425 | return err.includes('Invalid request'); 426 | }); 427 | }); 428 | 429 | }); 430 | 431 | /** 432 | * @dev Tier3 testing block 433 | */ 434 | describe('Testing operations for Tier3 (30%)', async () => { 435 | 436 | /** 437 | * @dev Test getEnabled(tier3) 438 | */ 439 | it('getEnabled(Tier3) is enabled', async () => { 440 | /// Attempt to get enabled status of tier2 441 | assert.equal(await rti.getEnabled.call(3), true, 'Not enabled'); 442 | }); 443 | 444 | /** 445 | * @dev Test getRate(tier3) 446 | */ 447 | it('getRate(Tier3) should return 1.3', async () => { 448 | /// Attempt to get current tier3 rate 449 | let rate = await rti.getRate.call(3); 450 | /// Return success if expected address is returned 451 | assert.equal(rate.toString(), '130000000', 'Not enabled'); 452 | }); 453 | 454 | /** 455 | * @dev Test getPrice(tier3) 456 | */ 457 | it('getPrice(Tier3) should return 0.30eth', async () => { 458 | /// Return success if expected address is returned 459 | assert.equal(await rti.getPrice.call(3), tier3eth, 'Not enabled'); 460 | }); 461 | 462 | /** 463 | * @dev Test updateTier(Tier3, 2, 0.1eth, false) 464 | */ 465 | it('updateTier(Tier3, 200000000, tier1eth, false, {from: 0x0 }) should fail', async () => { 466 | /// Attemp to update Tier3 data 467 | await rti.updateTier(3, 200000000, tier1eth, false, { from: 0x0 }) 468 | .then((response) => { 469 | /// Should not get here, return failure 470 | return assert(false); 471 | }) 472 | .catch((error) => { 473 | let err = new String(error); 474 | return assert(err.includes('from not found')) 475 | }); 476 | }); 477 | 478 | /** 479 | * @dev Test updateTier(Tier3, 2, 0.1eth, false) 480 | */ 481 | it('updateTier(Tier3, 200000000, tier1eth, false, { from: USER1 }) should fail', async () => { 482 | /// Attemp to update Tier3 data 483 | await rti.updateTier(3, 200000000, tier1eth, false, { from: USER1 }) 484 | .then((response) => { 485 | /// Should not get here, return failure 486 | return assert(false); 487 | }) 488 | .catch((error) => { 489 | let err = new String(error); 490 | return assert(err.includes('revert')); 491 | }); 492 | }); 493 | 494 | /** 495 | * @dev Test updateTier(Tier3, 2, 0.1eth, false) 496 | */ 497 | it('updateTier(Tier3, 200000000, tier1eth, false, { from: OWNER }) should pass', async () => { 498 | /// Attemp to update Tier3 data 499 | let tx = await rti.updateTier(3, 200000000, tier1eth, false, { 500 | from: OWNER 501 | }); 502 | truffleAssert.eventEmitted(tx, 'TierUpdated', (event) => { 503 | /// Return success if expected event emitted 504 | return event[0].toString() === '3' && event[1] == (2.0 * 10e7) && event[2] == tier1eth && event[3] === false; 505 | }) 506 | }); 507 | 508 | /** 509 | * @dev Test deleteTier(Tier3) 510 | */ 511 | it('deleteTier(Tier3, { from: 0x0 }) should fail', async () => { 512 | /// Attemps to delete Tier3 record 513 | await rti.deleteTier(3, { from: 0x0 }) 514 | .then((response) => { 515 | /// Should not make it here, return failure 516 | return false; 517 | }) 518 | .catch((error) => { 519 | let err = new String(error); 520 | /// Return success if expected value is returned 521 | return err.includes('Invalid request'); 522 | }); 523 | }); 524 | 525 | /** 526 | * @dev Test deleteTier(Tier3) 527 | */ 528 | it('deleteTier(Tier3, { from: USER1 }) should fail', async () => { 529 | /// Attemps to delete Tier2 record 530 | await rti.deleteTier(3, { from: USER1 }) 531 | .then((response) => { 532 | /// Should not make it here, return failure 533 | return false; 534 | }) 535 | .catch((error) => { 536 | let err = new String(error); 537 | /// Return success if expected value is returned 538 | return err.includes('Invalid request'); 539 | }); 540 | }); 541 | 542 | /** 543 | * @dev Test deleteTier(Tier2) 544 | */ 545 | it('deleteTier(Tier3, { from: OWNER }) should fail', async () => { 546 | /// Attemps to delete Tier2 record 547 | await rti.deleteTier(3, { from: OWNER }) 548 | .then((response) => { 549 | /// Should not make it here, return failure 550 | return false; 551 | }) 552 | .catch((error) => { 553 | let err = new String(error); 554 | /// Return success if expected value is returned 555 | return err.includes('Invalid request'); 556 | }); 557 | }); 558 | 559 | }); 560 | 561 | /** 562 | * @dev Tier4 testing block 563 | */ 564 | describe('Testing operations for additional Tier4 (40%)', async () => { 565 | 566 | /** 567 | * @dev Test addTier(Tier4, 1.4, 0.4eth, true) 568 | */ 569 | it('addTier(Tier4, 140000000, tier4eth, true, { from: 0x0 }) should fail', async () => { 570 | await rti.addTier(4, 1.4 * 10e7, tier4eth, true, { from: 0x0 }) 571 | .then((response) => { 572 | /// Should not get here, return failure 573 | return assert(false); 574 | }) 575 | .catch((error) => { 576 | let err = new String(error); 577 | return assert(err.includes('from not found')) 578 | }); 579 | }); 580 | 581 | /** 582 | * @dev Test addTier(Tier4, 1.4, tier4eth, true) 583 | */ 584 | it('addTier(Tier4, 140000000, tier4eth, true, { from: USER1 }) should fail', async () => { 585 | await rti.addTier(4, 1.4 * 10e7, tier4eth, true, { from: USER1 }) 586 | .then((response) => { 587 | /// Should not get here, return failure 588 | return assert(false); 589 | }) 590 | .catch((error) => { 591 | let err = new String(error); 592 | return assert(err.includes('revert')); 593 | }); 594 | }); 595 | 596 | /** 597 | * @dev Test addTier(Tier4, 140000000, tier4eth, true) 598 | */ 599 | it('addTier(Tier4, 140000000, tier4eth, true, { from: OWNER }) should pass', async () => { 600 | let tx = await rti.addTier(4, 1.4 * 10e7, tier4eth, true, { from: OWNER }); 601 | truffleAssert.eventEmitted(tx, 'TierAdded'); 602 | }); 603 | 604 | /** 605 | * @dev Test getEnabled(tier4) 606 | */ 607 | it('getEnabled(Tier4) is enabled', async () => { 608 | /// Attempt to get enabled status of tier4 609 | assert.equal(await rti.getEnabled.call(4), true, 'Not enabled'); 610 | }); 611 | 612 | /** 613 | * @dev Test getRate(tier4) 614 | */ 615 | it('getRate(Tier4) should return 1.4', async () => { 616 | /// Attempt to get current tier4 rate 617 | let rate = await rti.getRate.call(4); 618 | /// Return success if expected address is returned 619 | assert.equal(rate.toString(), '140000000', 'Not enabled'); 620 | }); 621 | 622 | /** 623 | * @dev Test getPrice(tier4) 624 | */ 625 | it('getPrice(Tier4) should return 0.40eth', async () => { 626 | /// Return success if expected address is returned 627 | assert.equal(await rti.getPrice.call(4), tier4eth, 'Not enabled'); 628 | }); 629 | 630 | /** 631 | * @dev Test updateTier(Tier4, 2, 0.1eth, false) 632 | */ 633 | it('updateTier(Tier4, 200000000, tier1eth, false, {from: 0x0 }) should fail', async () => { 634 | /// Attemp to update Tier4 data 635 | await rti.updateTier(4, 200000000, tier1eth, false, { from: 0x0 }) 636 | .then((response) => { 637 | /// Should not get here, return failure 638 | return assert(false); 639 | }) 640 | .catch((error) => { 641 | let err = new String(error); 642 | return assert(err.includes('from not found')); 643 | }); 644 | }); 645 | 646 | /** 647 | * @dev Test updateTier(Tier4, 2, 0.1eth, false) 648 | */ 649 | it('updateTier(Tier4, 200000000, tier1eth, false, { from: USER1 }) should fail', async () => { 650 | /// Attemp to update Tier3 data 651 | await rti.updateTier(4, 200000000, tier1eth, false, { from: USER1 }) 652 | .then((response) => { 653 | /// Should not get here, return failure 654 | return assert(false); 655 | }) 656 | .catch((error) => { 657 | let err = new String(error); 658 | return assert(err.includes('revert')); 659 | }); 660 | }); 661 | 662 | /** 663 | * @dev Test updateTier(Tier4, 2, 0.1eth, false) 664 | */ 665 | it('updateTier(Tier4, 200000000, tier1eth, false, { from: OWNER }) should pass', async () => { 666 | /// Attemp to update Tier3 data 667 | let tx = await rti.updateTier(4, 200000000, tier1eth, false, { from: OWNER }); 668 | truffleAssert.eventEmitted(tx, 'TierUpdated', (event) => { 669 | /// Return success if expected event emitted 670 | return event[0].toString() === '4' && event[1] == (2.0 * 10e7) && event[2] == tier1eth && event[3] === false; 671 | }) 672 | }); 673 | 674 | /** 675 | * @dev Test deleteTier(Tier4) 676 | */ 677 | it('deleteTier(Tier4, { from: 0x0 }) should fail', async () => { 678 | /// Attemps to delete Tier4 record 679 | await rti.deleteTier(4, { from: 0x0 }) 680 | .then((response) => { 681 | /// Should not make it here, return failure 682 | return false; 683 | }) 684 | .catch((error) => { 685 | let err = new String(error); 686 | /// Return success if expected value is returned 687 | return err.includes('Invalid request'); 688 | }); 689 | }); 690 | 691 | /** 692 | * @dev Test deleteTier(Tier4) 693 | */ 694 | it('deleteTier(Tier4, { from: USER1 }) should fail', async () => { 695 | /// Attemps to delete Tier2 record 696 | await rti.deleteTier(4, { from: USER1 }) 697 | .then((response) => { 698 | /// Should not make it here, return failure 699 | return false; 700 | }) 701 | .catch((error) => { 702 | let err = new String(error); 703 | /// Return success if expected value is returned 704 | return err.includes('Invalid request'); 705 | }); 706 | }); 707 | 708 | /** 709 | * @dev Test deleteTier(Tier4) 710 | */ 711 | it('deleteTier(Tier4, { from: OWNER }) should pass', async () => { 712 | /// Attemps to delete Tier4 record 713 | await rti.deleteTier(4, { from: OWNER }) 714 | .then((response) => { 715 | /// Should not make it here, return failure 716 | return false; 717 | }) 718 | .catch((error) => { 719 | let err = new String(error); 720 | /// Return success if expected value is returned 721 | return err.includes('Invalid request'); 722 | }); 723 | }); 724 | 725 | /** 726 | * @dev Test getEnabled(tier4) 727 | */ 728 | it('getEnabled(Tier4) is disabled', async () => { 729 | /// Attempt to get enabled status of tier4 730 | assert.equal(await rti.getEnabled.call(4), false, 'Not enabled'); 731 | }); 732 | 733 | /** 734 | * @dev Test getRate(tier4) 735 | */ 736 | it('getRate(Tier4) should fail', async () => { 737 | /// Attempt to get current tier4 rate 738 | let rate = await rti.getRate.call(4); 739 | /// Return success if expected address is returned 740 | assert.equal(rate.toString(), '0', 'Not enabled'); 741 | }); 742 | 743 | /** 744 | * @dev Test getPrice(tier4) 745 | */ 746 | it('getPrice(Tier4) should fail', async () => { 747 | /// Return success if expected address is returned 748 | assert.equal(await rti.getPrice.call(4), tier0eth, 'Not enabled'); 749 | }); 750 | 751 | }); 752 | 753 | }); 754 | -------------------------------------------------------------------------------- /test/TestSparkleLoyalty-Tier0.js: -------------------------------------------------------------------------------- 1 | /// Obtain artifacts for Loyalty, Timestamp and RewardTiers contacts 2 | const SparkleLoyalty = artifacts.require('./SparkleLoyalty'); 3 | const SparkleTimestamp = artifacts.require('./SparkleTimestamp'); 4 | /// Include assertion libraries to support tests 5 | const assert = require('chai').assert; 6 | const truffleAssert = require('truffle-assertions'); 7 | const helper = require("./helpers/truffle-time-helpers"); 8 | /// Set SparkleToken onchain address 9 | const onchainSparkleToken = '0x14d8d4e089a4ae60f315be178434c651d11f9b9a'; 10 | 11 | /** 12 | * @title Sparkle timestamp tests 13 | * @author SparkleLoyalty Inc. (c) 2019-2020 14 | */ 15 | contract('SparkleLoyalty - Workflow @ Tier0(5%)', async accounts => { 16 | /// Declare contract variables 17 | let st, pol, tsi; 18 | /// Declare eth value variables 19 | let OWNER, USER1, TREASURY; 20 | 21 | /** 22 | * @dev Initialize contracts used throughout this set of tests 23 | */ 24 | it('Initialize contract(s)', async() =>{ 25 | // Set account literals 26 | OWNER = accounts[0]; 27 | TREASURY = accounts[1]; 28 | USER1 = accounts[2]; 29 | USER2 = accounts[3]; 30 | COLLECTION = accounts[4]; 31 | // Initialize contract(s) 32 | st = new web3.eth.Contract([{ "constant": true, "inputs": [], "name": "name", "outputs": [{ "name": "", "type": "string" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [{ "name": "spender", "type": "address" }, { "name": "value", "type": "uint256" }], "name": "approve", "outputs": [{ "name": "", "type": "bool" }], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": true, "inputs": [], "name": "_tokenMaxSupply", "outputs": [{ "name": "", "type": "uint256" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [], "name": "totalSupply", "outputs": [{ "name": "", "type": "uint256" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [{ "name": "from", "type": "address" }, { "name": "to", "type": "address" }, { "name": "value", "type": "uint256" }], "name": "transferFrom", "outputs": [{ "name": "", "type": "bool" }], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": true, "inputs": [], "name": "_tokenDecimals", "outputs": [{ "name": "", "type": "uint8" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [], "name": "decimals", "outputs": [{ "name": "", "type": "uint8" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [{ "name": "spender", "type": "address" }, { "name": "addedValue", "type": "uint256" }], "name": "increaseAllowance", "outputs": [{ "name": "", "type": "bool" }], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": true, "inputs": [{ "name": "owner", "type": "address" }], "name": "balanceOf", "outputs": [{ "name": "", "type": "uint256" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [], "name": "renounceOwnership", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": true, "inputs": [], "name": "owner", "outputs": [{ "name": "", "type": "address" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [], "name": "isOwner", "outputs": [{ "name": "", "type": "bool" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [], "name": "symbol", "outputs": [{ "name": "", "type": "string" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [], "name": "_tokenName", "outputs": [{ "name": "", "type": "string" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [{ "name": "spender", "type": "address" }, { "name": "subtractedValue", "type": "uint256" }], "name": "decreaseAllowance", "outputs": [{ "name": "", "type": "bool" }], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": false, "inputs": [{ "name": "to", "type": "address" }, { "name": "value", "type": "uint256" }], "name": "transfer", "outputs": [{ "name": "", "type": "bool" }], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": true, "inputs": [{ "name": "owner", "type": "address" }, { "name": "spender", "type": "address" }], "name": "allowance", "outputs": [{ "name": "", "type": "uint256" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [], "name": "_tokenVersion", "outputs": [{ "name": "", "type": "string" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [], "name": "_tokenSymbol", "outputs": [{ "name": "", "type": "string" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [{ "name": "newOwner", "type": "address" }], "name": "transferOwnership", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], "payable": false, "stateMutability": "nonpayable", "type": "constructor" }, { "anonymous": false, "inputs": [{ "indexed": true, "name": "from", "type": "address" }, { "indexed": true, "name": "to", "type": "address" }, { "indexed": false, "name": "value", "type": "uint256" }], "name": "Transfer", "type": "event" }, { "anonymous": false, "inputs": [{ "indexed": true, "name": "owner", "type": "address" }, { "indexed": true, "name": "spender", "type": "address" }, { "indexed": false, "name": "value", "type": "uint256" }], "name": "Approval", "type": "event" }, { "anonymous": false, "inputs": [{ "indexed": true, "name": "previousOwner", "type": "address" }, { "indexed": true, "name": "newOwner", "type": "address" }], "name": "OwnershipTransferred", "type": "event" }], onchainSparkleToken); 33 | pol = await SparkleLoyalty.deployed({ overwrite: true }); 34 | tsi = await SparkleTimestamp.deployed({ overwrite: true }); 35 | /// Configure controller contract address 36 | await tsi.setContractAddress(pol.address, { from: OWNER }); 37 | /// Configure reward time period to be 86400 seconds (24H~) 38 | await tsi.setTimePeriod(60*60*24, {from: OWNER}); 39 | /// Declare eth amount variables 40 | return assert(await tsi.getContractAddress.call({ from: OWNER }) == pol.address && (await tsi.getTimePeriod.call())/*.toString()*/ == 86400); 41 | }); 42 | 43 | /** 44 | * @dev Test user token approval functionality 45 | */ 46 | describe('Step1: Approve 1000 Tokens', async() => { 47 | 48 | /** 49 | * @dev Test generic ERC20 approve operation 50 | */ 51 | it('SparkleToken.approve(pol.address, 1000 * 10e7) should pass', async () => { 52 | /// Attempt to approve 1000 tokens 53 | await st.methods.approve(pol.address, 1000 * 10e7).send({ from: USER1 }); 54 | /// Return success if expected values are returned 55 | return assert.equal(await st.methods.allowance(USER1, pol.address).call(), 1000 * 10e7); 56 | }); 57 | 58 | }); 59 | 60 | /** 61 | * @dev Test user token deposit/SparkleLoyalty join operation(s) 62 | */ 63 | describe('Step2: Deposit Tokens', async () => { 64 | 65 | /** 66 | * @dev Test SparkleLoyalty deposit operation 67 | */ 68 | it('depositLoyalty(1000 * 10e7, { from:USER1 }) should pass', async () => { 69 | /// Return success if expected values are returned 70 | return truffleAssert.eventEmitted(await pol.depositLoyalty(1000 * 10e7, { 71 | from: USER1 72 | }), 'DepositLoyaltyEvent'); 73 | }); 74 | 75 | /** 76 | * @dev Test that previous deposit operation was successfull 77 | */ 78 | it('SparkleToken.balanceOf(pol.address)POL Balance echeck should equal 1000 tokens', async () => { 79 | /// Return success if expected values are returned 80 | return assert.equal(await st.methods.balanceOf(pol.address).call(), 1000 * 10e7); 81 | }); 82 | 83 | /** 84 | * @dev Test additionally that alloance has been consumed 85 | */ 86 | it('Allowance re-check should equal 0 tokens', async () => { 87 | /// Return success if expected values are returned 88 | return assert.equal(await st.methods.allowance(USER1, pol.address).call(), 0); 89 | }); 90 | 91 | }); 92 | 93 | /** 94 | * @dev Test claim reward for Day 1 95 | */ 96 | describe('Step3: Claim loyalty Day 1', async () => { 97 | 98 | /** 99 | * @dev Test claim reward token for USER1 @6h~ 100 | */ 101 | it('Loyalty claim @ ~6h should fail', async () => { 102 | /// Advance blockchain by ~6h 103 | await helper.advanceTimeAndBlock(21600); 104 | /// Attempt to claim loyalty rewards for USER1 105 | await pol.claimLoyaltyReward({ from: USER1 }) 106 | .then((response) => { 107 | /// Should not get here, return failure 108 | return assert(false); 109 | }) 110 | .catch((error) => { 111 | let err = new String(error); 112 | /// Return success if expected error values returned 113 | return assert(err.includes('Not mature')); 114 | }); 115 | }); 116 | 117 | /** 118 | * @dev Test claim reward token for USER1 @12~ 119 | */ 120 | it('Loyalty claim @ ~12h should fail', async () => { 121 | /// Advance blockchain by ~6h 122 | await helper.advanceTimeAndBlock(21600); 123 | /// Attempt to claim loyalty rewards for USER1 124 | await pol.claimLoyaltyReward({ from: USER1 }) 125 | .then((response) => { 126 | /// Should not get here, return failure 127 | return assert(false); 128 | }) 129 | .catch((error) => { 130 | let err = new String(error); 131 | /// Return success if expected error values returned 132 | return assert(err.includes('Not mature')); 133 | }); 134 | }); 135 | 136 | /** 137 | * @dev Test claim reward token for USER1 @18h~ 138 | */ 139 | it('Loyalty claim @ ~18h should fail', async () => { 140 | /// Advance blockchain by ~6h 141 | await helper.advanceTimeAndBlock(21600); 142 | /// Attempt to claim loyalty rewards for USER1 143 | await pol.claimLoyaltyReward({ from: USER1 }) 144 | .then((response) => { 145 | /// Should not get here, return failure 146 | return assert(false); 147 | }) 148 | .catch((error) => { 149 | let err = new String(error); 150 | /// Return success if expected error values returned 151 | return assert(err.includes('Not mature')); 152 | }); 153 | }); 154 | 155 | /** 156 | * @dev Test claim reward token for USER1 @24h~ 157 | */ 158 | it('Loyalty claim @ ~24h should pass', async () => { 159 | /// Advance blockchain by ~6h 160 | await helper.advanceTimeAndBlock(21601); 161 | /// Return success if expected event was emitted from transaction 162 | return truffleAssert.eventEmitted(await pol.claimLoyaltyReward({ from: USER1 }), 'RewardClaimedEvent'); 163 | }); 164 | 165 | /** 166 | * @dev Test amount of tokens collected for Day 1 167 | */ 168 | it('Collected reward(s) should equal 0.13690000 tokens', async () => { 169 | /// Return success if expected values are returned 170 | assert.equal(await pol.getTokensCollected.call(USER1) / 10e7, 0.1369); 171 | }); 172 | }); 173 | 174 | /** 175 | * @dev Test claim reward for Day 2 176 | */ 177 | describe('Step3: Claim loyalty Day 2', async () => { 178 | 179 | /** 180 | * @dev Test claim reward token for USER1 @6h~ 181 | */ 182 | it('Loyalty claim @ ~6h should fail', async () => { 183 | /// Advance blockchain by ~6h 184 | await helper.advanceTimeAndBlock(21600); 185 | /// Attempt to claim loyalty rewards for USER1 186 | await pol.claimLoyaltyReward({ from: USER1 }) 187 | .then((response) => { 188 | /// Should not get here, return failure 189 | return assert(false); 190 | }) 191 | .catch((error) => { 192 | let err = new String(error); 193 | /// Return success if expected error values returned 194 | return assert(err.includes('Not mature')); 195 | }); 196 | }); 197 | 198 | /** 199 | * @dev Test claim reward token for USER1 @12~ 200 | */ 201 | it('Loyalty claim @ ~12h should fail', async () => { 202 | /// Advance blockchain by ~6h 203 | await helper.advanceTimeAndBlock(21600); 204 | /// Attempt to claim loyalty rewards for USER1 205 | await pol.claimLoyaltyReward({ from: USER1 }) 206 | .then((response) => { 207 | /// Should not get here, return failure 208 | return assert(false); 209 | }) 210 | .catch((error) => { 211 | let err = new String(error); 212 | /// Return success if expected error values returned 213 | return assert(err.includes('Not mature')); 214 | }); 215 | }); 216 | 217 | /** 218 | * @dev Test claim reward token for USER1 @18h~ 219 | */ 220 | it('Loyalty claim @ ~18h should fail', async () => { 221 | /// Advance blockchain by ~6h 222 | await helper.advanceTimeAndBlock(21600); 223 | /// Attempt to claim loyalty rewards for USER1 224 | await pol.claimLoyaltyReward({ from: USER1 }) 225 | .then((response) => { 226 | /// Should not get here, return failure 227 | return assert(false); 228 | }) 229 | .catch((error) => { 230 | let err = new String(error); 231 | /// Return success if expected error values returned 232 | return assert(err.includes('Not mature')); 233 | }); 234 | }); 235 | 236 | /** 237 | * @dev Test claim reward token for USER1 @24h~ 238 | */ 239 | it('Loyalty claim @ ~24h should pass', async () => { 240 | /// Advance blockchain by ~6h 241 | await helper.advanceTimeAndBlock(21601); 242 | /// Return success if expected event was emitted from transaction 243 | return truffleAssert.eventEmitted(await pol.claimLoyaltyReward({ from: USER1 }), 'RewardClaimedEvent'); 244 | 245 | }); 246 | 247 | /** 248 | * @dev Test amount of tokens collected for Day 2 249 | */ 250 | it('Collected reward(s) should equal 0.273818740 tokens', async () => { 251 | /// Return success if expected values are returned 252 | assert.equal(await pol.getTokensCollected.call(USER1) / 10e7, 0.27381874); 253 | }); 254 | }) 255 | 256 | /** 257 | * @dev Test claim reward for Day 3 258 | */ 259 | describe('Step3: Claim loyalty Day 3', async () => { 260 | 261 | /** 262 | * @dev Test claim reward token for USER1 @6h~ 263 | */ 264 | it('Loyalty claim @ ~6h should fail', async () => { 265 | /// Advance blockchain by ~6h 266 | await helper.advanceTimeAndBlock(21600); 267 | /// Attempt to claim loyalty rewards for USER1 268 | await pol.claimLoyaltyReward({ from: USER1 }) 269 | .then((response) => { 270 | /// Should not get here, return failure 271 | return assert(false); 272 | }) 273 | .catch((error) => { 274 | let err = new String(error); 275 | /// Return success if expected error values returned 276 | return assert(err.includes('Not mature')); 277 | }); 278 | }); 279 | 280 | /** 281 | * @dev Test claim reward token for USER1 @12~ 282 | */ 283 | it('Loyalty claim @ ~12h should fail', async () => { 284 | /// Advance blockchain by ~6h 285 | await helper.advanceTimeAndBlock(21600); 286 | /// Attempt to claim loyalty rewards for USER1 287 | await pol.claimLoyaltyReward({ from: USER1 }) 288 | .then((response) => { 289 | /// Should not get here, return failure 290 | return assert(false); 291 | }) 292 | .catch((error) => { 293 | let err = new String(error); 294 | /// Return success if expected error values returned 295 | return assert(err.includes('Not mature')); 296 | }); 297 | }); 298 | 299 | /** 300 | * @dev Test claim reward token for USER1 @18h~ 301 | */ 302 | it('Loyalty claim @ ~18h should fail', async () => { 303 | /// Advance blockchain by ~6h 304 | await helper.advanceTimeAndBlock(21600); 305 | /// Attempt to claim loyalty rewards for USER1 306 | await pol.claimLoyaltyReward({ from: USER1 }) 307 | .then((response) => { 308 | /// Should not get here, return failure 309 | return assert(false); 310 | }) 311 | .catch((error) => { 312 | let err = new String(error); 313 | /// Return success if expected error values returned 314 | return assert(err.includes('Not mature')); 315 | }); 316 | }); 317 | 318 | /** 319 | * @dev Test claim reward token for USER1 @24h~ 320 | */ 321 | it('Loyalty claim @ ~24h should pass', async () => { 322 | /// Advance blockchain by ~6h 323 | await helper.advanceTimeAndBlock(21601); 324 | /// Return success if expected event was emitted from transaction 325 | return truffleAssert.eventEmitted(await pol.claimLoyaltyReward({ from: USER1 }), 'RewardClaimedEvent'); 326 | }); 327 | 328 | /** 329 | * @dev Test amount of tokens collected for Day 3 330 | */ 331 | it('Collected reward(s) should equal 0.41075622 tokens', async () => { 332 | /// Return success if expected values are returned 333 | assert.equal(await pol.getTokensCollected.call(USER1) / 10e7, 0.41075622); 334 | }); 335 | }) 336 | 337 | /** 338 | * @dev Test claim reward for Day 4 339 | */ 340 | describe('Step3: Claim loyalty Day 4', async () => { 341 | 342 | /** 343 | * @dev Test claim reward token for USER1 @6h~ 344 | */ 345 | it('Loyalty claim @ ~6h should fail', async () => { 346 | /// Advance blockchain by ~6h 347 | await helper.advanceTimeAndBlock(21600); 348 | /// Attempt to claim loyalty rewards for USER1 349 | await pol.claimLoyaltyReward({ from: USER1 }) 350 | .then((response) => { 351 | /// Should not get here, return failure 352 | return assert(false); 353 | }) 354 | .catch((error) => { 355 | let err = new String(error); 356 | /// Return success if expected error values returned 357 | return assert(err.includes('Not mature')); 358 | }); 359 | }); 360 | 361 | /** 362 | * @dev Test claim reward token for USER1 @12~ 363 | */ 364 | it('Loyalty claim @ ~12h should fail', async () => { 365 | /// Advance blockchain by ~6h 366 | await helper.advanceTimeAndBlock(21600); 367 | /// Attempt to claim loyalty rewards for USER1 368 | await pol.claimLoyaltyReward({ from: USER1 }) 369 | .catch((err) => { 370 | assert.equal(err.reason, 'Not mature'); 371 | }); 372 | }); 373 | 374 | /** 375 | * @dev Test time remaining consistant with current position in reward cycle 376 | */ 377 | it('getTimeRemaining(USER1) should be >= 43150', async () => { 378 | /// Attermp to obtain the current time remaining until reward maturity 379 | await pol.getTimeRemaining.call(USER1) 380 | .then((response) => { 381 | /// Return success if expected value returned 382 | return assert(response[0] >= 43150); 383 | }) 384 | .catch((error) => { 385 | /// Should not make it here, return failure 386 | return assert(false); 387 | }); 388 | }); 389 | 390 | /** 391 | * @dev Test claim reward token for USER1 @18h~ 392 | */ 393 | it('Loyalty claim @ ~18h should fail', async () => { 394 | /// Advance blockchain by ~6h 395 | await helper.advanceTimeAndBlock(21600); 396 | /// Attempt to claim loyalty rewards for USER1 397 | await pol.claimLoyaltyReward({ from: USER1 }) 398 | .then((response) => { 399 | /// Should not get here, return failure 400 | return assert(false); 401 | }) 402 | .catch((error) => { 403 | let err = new String(error); 404 | /// Return success if expected error values returned 405 | return assert(err.includes('Not mature')); 406 | }); 407 | }); 408 | 409 | /** 410 | * @dev Test claim reward token for USER1 @24h~ 411 | */ 412 | it('Loyalty claim @ ~24h should pass', async () => { 413 | /// Advance blockchain by ~6h 414 | await helper.advanceTimeAndBlock(21601); 415 | /// Return success if expected event was emitted from transaction 416 | return truffleAssert.eventEmitted(await pol.claimLoyaltyReward({ from: USER1 }), 'RewardClaimedEvent'); 417 | }); 418 | 419 | /** 420 | * @dev Test amount of tokens collected for Day 4 421 | */ 422 | it('Collected reward(s) should equal 0.54771245 tokens', async () => { 423 | /// Return success if expected values are returned 424 | assert.equal(await pol.getTokensCollected.call(USER1) / 10e7, 0.54771245); 425 | }); 426 | }) 427 | 428 | /** 429 | * @dev Test claim reward for Day 5 430 | */ 431 | describe('Step3: Claim loyalty Day 5', async () => { 432 | 433 | /** 434 | * @dev Test claim reward token for USER1 @6h~ 435 | */ 436 | it('Loyalty claim @ ~6h should fail', async () => { 437 | /// Advance blockchain by ~6h 438 | await helper.advanceTimeAndBlock(21600); 439 | /// Attempt to claim loyalty rewards for USER1 440 | await pol.claimLoyaltyReward({ from: USER1 }) 441 | .then((response) => { 442 | /// Should not get here, return failure 443 | return assert(false); 444 | }) 445 | .catch((error) => { 446 | let err = new String(error); 447 | /// Return success if expected error values returned 448 | return assert(err.includes('Not mature')); 449 | }); 450 | }); 451 | 452 | /** 453 | * @dev Test claim reward token for USER1 @12~ 454 | */ 455 | it('Loyalty claim @ ~12h should fail', async () => { 456 | /// Advance blockchain by ~6h 457 | await helper.advanceTimeAndBlock(21600); 458 | /// Attempt to claim loyalty rewards for USER1 459 | await pol.claimLoyaltyReward({ from: USER1 }) 460 | .then((response) => { 461 | /// Should not get here, return failure 462 | return assert(false); 463 | }) 464 | .catch((error) => { 465 | let err = new String(error); 466 | /// Return success if expected error values returned 467 | return assert(err.includes('Not mature')); 468 | }); 469 | }); 470 | 471 | /** 472 | * @dev Test claim reward token for USER1 @18h~ 473 | */ 474 | it('Loyalty claim @ ~18h should fail', async () => { 475 | /// Advance blockchain by ~6h 476 | await helper.advanceTimeAndBlock(21600); 477 | /// Attempt to claim loyalty rewards for USER1 478 | await pol.claimLoyaltyReward({ from: USER1 }) 479 | .then((response) => { 480 | /// Should not get here, return failure 481 | return assert(false); 482 | }) 483 | .catch((error) => { 484 | let err = new String(error); 485 | /// Return success if expected error values returned 486 | return assert(err.includes('Not mature')); 487 | }); 488 | }); 489 | 490 | /** 491 | * @dev Test claim reward token for USER1 @24h~ 492 | */ 493 | it('Loyalty claim @ ~24h should pass', async () => { 494 | /// Advance blockchain by ~6h 495 | await helper.advanceTimeAndBlock(21601); 496 | /// Return success if expected event was emitted from transaction 497 | return truffleAssert.eventEmitted(await pol.claimLoyaltyReward({ from: USER1 }), 'RewardClaimedEvent'); 498 | }); 499 | 500 | /** 501 | * @dev Test amount of tokens collected for Day 5 502 | */ 503 | it('Collected reward(s) should equal 0.68468743 tokens', async () => { 504 | /// Return success if expected values are returned 505 | assert.equal(await pol.getTokensCollected.call(USER1) / 10e7, 0.68468743); 506 | }); 507 | 508 | }) 509 | 510 | /** 511 | * @dev Test claim reward for Day 365 skipping Days 6 thru 364 512 | */ 513 | describe('Step3: Claim loyalty Day 365 (Days 6-364 skipped)', async () => { 514 | 515 | /** 516 | * @dev Test claim reward token for USER1 @360d~ 517 | */ 518 | it('Claim loyalty attempt @ day ~365 should pass', async () => { 519 | /// Advance blockchain by ~360d 520 | await helper.advanceTimeAndBlock(31104000); 521 | /// Return success if expected event was emitted from transaction 522 | return truffleAssert.eventEmitted(await pol.claimLoyaltyReward({ from: USER1 }), 'RewardClaimedEvent'); 523 | }); 524 | 525 | /** 526 | * @dev Test amount of tokens collected for Day 365 527 | */ 528 | it('Collected reward(s) should equal 50.00243156 tokens', async () => { 529 | /// Return success if expected values are returned 530 | assert.equal(await pol.getTokensCollected.call(USER1) / 10e7, 50.00243156); 531 | }); 532 | 533 | /** 534 | * @dev Test times reward was claimed by user 535 | */ 536 | it('Total times claimed for user should equal 6', async() => { 537 | /// Return success if expected values are returned 538 | assert.equal(await pol.getTimesClaimed.call(USER1), 6); 539 | }); 540 | }) 541 | 542 | /** 543 | * @dev Test withdraw from SparkleLoyalty program 544 | */ 545 | describe('Step4: Withdraw loyalty', async() => { 546 | 547 | /** 548 | * @dev Test ERC20 approval of 10,000 tokens from treasury address 549 | */ 550 | it('Approve 10,000 tokens to SparkleLoyalty by TREASURY', async () => { 551 | /// Attempt to approve 10,000 tokens 552 | await st.methods.approve(pol.address, 10000 * 10e7).send({ from: TREASURY }); 553 | /// Return success if expected values are returned 554 | assert.equal(await st.methods.allowance(TREASURY, pol.address).call(), 10000 * 10e7); 555 | }); 556 | 557 | /** 558 | * @dev Test correct deposit balance for USER1 559 | */ 560 | it('Current deposit should equal 1000 tokens', async () => { 561 | /// Return success if expected values are returned 562 | assert.equal(await pol.getDepositBalance.call(USER1) / 10e7, 1000); 563 | }); 564 | 565 | /** 566 | * @dev Test currently collected loyalty rewards for USER1 567 | */ 568 | it('Currrent collected rewards should equal 50.00243156 tokens', async () => { 569 | /// Return success if expected values are returned 570 | assert.equal(await pol.getTokensCollected.call(USER1) / 10e7, 50.00243156); 571 | }); 572 | 573 | /** 574 | * @dev Test correct amount of reward tokens to be withdrawn 575 | */ 576 | it('Total withdraw amount should equal 1050.00243156 tokens', async () => { 577 | /// Return success if expected values are returned 578 | assert.equal(await pol.getTotalBalance.call(USER1) / 10e7, 1050.00243156); 579 | }); 580 | 581 | /** 582 | * @dev Test loyalty record address validation for USER1 583 | */ 584 | it('getLoyaltyAddress(USER1) should equal USER1', async () => { 585 | /// Attempt to obtain USER1's loyalty address 586 | await pol.getLoyaltyAddress.call(USER1) 587 | .then((response) => { 588 | /// Return success is expected value returned 589 | return assert.equal(response, USER1); 590 | }) 591 | .catch((error) => { 592 | /// Should never get here, return failure 593 | return assert(false); 594 | }) 595 | }); 596 | 597 | /** 598 | * @dev Test loyalty record tier validation for USER1 599 | */ 600 | it('getRewardTier(USER1) should equal Tier0', async () => { 601 | /// Attempt to obtain USER1's tier reward index 602 | await pol.getRewardTier.call(USER1) 603 | .then((response) => { 604 | /// Return success is expected value returned 605 | return assert.equal(response, 0); 606 | }) 607 | .catch((error) => { 608 | /// Should never get here, return failure 609 | return assert(false); 610 | }) 611 | }); 612 | 613 | /** 614 | * @dev Test loyalty record locked status for USER1 615 | */ 616 | it('isLocked(USER1) should return false', async () => { 617 | /// Attempt to determine if USER1's account has been locked 618 | await pol.isLocked.call(USER1) 619 | .then((response) => { 620 | /// Return success is expected value returned 621 | assert.equal(response, false); 622 | }) 623 | .catch((error) => { 624 | /// Should never get here, return failure 625 | return assert(false); 626 | }); 627 | }); 628 | 629 | /** 630 | * @dev Test loyalty record locking for USER1 631 | */ 632 | it('lockAccount(USER1, true, { from: OWNER }) should pass', async () => { 633 | /// Attempt to lock USER1's account 634 | await pol.lockAccount(USER1, true, { from: OWNER }) 635 | .then((response) => { 636 | truffleAssert.eventEmitted(response, 'LockedAccountEvent', (event) => { 637 | /// Return success is expected value returned 638 | return (event[0] == USER1 && event[1] == true); 639 | }); 640 | }) 641 | .catch((error) => { 642 | /// Should never get here, return failure 643 | return assert(false); 644 | }); 645 | }); 646 | 647 | /** 648 | * @dev Test loyalty record locked status for USER1 649 | */ 650 | it('isLocked(USER1) should return true', async () => { 651 | /// Attempt to determine if USER1's account has been locked 652 | await pol.isLocked.call(USER1) 653 | .then((response) => { 654 | /// Return success is expected value returned 655 | return assert.equal(response, true); 656 | }) 657 | .catch((error) => { 658 | /// Should never get here, return failure 659 | return assert(false); 660 | }); 661 | }); 662 | 663 | /// Declare scratch variables 664 | let preBalanceTreasury, preBalancePol; 665 | let postBalanceTreasury, postBalancePol; 666 | let deposit, collected; 667 | 668 | /** 669 | * @dev Test loyalty withdraw from locked USER1 account 670 | */ 671 | it('Withdraw loyalty for USER1 should fail (locked)', async () => { 672 | /// Obtain the balance of treasury and pol before withdraw 673 | preBalanceTreasury = await st.methods.balanceOf(TREASURY).call(); 674 | preBalancePol = await st.methods.balanceOf(pol.address).call(); 675 | /// Obtain current deposit and collected amounts for checking 676 | deposit = await pol.getDepositBalance.call(USER1); 677 | collected = await pol.getTokensCollected.call(USER1); 678 | /// Attempt to perform loyalty withdrawl for USER1 679 | await pol.withdrawLoyalty({ from: USER1 }) 680 | .then(async (tx) => { 681 | console.log('tx:', tx); 682 | /// Attempt to obtain balances of treasury and SparkleLoyalty addresses 683 | postBalanceTreasury = await st.methods.balanceOf(TREASURY).call(); 684 | postBalancePol = await st.methods.balanceOf(pol.address).call(); 685 | truffleAssert.eventEmitted(tx, 'LoyaltyWithdrawnEvent', (event) => { 686 | /// Return success is expected value returned 687 | return (event[0] == USER1 && event[2].eq(deposit.add(collected))); 688 | }); 689 | }) 690 | .catch((error) => { 691 | let err = new String(error); 692 | /// Return success if expected event was emitted from transaction 693 | return assert(err.includes('Locked')); 694 | }); 695 | }); 696 | 697 | /** 698 | * @dev Test loyalty record unlocking for USER1 699 | */ 700 | it('lockAccount(USER1, false, { from: OWNER }) should pass', async () => { 701 | /// Attempt to unlock USER1's account 702 | await pol.lockAccount(USER1, false, { from: OWNER }) 703 | .then((response) => { 704 | truffleAssert.eventEmitted(response, 'LockedAccountEvent', (event) => { 705 | /// Return success is expected value returned 706 | return (event[0] == USER1 && event[1] == false); 707 | }); 708 | }) 709 | .catch((error) => { 710 | /// Should never get here, return failure 711 | return assert(false); 712 | }) 713 | }); 714 | 715 | /** 716 | * @dev Test loyalty record locked status for USER1 717 | */ 718 | it('isLocked(USER1) should return false', async () => { 719 | /// Attempt to determine if USER1's account has been locked 720 | await pol.isLocked.call(USER1) 721 | .then((response) => { 722 | /// Return success is expected value returned 723 | return assert.equal(response, false); 724 | }) 725 | .catch((error) => { 726 | let err = new String(error); 727 | /// Should never get here, return failure 728 | return assert(false); 729 | }) 730 | }); 731 | 732 | /** 733 | * @dev Test loyalty withdraw from unlocked USER1 account 734 | */ 735 | it('Withdraw loyalty for USER1 should pass (unlocked)', async () => { 736 | // Obtain the balance of treasury and pol before withdraw 737 | preBalanceTreasury = await st.methods.balanceOf(TREASURY).call(); 738 | preBalancePol = await st.methods.balanceOf(pol.address).call(); 739 | // Obtain current deposit and collected amounts for checking 740 | deposit = await pol.getDepositBalance.call(USER1); 741 | collected = await pol.getTokensCollected.call(USER1); 742 | /// Attempt to perform loyalty withdrawl for USER1 743 | await pol.withdrawLoyalty({ from: USER1 }) 744 | .then(async (tx) => { 745 | /// Attempt to obtain balances of treasury and SparkleLoyalty addresses 746 | postBalanceTreasury = await st.methods.balanceOf(TREASURY).call(); 747 | postBalancePol = await st.methods.balanceOf(pol.address).call(); 748 | truffleAssert.eventEmitted(tx, 'LoyaltyWithdrawnEvent', (event) => { 749 | /// Return success if expected event was emitted from transaction 750 | return (event[0] == USER1 && event[1].eq(deposit.add(collected))); 751 | }); 752 | }) 753 | .catch((error) => { 754 | /// Return success if expected error encountered 755 | return assert(false); 756 | }); 757 | }); 758 | 759 | /** 760 | * @dev Check balances to verify proper operation of loyalty rewards program 761 | */ 762 | it('Post check balances should pass', async () => { 763 | /// Return success if result of balance calulation is expected 764 | return assert((preBalancePol - postBalancePol) == deposit && (preBalanceTreasury - postBalanceTreasury) == collected); 765 | }); 766 | 767 | /** 768 | * @dev Test subsequent attempt to claim loyalty after record deleted 769 | */ 770 | it('Loyalty claim attempt should fail', async () => { 771 | /// Attempt to claim loyalty rewards from withdrawn USER1 account 772 | await pol.claimLoyaltyReward.call({ from: USER1 }) 773 | .then((response) => { 774 | /// Should never get here, return faiure 775 | return assert(false); 776 | }) 777 | .catch((error) => { 778 | let err = new String(error); 779 | /// Return success if error value is expected 780 | return assert(err.includes('No record')); 781 | }); 782 | }); 783 | 784 | }); 785 | 786 | /** 787 | * @dev Test remaining SparkleLoyalty functions not in other coverages 788 | */ 789 | describe('Post: Audit claimed and token totals', async () => { 790 | 791 | /** 792 | * @dev Test total number of times a reward was claimed by all users 793 | */ 794 | it('getTotalTimesClaimed() should return 6', async () => { 795 | /// Attempt to obtain the total times a reward was claimed 796 | await pol.getTotalTimesClaimed.call() 797 | .then((count) => { 798 | /// Return success if expected value returned 799 | return assert.equal(count, 6); 800 | }) 801 | .catch((error) => { 802 | /// Should not get here, return failure 803 | return assert(false); 804 | }) 805 | }); 806 | 807 | /** 808 | * @dev Test total number of reward tokens claimed by all users 809 | */ 810 | it('getTotalTokensClaimed() should return 50.00243156', async () => { 811 | /// Attempt to obtain the total number of tokens claimed by all users 812 | await pol.getTotalTokensClaimed.call() 813 | .then((count) => { 814 | /// Return success if expected value is returned 815 | assert.equal(count, 5000243156); 816 | }) 817 | .catch((error) => { 818 | /// Should not get here, return failure 819 | return assert(false); 820 | }); 821 | }); 822 | 823 | }); 824 | 825 | /** 826 | * @dev Return any tokens to the appropriate addresses 827 | */ 828 | describe('Finalize: Return any tokens deposited', async() => { 829 | /** 830 | * @dev Upon success return the earned reward tokens back to the treasury 831 | */ 832 | it('Return tokens to Accounts[2]', async() => { 833 | /// Attempt to ERC20 transfer reward tokens earned through this test back to treasury 834 | await st.methods.transfer(accounts[1], 50.00243156 * 10e7).send({ from: accounts[2] }); 835 | }); 836 | 837 | }); 838 | 839 | }); 840 | -------------------------------------------------------------------------------- /contracts/SparkleLoyalty.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | 3 | /// SWC-103: Floating Pragma 4 | pragma solidity 0.6.12; 5 | 6 | import "@openzeppelin/contracts/math/SafeMath.sol"; 7 | import "@openzeppelin/contracts/access/Ownable.sol"; 8 | import '@openzeppelin/contracts/utils/Pausable.sol'; 9 | import '@openzeppelin/contracts/utils/ReentrancyGuard.sol'; 10 | import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; 11 | import './ISparkleTimestamp.sol'; 12 | import './ISparkleRewardTiers.sol'; 13 | 14 | /** 15 | * @dev Sparkle Loyalty Rewards 16 | * @author SparkleMobile Inc. 17 | */ 18 | contract SparkleLoyalty is Ownable, Pausable, ReentrancyGuard { 19 | 20 | /** 21 | * @dev Ensure math safety through SafeMath 22 | */ 23 | using SafeMath for uint256; 24 | 25 | // Gas to send with certain transations that may cost more in the future due to chain growth 26 | uint256 private gasToSendWithTX = 25317; 27 | // Base rate APR (5%) factored to 365.2422 gregorian days 28 | uint256 private baseRate = 0.00041069 * 10e7; // A full year is 365.2422 gregorian days (5%) 29 | 30 | // Account data structure 31 | struct Account { 32 | address _address; // Loyalty reward address 33 | uint256 _balance; // Total tokens deposited 34 | uint256 _collected; // Total tokens collected 35 | uint256 _claimed; // Total succesfull reward claims 36 | uint256 _joined; // Total times address has joined 37 | uint256 _tier; // Tier index of reward tier 38 | bool _isLocked; // Is the account locked 39 | } 40 | 41 | // tokenAddress of erc20 token address 42 | address private tokenAddress; 43 | 44 | // timestampAddress of time stamp contract address 45 | address private timestampAddress; 46 | 47 | // treasuryAddress of token treeasury address 48 | address private treasuryAddress; 49 | 50 | // collectionAddress to receive eth payed for tier upgrades 51 | address private collectionAddress; 52 | 53 | // rewardTiersAddress to resolve reward tier specifications 54 | address private tiersAddress; 55 | 56 | // minProofRequired to deposit of rewards to be eligibile 57 | uint256 private minRequired; 58 | 59 | // maxProofAllowed for deposit to be eligibile 60 | uint256 private maxAllowed; 61 | 62 | // totalTokensClaimed of all rewards awarded 63 | uint256 private totalTokensClaimed; 64 | 65 | // totalTimesClaimed of all successfully claimed rewards 66 | uint256 private totalTimesClaimed; 67 | 68 | // totalActiveAccounts count of all currently active addresses 69 | uint256 private totalActiveAccounts; 70 | 71 | // Accounts mapping of user loyalty records 72 | mapping(address => Account) private accounts; 73 | 74 | /** 75 | * @dev Sparkle Loyalty Rewards Program contract .cTor 76 | * @param _tokenAddress of token used for proof of loyalty rewards 77 | * @param _treasuryAddress of proof of loyalty token reward distribution 78 | * @param _collectionAddress of ethereum account to collect tier upgrade eth 79 | * @param _tiersAddress of the proof of loyalty tier rewards support contract 80 | * @param _timestampAddress of the proof of loyalty timestamp support contract 81 | */ 82 | constructor(address _tokenAddress, address _treasuryAddress, address _collectionAddress, address _tiersAddress, address _timestampAddress) 83 | public 84 | Ownable() 85 | Pausable() 86 | ReentrancyGuard() 87 | { 88 | // Initialize contract internal addresse(s) from params 89 | tokenAddress = _tokenAddress; 90 | treasuryAddress = _treasuryAddress; 91 | collectionAddress = _collectionAddress; 92 | tiersAddress = _tiersAddress; 93 | timestampAddress = _timestampAddress; 94 | 95 | // Initialize minimum/maximum allowed deposit limits 96 | minRequired = uint256(1000).mul(10e7); 97 | maxAllowed = uint256(250000).mul(10e7); 98 | } 99 | 100 | /** 101 | * @dev Deposit additional tokens to a reward address loyalty balance 102 | * @param _depositAmount of tokens to deposit into a reward address balance 103 | * @return bool indicating the success of the deposit operation (true == success) 104 | */ 105 | function depositLoyalty(uint _depositAmount) 106 | public 107 | whenNotPaused 108 | nonReentrant 109 | returns (bool) 110 | { 111 | // Validate calling address (msg.sender) 112 | require(msg.sender != address(0), 'Invalid {from}1'); 113 | // Validate specified value meets minimum requirements 114 | require(_depositAmount >= minRequired, 'Minimum required'); 115 | 116 | // Determine if caller has approved enough allowance for this deposit 117 | if(IERC20(tokenAddress).allowance(msg.sender, address(this)) < _depositAmount) { 118 | // No, rever informing that deposit amount exceeded allownce amount 119 | revert('Exceeds allowance'); 120 | } 121 | 122 | // Obtain a storage instsance of callers account record 123 | Account storage loyaltyAccount = accounts[msg.sender]; 124 | 125 | // Determine if there is an upper deposit cap 126 | if(maxAllowed > 0) { 127 | // Yes, determine if the deposit amount + current balance exceed max deposit cap 128 | if(loyaltyAccount._balance.add(_depositAmount) > maxAllowed || _depositAmount > maxAllowed) { 129 | // Yes, revert informing that the maximum deposit cap has been exceeded 130 | revert('Exceeds cap'); 131 | } 132 | 133 | } 134 | 135 | // Determine if the tier selected is enabled 136 | if(!ISparkleRewardTiers(tiersAddress).getEnabled(loyaltyAccount._tier)) { 137 | // No, then this tier cannot be selected 138 | revert('Invalid tier'); 139 | } 140 | 141 | // Determine of transfer from caller has succeeded 142 | if(IERC20(tokenAddress).transferFrom(msg.sender, address(this), _depositAmount)) { 143 | // Yes, thend determine if the specified address has a timestamp record 144 | if(ISparkleTimestamp(timestampAddress).hasTimestamp(msg.sender)) { 145 | // Yes, update callers account balance by deposit amount 146 | loyaltyAccount._balance = loyaltyAccount._balance.add(_depositAmount); 147 | // Reset the callers reward timestamp 148 | _resetTimestamp(msg.sender); 149 | // 150 | emit DepositLoyaltyEvent(msg.sender, _depositAmount, true); 151 | // Return success 152 | return true; 153 | } 154 | 155 | // Determine if a timestamp has been added for caller 156 | if(!ISparkleTimestamp(timestampAddress).addTimestamp(msg.sender)) { 157 | // No, revert indicating there was some kind of error 158 | revert('No timestamp created'); 159 | } 160 | 161 | // Prepare loyalty account record 162 | loyaltyAccount._address = address(msg.sender); 163 | loyaltyAccount._balance = _depositAmount; 164 | loyaltyAccount._joined = loyaltyAccount._joined.add(1); 165 | // Update global account counter 166 | totalActiveAccounts = totalActiveAccounts.add(1); 167 | // 168 | emit DepositLoyaltyEvent(msg.sender, _depositAmount, false); 169 | // Return success 170 | return true; 171 | } 172 | 173 | // Return failure 174 | return false; 175 | } 176 | 177 | /** 178 | * @dev Claim Sparkle Loyalty reward 179 | */ 180 | function claimLoyaltyReward() 181 | public 182 | whenNotPaused 183 | nonReentrant 184 | returns(bool) 185 | { 186 | // Validate calling address (msg.sender) 187 | require(msg.sender != address(0), 'Invalid {from}'); 188 | // Validate caller has a timestamp and it has matured 189 | require(ISparkleTimestamp(timestampAddress).hasTimestamp(msg.sender), 'No record'); 190 | require(ISparkleTimestamp(timestampAddress).isRewardReady(msg.sender), 'Not mature'); 191 | 192 | // Obtain the current state of the callers timestamp 193 | (uint256 timeRemaining, bool isReady, uint256 rewardDate) = ISparkleTimestamp(timestampAddress).getTimeRemaining(msg.sender); 194 | // Determine if the callers reward has matured 195 | if(isReady) { 196 | // Value not used but throw unused var warning (cleanup) 197 | rewardDate = 0; 198 | // Yes, then obtain a storage instance of callers account record 199 | Account storage loyaltyAccount = accounts[msg.sender]; 200 | // Obtain values required for caculations 201 | uint256 dayCount = (timeRemaining.div(ISparkleTimestamp(timestampAddress).getTimePeriod())).add(1); 202 | uint256 tokenBalance = loyaltyAccount._balance.add(loyaltyAccount._collected); 203 | uint256 rewardRate = ISparkleRewardTiers(tiersAddress).getRate(loyaltyAccount._tier); 204 | uint256 rewardTotal = baseRate.mul(tokenBalance).mul(rewardRate).mul(dayCount).div(10e7).div(10e7); 205 | // Increment collected by reward total 206 | loyaltyAccount._collected = loyaltyAccount._collected.add(rewardTotal); 207 | // Increment total number of times a reward has been claimed 208 | loyaltyAccount._claimed = loyaltyAccount._claimed.add(1); 209 | // Incrememtn total number of times rewards have been collected by all 210 | totalTimesClaimed = totalTimesClaimed.add(1); 211 | // Increment total number of tokens claimed 212 | totalTokensClaimed += rewardTotal; 213 | // Reset the callers timestamp record 214 | _resetTimestamp(msg.sender); 215 | // Emit event log to the block chain for future web3 use 216 | emit RewardClaimedEvent(msg.sender, rewardTotal); 217 | // Return success 218 | return true; 219 | } 220 | 221 | // Revert opposed to returning boolean (May or may not return a txreceipt) 222 | revert('Failed claim'); 223 | } 224 | 225 | /** 226 | * @dev Withdraw the current deposit balance + any earned loyalty rewards 227 | */ 228 | function withdrawLoyalty() 229 | public 230 | whenNotPaused 231 | nonReentrant 232 | { 233 | // Validate calling address (msg.sender) 234 | require(msg.sender != address(0), 'Invalid {from}'); 235 | // validate that caller has a loyalty timestamp 236 | require(ISparkleTimestamp(timestampAddress).hasTimestamp(msg.sender), 'No timestamp2'); 237 | 238 | // Determine if the account has been locked 239 | if(accounts[msg.sender]._isLocked) { 240 | // Yes, revert informing that this loyalty account has been locked 241 | revert('Locked'); 242 | } 243 | 244 | // Obtain values needed from account record before zeroing 245 | uint256 joinCount = accounts[msg.sender]._joined; 246 | uint256 collected = accounts[msg.sender]._collected; 247 | uint256 deposit = accounts[msg.sender]._balance; 248 | bool isLocked = accounts[msg.sender]._isLocked; 249 | // Zero out the callers account record 250 | Account storage account = accounts[msg.sender]; 251 | account._address = address(0x0); 252 | account._balance = 0x0; 253 | account._collected = 0x0; 254 | account._joined = joinCount; 255 | account._claimed = 0x0; 256 | account._tier = 0x0; 257 | // Preserve account lock even after withdraw (account always locked) 258 | account._isLocked = isLocked; 259 | // Decement the total number of active accounts 260 | totalActiveAccounts = totalActiveAccounts.sub(1); 261 | 262 | // Delete the callers timestamp record 263 | _deleteTimestamp(msg.sender); 264 | 265 | // Determine if transfer from treasury address is a success 266 | if(!IERC20(tokenAddress).transferFrom(treasuryAddress, msg.sender, collected)) { 267 | // No, revert indicating that the transfer and wisthdraw has failed 268 | revert('Withdraw failed'); 269 | } 270 | 271 | // Determine if transfer from contract address is a sucess 272 | if(!IERC20(tokenAddress).transfer(msg.sender, deposit)) { 273 | // No, revert indicating that the treansfer and withdraw has failed 274 | revert('Withdraw failed'); 275 | } 276 | 277 | // Emit event log to the block chain for future web3 use 278 | emit LoyaltyWithdrawnEvent(msg.sender, deposit.add(collected)); 279 | } 280 | 281 | function returnLoyaltyDeposit(address _rewardAddress) 282 | public 283 | whenNotPaused 284 | onlyOwner 285 | nonReentrant 286 | { 287 | // Validate calling address (msg.sender) 288 | require(msg.sender != address(0), 'Invalid {from}'); 289 | // validate that caller has a loyalty timestamp 290 | require(ISparkleTimestamp(timestampAddress).hasTimestamp(_rewardAddress), 'No timestamp2'); 291 | // Validate that reward address is locked 292 | require(accounts[_rewardAddress]._isLocked, 'Lock account first'); 293 | uint256 deposit = accounts[_rewardAddress]._balance; 294 | Account storage account = accounts[_rewardAddress]; 295 | account._balance = 0x0; 296 | // Determine if transfer from contract address is a sucess 297 | if(!IERC20(tokenAddress).transfer(_rewardAddress, deposit)) { 298 | // No, revert indicating that the treansfer and withdraw has failed 299 | revert('Withdraw failed'); 300 | } 301 | 302 | // Emit event log to the block chain for future web3 use 303 | emit LoyaltyDepositWithdrawnEvent(_rewardAddress, deposit); 304 | } 305 | 306 | function returnLoyaltyCollected(address _rewardAddress) 307 | public 308 | whenNotPaused 309 | onlyOwner 310 | nonReentrant 311 | { 312 | // Validate calling address (msg.sender) 313 | require(msg.sender != address(0), 'Invalid {from}'); 314 | // validate that caller has a loyalty timestamp 315 | require(ISparkleTimestamp(timestampAddress).hasTimestamp(_rewardAddress), 'No timestamp2b'); 316 | // Validate that reward address is locked 317 | require(accounts[_rewardAddress]._isLocked, 'Lock account first'); 318 | uint256 collected = accounts[_rewardAddress]._collected; 319 | Account storage account = accounts[_rewardAddress]; 320 | account._collected = 0x0; 321 | // Determine if transfer from treasury address is a success 322 | if(!IERC20(tokenAddress).transferFrom(treasuryAddress, _rewardAddress, collected)) { 323 | // No, revert indicating that the transfer and wisthdraw has failed 324 | revert('Withdraw failed'); 325 | } 326 | 327 | // Emit event log to the block chain for future web3 use 328 | emit LoyaltyCollectedWithdrawnEvent(_rewardAddress, collected); 329 | } 330 | 331 | function removeLoyaltyAccount(address _rewardAddress) 332 | public 333 | whenNotPaused 334 | onlyOwner 335 | nonReentrant 336 | { 337 | // Validate calling address (msg.sender) 338 | require(msg.sender != address(0), 'Invalid {from}'); 339 | // validate that caller has a loyalty timestamp 340 | require(ISparkleTimestamp(timestampAddress).hasTimestamp(_rewardAddress), 'No timestamp2b'); 341 | // Validate that reward address is locked 342 | require(accounts[_rewardAddress]._isLocked, 'Lock account first'); 343 | uint256 joinCount = accounts[_rewardAddress]._joined; 344 | Account storage account = accounts[_rewardAddress]; 345 | account._address = address(0x0); 346 | account._balance = 0x0; 347 | account._collected = 0x0; 348 | account._joined = joinCount; 349 | account._claimed = 0x0; 350 | account._tier = 0x0; 351 | account._isLocked = false; 352 | // Decement the total number of active accounts 353 | totalActiveAccounts = totalActiveAccounts.sub(1); 354 | 355 | // Delete the callers timestamp record 356 | _deleteTimestamp(_rewardAddress); 357 | 358 | emit LoyaltyAccountRemovedEvent(_rewardAddress); 359 | } 360 | 361 | /** 362 | * @dev Gets the locked status of the specified address 363 | * @param _loyaltyAddress of account 364 | * @return (bool) indicating locked status 365 | */ 366 | function isLocked(address _loyaltyAddress) 367 | public 368 | view 369 | whenNotPaused 370 | returns (bool) 371 | { 372 | return accounts[_loyaltyAddress]._isLocked; 373 | } 374 | 375 | function lockAccount(address _rewardAddress, bool _value) 376 | public 377 | onlyOwner 378 | whenNotPaused 379 | nonReentrant 380 | { 381 | // Validate calling address (msg.sender) 382 | require(msg.sender != address(0x0), 'Invalid {from}'); 383 | require(_rewardAddress != address(0x0), 'Invalid {reward}'); 384 | // Validate specified address has timestamp 385 | require(ISparkleTimestamp(timestampAddress).hasTimestamp(_rewardAddress), 'No timstamp'); 386 | // Set the specified address' locked status 387 | accounts[_rewardAddress]._isLocked = _value; 388 | // Emit event log to the block chain for future web3 use 389 | emit LockedAccountEvent(_rewardAddress, _value); 390 | } 391 | 392 | /** 393 | * @dev Gets the storage address value of the specified address 394 | * @param _loyaltyAddress of account 395 | * @return (address) indicating the address stored calls account record 396 | */ 397 | function getLoyaltyAddress(address _loyaltyAddress) 398 | public 399 | view 400 | whenNotPaused 401 | returns(address) 402 | { 403 | return accounts[_loyaltyAddress]._address; 404 | } 405 | 406 | /** 407 | * @dev Get the deposit balance value of specified address 408 | * @param _loyaltyAddress of account 409 | * @return (uint256) indicating the balance value 410 | */ 411 | function getDepositBalance(address _loyaltyAddress) 412 | public 413 | view 414 | whenNotPaused 415 | returns(uint256) 416 | { 417 | return accounts[_loyaltyAddress]._balance; 418 | } 419 | 420 | /** 421 | * @dev Get the tokens collected by the specified address 422 | * @param _loyaltyAddress of account 423 | * @return (uint256) indicating the tokens collected 424 | */ 425 | function getTokensCollected(address _loyaltyAddress) 426 | public 427 | view 428 | whenNotPaused 429 | returns(uint256) 430 | { 431 | return accounts[_loyaltyAddress]._collected; 432 | } 433 | 434 | /** 435 | * @dev Get the total balance (deposit + collected) of tokens 436 | * @param _loyaltyAddress of account 437 | * @return (uint256) indicating total balance 438 | */ 439 | function getTotalBalance(address _loyaltyAddress) 440 | public 441 | view 442 | whenNotPaused 443 | returns(uint256) 444 | { 445 | return accounts[_loyaltyAddress]._balance.add(accounts[_loyaltyAddress]._collected); 446 | } 447 | 448 | /** 449 | * @dev Get the times loyalty has been claimed 450 | * @param _loyaltyAddress of account 451 | * @return (uint256) indicating total time claimed 452 | */ 453 | function getTimesClaimed(address _loyaltyAddress) 454 | public 455 | view 456 | whenNotPaused 457 | returns(uint256) 458 | { 459 | return accounts[_loyaltyAddress]._claimed; 460 | } 461 | 462 | /** 463 | * @dev Get total number of times joined 464 | * @param _loyaltyAddress of account 465 | * @return (uint256) 466 | */ 467 | function getTimesJoined(address _loyaltyAddress) 468 | public 469 | view 470 | whenNotPaused 471 | returns(uint256) 472 | { 473 | return accounts[_loyaltyAddress]._joined; 474 | } 475 | 476 | /** 477 | * @dev Get time remaining before reward maturity 478 | * @param _loyaltyAddress of account 479 | * @return (uint256, bool) Indicating time remaining/past and boolean indicating maturity 480 | */ 481 | function getTimeRemaining(address _loyaltyAddress) 482 | public 483 | whenNotPaused 484 | returns (uint256, bool, uint256) 485 | { 486 | (uint256 remaining, bool status, uint256 deposit) = ISparkleTimestamp(timestampAddress).getTimeRemaining(_loyaltyAddress); 487 | return (remaining, status, deposit); 488 | } 489 | 490 | /** 491 | * @dev Withdraw any ether that has been sent directly to the contract 492 | * @param _loyaltyAddress of account 493 | * @return Total number of tokens that have been claimed by users 494 | * @notice Test(s) Not written 495 | */ 496 | function getRewardTier(address _loyaltyAddress) 497 | public 498 | view whenNotPaused 499 | returns(uint256) 500 | { 501 | return accounts[_loyaltyAddress]._tier; 502 | } 503 | 504 | /** 505 | * @dev Select reward tier for msg.sender 506 | * @param _tierSelected id of the reward tier interested in purchasing 507 | * @return (bool) indicating failure/success 508 | */ 509 | function selectRewardTier(uint256 _tierSelected) 510 | public 511 | payable 512 | whenNotPaused 513 | nonReentrant 514 | returns(bool) 515 | { 516 | // Validate calling address (msg.sender) 517 | require(msg.sender != address(0x0), 'Invalid {From}'); 518 | // Validate specified address has a timestamp 519 | require(accounts[msg.sender]._address == address(msg.sender), 'No timestamp3'); 520 | // Validate tier selection 521 | require(accounts[msg.sender]._tier != _tierSelected, 'Already selected'); 522 | // Validate that ether was sent with the call 523 | require(msg.value > 0, 'No ether'); 524 | 525 | // Determine if the specified rate is > than existing rate 526 | if(ISparkleRewardTiers(tiersAddress).getRate(accounts[msg.sender]._tier) >= ISparkleRewardTiers(tiersAddress).getRate(_tierSelected)) { 527 | // No, revert indicating failure 528 | revert('Invalid tier'); 529 | } 530 | 531 | // Determine if ether transfer for tier upgrade has completed successfully 532 | (bool success, ) = address(collectionAddress).call{value: ISparkleRewardTiers(tiersAddress).getPrice(_tierSelected), gas: gasToSendWithTX}(''); 533 | require(success, 'Rate unchanged'); 534 | 535 | // Update callers rate with the new selected rate 536 | accounts[msg.sender]._tier = _tierSelected; 537 | emit TierSelectedEvent(msg.sender, _tierSelected); 538 | // Return success 539 | return true; 540 | } 541 | 542 | function getRewardTiersAddress() 543 | public 544 | view 545 | whenNotPaused 546 | returns(address) 547 | { 548 | return tiersAddress; 549 | } 550 | 551 | /** 552 | * @dev Set tier collectionm address 553 | * @param _newAddress of new collection address 554 | * @notice Test(s) not written 555 | */ 556 | function setRewardTiersAddress(address _newAddress) 557 | public 558 | whenNotPaused 559 | onlyOwner 560 | nonReentrant 561 | { 562 | // Validate calling address (msg.sender) 563 | require(msg.sender != address(0x0), 'Invalid {From}'); 564 | // Validate specified address is valid 565 | require(_newAddress != address(0), 'Invalid {reward}'); 566 | // Set tier rewards contract address 567 | tiersAddress = _newAddress; 568 | emit TiersAddressChanged(_newAddress); 569 | } 570 | 571 | function getCollectionAddress() 572 | public 573 | view 574 | whenNotPaused 575 | returns(address) 576 | { 577 | return collectionAddress; 578 | } 579 | 580 | /** @notice Test(s) passed 581 | * @dev Set tier collectionm address 582 | * @param _newAddress of new collection address 583 | */ 584 | function setCollectionAddress(address _newAddress) 585 | public 586 | whenNotPaused 587 | onlyOwner 588 | nonReentrant 589 | { 590 | // Validate calling address (msg.sender) 591 | require(msg.sender != address(0x0), 'Invalid {From}'); 592 | // Validate specified address is valid 593 | require(_newAddress != address(0), 'Invalid {collection}'); 594 | // Set tier collection address 595 | collectionAddress = _newAddress; 596 | emit CollectionAddressChanged(_newAddress); 597 | } 598 | 599 | function getTreasuryAddress() 600 | public 601 | view 602 | whenNotPaused 603 | returns(address) 604 | { 605 | return treasuryAddress; 606 | } 607 | 608 | /** 609 | * @dev Set treasury address 610 | * @param _newAddress of the treasury address 611 | * @notice Test(s) passed 612 | */ 613 | function setTreasuryAddress(address _newAddress) 614 | public 615 | onlyOwner 616 | whenNotPaused 617 | nonReentrant 618 | { 619 | // Validate calling address (msg.sender) 620 | require(msg.sender != address(0), "Invalid {from}"); 621 | // Validate specified address 622 | require(_newAddress != address(0), "Invalid {treasury}"); 623 | // Set current treasury contract address 624 | treasuryAddress = _newAddress; 625 | emit TreasuryAddressChanged(_newAddress); 626 | } 627 | 628 | function getTimestampAddress() 629 | public 630 | view 631 | whenNotPaused 632 | returns(address) 633 | { 634 | return timestampAddress; 635 | } 636 | 637 | /** 638 | * @dev Set the timestamp address 639 | * @param _newAddress of timestamp address 640 | * @notice Test(s) passed 641 | */ 642 | function setTimestampAddress(address _newAddress) 643 | public 644 | onlyOwner 645 | whenNotPaused 646 | nonReentrant 647 | { 648 | // Validate calling address (msg.sender) 649 | require(msg.sender != address(0), "Invalid {from}"); 650 | // Set current timestamp contract address 651 | timestampAddress = _newAddress; 652 | emit TimestampAddressChanged(_newAddress); 653 | } 654 | 655 | function getTokenAddress() 656 | public 657 | view 658 | whenNotPaused 659 | returns(address) 660 | { 661 | return tokenAddress; 662 | } 663 | 664 | /** 665 | * @dev Set the loyalty token address 666 | * @param _newAddress of the new token address 667 | * @notice Test(s) passed 668 | */ 669 | function setTokenAddress(address _newAddress) 670 | public 671 | onlyOwner 672 | whenNotPaused 673 | nonReentrant 674 | { 675 | // Validate calling address (msg.sender) 676 | require(msg.sender != address(0), "Invalid {from}"); 677 | // Set current token contract address 678 | tokenAddress = _newAddress; 679 | emit TokenAddressChangedEvent(_newAddress); 680 | } 681 | 682 | function getSentGasAmount() 683 | public 684 | view 685 | whenNotPaused 686 | returns(uint256) 687 | { 688 | return gasToSendWithTX; 689 | } 690 | 691 | function setSentGasAmount(uint256 _amount) 692 | public 693 | onlyOwner 694 | whenNotPaused 695 | { 696 | // Validate calling address (msg.sender) 697 | require(msg.sender != address(0), 'Invalid {from}'); 698 | // Set the current minimum deposit allowed 699 | gasToSendWithTX = _amount; 700 | emit GasSentChanged(_amount); 701 | } 702 | 703 | function getBaseRate() 704 | public 705 | view 706 | whenNotPaused 707 | returns(uint256) 708 | { 709 | return baseRate; 710 | } 711 | 712 | function setBaseRate(uint256 _newRate) 713 | public 714 | onlyOwner 715 | whenNotPaused 716 | { 717 | // Validate calling address (msg.sender) 718 | require(msg.sender != address(0), 'Invalid {from}'); 719 | // Set the current minimum deposit allowed 720 | baseRate = _newRate; 721 | emit BaseRateChanged(_newRate); 722 | } 723 | 724 | /** 725 | * @dev Set the minimum Proof Of Loyalty amount allowed for deposit 726 | * @param _minProof amount for new minimum accepted loyalty reward deposit 727 | * @notice _minProof value is multiplied internally by 10e7. Do not multiply before calling! 728 | */ 729 | function setMinProof(uint256 _minProof) 730 | public 731 | onlyOwner 732 | whenNotPaused 733 | nonReentrant 734 | { 735 | // Validate calling address (msg.sender) 736 | require(msg.sender != address(0), 'Invalid {from}'); 737 | // Validate specified minimum is not lower than 1000 tokens 738 | require(_minProof >= 1000, 'Invalid amount'); 739 | // Set the current minimum deposit allowed 740 | minRequired = _minProof.mul(10e7); 741 | emit MinProofChanged(minRequired); 742 | } 743 | 744 | event MinProofChanged(uint256); 745 | /** 746 | * @dev Get the minimum Proof Of Loyalty amount allowed for deposit 747 | * @return Amount of tokens required for Proof Of Loyalty Rewards 748 | * @notice Test(s) passed 749 | */ 750 | function getMinProof() 751 | public 752 | view 753 | whenNotPaused 754 | returns(uint256) 755 | { 756 | // Return indicating minimum deposit allowed 757 | return minRequired; 758 | } 759 | 760 | /** 761 | * @dev Set the maximum Proof Of Loyalty amount allowed for deposit 762 | * @param _maxProof amount for new maximum loyalty reward deposit 763 | * @notice _maxProof value is multiplied internally by 10e7. Do not multiply before calling! 764 | * @notice Smallest maximum value is 1000 + _minProof amount. (Ex: If _minProof == 1000 then smallest _maxProof possible is 2000) 765 | */ 766 | function setMaxProof(uint256 _maxProof) 767 | public 768 | onlyOwner 769 | whenNotPaused 770 | nonReentrant 771 | { 772 | // Validate calling address (msg.sender) 773 | require(msg.sender != address(0), 'Invalid {from}'); 774 | require(_maxProof >= 2000, 'Invalid amount'); 775 | // Set allow maximum deposit 776 | maxAllowed = _maxProof.mul(10e7); 777 | } 778 | 779 | /** 780 | * @dev Get the maximum Proof Of Loyalty amount allowed for deposit 781 | * @return Maximum amount of tokens allowed for Proof Of Loyalty deposit 782 | * @notice Test(s) passed 783 | */ 784 | function getMaxProof() 785 | public 786 | view 787 | whenNotPaused 788 | returns(uint256) 789 | { 790 | // Return indicating current allowed maximum deposit 791 | return maxAllowed; 792 | } 793 | 794 | /** 795 | * @dev Get the total number of tokens claimed by all users 796 | * @return Total number of tokens that have been claimed by users 797 | * @notice Test(s) Not written 798 | */ 799 | function getTotalTokensClaimed() 800 | public 801 | view 802 | whenNotPaused 803 | returns(uint256) 804 | { 805 | // Return indicating total number of tokens that have been claimed by all 806 | return totalTokensClaimed; 807 | } 808 | 809 | /** 810 | * @dev Get total number of times rewards have been claimed for all users 811 | * @return Total number of times rewards have been claimed 812 | */ 813 | function getTotalTimesClaimed() 814 | public 815 | view 816 | whenNotPaused 817 | returns(uint256) 818 | { 819 | // Return indicating total number of tokens that have been claimed by all 820 | return totalTimesClaimed; 821 | } 822 | 823 | /** 824 | * @dev Withdraw any ether that has been sent directly to the contract 825 | */ 826 | function withdrawEth(address _toAddress) 827 | public 828 | onlyOwner 829 | whenNotPaused 830 | nonReentrant 831 | { 832 | // Validate calling address (msg.sender) 833 | require(msg.sender != address(0x0), 'Invalid {from}'); 834 | // Validate specified address 835 | require(_toAddress != address(0x0), 'Invalid {to}'); 836 | // Validate there is ether to withdraw 837 | require(address(this).balance > 0, 'No ether'); 838 | // Determine if ether transfer of stored ether has completed successfully 839 | // require(address(_toAddress).call.value(address(this).balance).gas(gasToSendWithTX)(), 'Withdraw failed'); 840 | (bool success, ) = address(_toAddress).call{value:address(this).balance, gas: gasToSendWithTX}(''); 841 | require(success, 'Withdraw failed'); 842 | } 843 | 844 | /** 845 | * @dev Withdraw any ether that has been sent directly to the contract 846 | * @param _toAddress to receive any stored token balance 847 | */ 848 | function withdrawTokens(address _toAddress) 849 | public 850 | onlyOwner 851 | whenNotPaused 852 | nonReentrant 853 | { 854 | // Validate calling address (msg.sender) 855 | require(msg.sender != address(0x0), 'Invalid {from}'); 856 | // Validate specified address 857 | require(_toAddress != address(0), "Invalid {to}"); 858 | // Validate there are tokens to withdraw 859 | uint256 balance = IERC20(tokenAddress).balanceOf(address(this)); 860 | require(balance != 0, "No tokens"); 861 | 862 | // Validate the transfer of tokens completed successfully 863 | if(IERC20(tokenAddress).transfer(_toAddress, balance)) { 864 | emit TokensWithdrawn(_toAddress, balance); 865 | } 866 | } 867 | 868 | /** 869 | * @dev Override loyalty account tier by contract owner 870 | * @param _loyaltyAccount loyalty account address to tier override 871 | * @param _tierSelected reward tier to override current tier value 872 | * @return (bool) indicating success status 873 | */ 874 | function overrideRewardTier(address _loyaltyAccount, uint256 _tierSelected) 875 | public 876 | whenNotPaused 877 | onlyOwner 878 | nonReentrant 879 | returns(bool) 880 | { 881 | // Validate calling address (msg.sender) 882 | require(msg.sender != address(0x0), 'Invalid {from}'); 883 | require(_loyaltyAccount != address(0x0), 'Invalid {account}'); 884 | // Validate specified address has a timestamp 885 | require(accounts[_loyaltyAccount]._address == address(_loyaltyAccount), 'No timestamp4'); 886 | // Update the specified loyalty address tier reward index 887 | accounts[_loyaltyAccount]._tier = _tierSelected; 888 | emit RewardTierChanged(_loyaltyAccount, _tierSelected); 889 | } 890 | 891 | /** 892 | * @dev Reset the specified loyalty account timestamp 893 | * @param _rewardAddress of the loyalty account to perfornm a reset 894 | */ 895 | function _resetTimestamp(address _rewardAddress) 896 | internal 897 | { 898 | // Validate calling address (msg.sender) 899 | require(msg.sender != address(0x0), 'Invalid {from}'); 900 | // Validate specified address 901 | require(_rewardAddress != address(0), "Invalid {reward}"); 902 | // Reset callers timestamp for specified address 903 | require(ISparkleTimestamp(timestampAddress).resetTimestamp(_rewardAddress), 'Reset failed'); 904 | emit ResetTimestampEvent(_rewardAddress); 905 | } 906 | 907 | /** 908 | * @dev Delete the specified loyalty account timestamp 909 | * @param _rewardAddress of the loyalty account to perfornm the delete 910 | */ 911 | function _deleteTimestamp(address _rewardAddress) 912 | internal 913 | { 914 | // Validate calling address (msg.sender) 915 | require(msg.sender != address(0x0), 'Invalid {from}16'); 916 | // Validate specified address 917 | require(_rewardAddress != address(0), "Invalid {reward}"); 918 | // Delete callers timestamp for specified address 919 | require(ISparkleTimestamp(timestampAddress).deleteTimestamp(_rewardAddress), 'Delete failed'); 920 | emit DeleteTimestampEvent(_rewardAddress); 921 | } 922 | 923 | /** 924 | * @dev Event signal: Treasury address updated 925 | */ 926 | event TreasuryAddressChanged(address); 927 | 928 | /** 929 | * @dev Event signal: Timestamp address updated 930 | */ 931 | event TimestampAddressChanged(address); 932 | 933 | /** 934 | * @dev Event signal: Token address updated 935 | */ 936 | event TokenAddressChangedEvent(address); 937 | 938 | /** 939 | * @dev Event signal: Timestamp reset 940 | */ 941 | event ResetTimestampEvent(address _rewardAddress); 942 | 943 | /** 944 | * @dev Event signal: Timestamp deleted 945 | */ 946 | event DeleteTimestampEvent(address _rewardAddress); 947 | 948 | /** 949 | * @dev Event signal: Loyalty deposited event 950 | */ 951 | event DepositLoyaltyEvent(address, uint256, bool); 952 | 953 | /** 954 | * @dev Event signal: Reward claimed successfully for address 955 | */ 956 | event RewardClaimedEvent(address, uint256); 957 | 958 | /** 959 | * @dev Event signal: Loyalty withdrawn 960 | */ 961 | event LoyaltyWithdrawnEvent(address, uint256); 962 | 963 | /** 964 | * @dev Event signal: Account locked/unlocked 965 | */ 966 | event LockedAccountEvent(address _rewardAddress, bool _locked); 967 | 968 | /** 969 | * @dev Event signal: Loyalty deposit balance withdrawn 970 | */ 971 | event LoyaltyDepositWithdrawnEvent(address, uint256); 972 | 973 | /** 974 | * @dev Event signal: Loyalty collected balance withdrawn 975 | */ 976 | event LoyaltyCollectedWithdrawnEvent(address, uint256); 977 | 978 | /** 979 | * @dev Event signal: Loyalty account removed 980 | */ 981 | event LoyaltyAccountRemovedEvent(address); 982 | 983 | /** 984 | * @dev Event signal: Gas sent with call.value amount updated 985 | */ 986 | event GasSentChanged(uint256); 987 | /** 988 | * @dev Event signal: Reward tiers address updated 989 | */ 990 | event TierSelectedEvent(address, uint256); 991 | 992 | /** 993 | * @dev Event signal: Reward tiers address updated 994 | */ 995 | event TiersAddressChanged(address); 996 | 997 | /** 998 | * @dev Event signal: Reward tier has been updated 999 | */ 1000 | event RewardTierChanged(address, uint256); 1001 | 1002 | /** 1003 | * @dev Event signal: Collection address updated 1004 | */ 1005 | event CollectionAddressChanged(address); 1006 | 1007 | /** 1008 | * @dev Event signal: All stored tokens have been removed 1009 | */ 1010 | event TokensWithdrawn(address, uint256); 1011 | 1012 | /** 1013 | * @dev Event signal: Apr base rate has been changed 1014 | */ 1015 | event BaseRateChanged(uint256); 1016 | } --------------------------------------------------------------------------------