├── .gitattributes ├── .gitignore ├── migrations ├── 1_initial_migration.js └── 2_deploy_dos_contracts.js ├── scripts ├── README.md ├── config_hecotest.js ├── config_heco.js ├── config_bsctest.js ├── guardian_bsc.js └── guardian_heco.js ├── package.json ├── contracts ├── Migrations.sol ├── examples │ ├── CoinbaseEthUsd.sol │ ├── AskMeAnything.sol │ └── SimpleDice.sol ├── interfaces │ └── StreamInterface.sol ├── mocks │ ├── BN256Mock.sol │ └── StringUtilsMock.sol ├── Ownable.sol ├── CoingeckoParserV2.sol ├── TestToken.sol ├── DOSAddressBridge.sol ├── lib │ ├── math.sol │ ├── BN256.sol │ ├── SafeMath.sol │ └── StringUtils.sol ├── CommitReveal.sol ├── MegaStream.sol ├── DOSOnChainSDK.sol ├── DOSOnChainSDK-bsc.sol ├── DOSOnChainSDK-heco.sol ├── DOSOnChainSDK-bsctest.sol ├── DOSOnChainSDK-hecotest.sol ├── StreamsManager.sol ├── ContractGateway.sol ├── DOSPayment.sol └── Stream.sol ├── README.md ├── LICENSE ├── truffle-config.js ├── test ├── bn256_test.js ├── stringutils_test.js └── dosproxy_test.js └── deployed.json /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package-lock.json 3 | build/ 4 | *.*swp 5 | -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | var Migrations = artifacts.require("./Migrations.sol"); 2 | 3 | module.exports = function(deployer) { 4 | deployer.deploy(Migrations); 5 | }; 6 | -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | ### Compile 2 | * `cd .. && npm install` 3 | * `truffle compile --all` 4 | 5 | ### Config 6 | * Change `httpProvider` for supported chains. 7 | * Change `MetaSource` for guardian node to check data changes and notify on-chain systems. 8 | 9 | ### Execute 10 | * `export PK=xxxxx` () 11 | * `pm2 start guardian.js` 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "contracts", 3 | "version": "1.0.0", 4 | "dependencies": { 5 | "@truffle/hdwallet-provider": "^1.0.42", 6 | "assert": "^2.0.0", 7 | "jsonpath": "^1.1.0", 8 | "truffle-assertions": "^0.9.2" 9 | }, 10 | "devDependencies": { 11 | "@openzeppelin/test-helpers": "^0.5.3", 12 | "bl": ">=1.2.3", 13 | "elliptic": ">=6.5.4", 14 | "lodash": ">=4.17.21", 15 | "minimist": ">=1.2.2", 16 | "node-fetch": ">=2.6.1", 17 | "path-parse": ">=1.0.7", 18 | "yargs-parser": ">=13.1.2" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >= 0.4.24; 2 | 3 | contract Migrations { 4 | address public owner; 5 | uint public last_completed_migration; 6 | 7 | constructor() public { 8 | owner = msg.sender; 9 | } 10 | 11 | modifier restricted() { 12 | if (msg.sender == owner) _; 13 | } 14 | 15 | function setCompleted(uint completed) public restricted { 16 | last_completed_migration = completed; 17 | } 18 | 19 | function upgrade(address new_address) public restricted { 20 | Migrations upgraded = Migrations(new_address); 21 | upgraded.setCompleted(last_completed_migration); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /scripts/config_hecotest.js: -------------------------------------------------------------------------------- 1 | const deployed = require('../deployed.json'); 2 | const stream = require('../build/contracts/Stream.json'); 3 | const manager = require('../build/contracts/StreamsManager.json'); 4 | const mega = require('../build/contracts/MegaStream.json'); 5 | 6 | module.exports = { 7 | httpProvider: 'https://http-testnet.hecochain.com', 8 | coingeckoMegaSource: 'https://api.coingecko.com/api/v3/simple/price?vs_currencies=usd&ids=bitcoin,dos-network,ethereum,huobi-token,polkadot', 9 | coingeckoMegaSelector: '$..usd', 10 | coingeckoStreamsManagerAddr: deployed.hecoTestnet.CoingeckoStreamsManager, 11 | managerABI: manager.abi, 12 | streamABI: stream.abi, 13 | megaStreamABI: mega.abi, 14 | triggerMaxGas: 800000, 15 | heartbeat: 60 * 1000, // 60 seconds 16 | }; 17 | -------------------------------------------------------------------------------- /scripts/config_heco.js: -------------------------------------------------------------------------------- 1 | const deployed = require('../deployed.json'); 2 | const stream = require('../build/contracts/Stream.json'); 3 | const manager = require('../build/contracts/StreamsManager.json'); 4 | const mega = require('../build/contracts/MegaStream.json'); 5 | 6 | module.exports = { 7 | httpProvider: 'https://http-mainnet.hecochain.com', 8 | coingeckoMegaSource: 'https://api.coingecko.com/api/v3/simple/price?vs_currencies=usd&ids=bitcoin,dos-network,ethereum,filecoin,huobi-pool-token,huobi-token,polkadot', 9 | coingeckoMegaSelector: '$..usd', 10 | coingeckoStreamsManagerAddr: deployed.heco.CoingeckoStreamsManager, 11 | managerABI: manager.abi, 12 | streamABI: stream.abi, 13 | megaStreamABI: mega.abi, 14 | triggerMaxGas: 800000, 15 | gasPriceGwei: 1.01, // Gwei 16 | heartbeat: 60 * 1000, // 60 seconds 17 | }; 18 | -------------------------------------------------------------------------------- /scripts/config_bsctest.js: -------------------------------------------------------------------------------- 1 | const deployed = require('../deployed.json'); 2 | const stream = require('../build/contracts/Stream.json'); 3 | const manager = require('../build/contracts/StreamsManager.json'); 4 | const mega = require('../build/contracts/MegaStream.json'); 5 | 6 | module.exports = { 7 | httpProvider: 'https://data-seed-prebsc-1-s1.binance.org:8545', 8 | coingeckoMegaSource: 'https://api.coingecko.com/api/v3/simple/price?vs_currencies=usd&ids=binancecoin,bitcoin,dos-network,ethereum,filecoin,polkadot', 9 | coingeckoMegaSelector: '$..usd', 10 | coingeckoStreamsManagerAddr: deployed.bscTestnet.CoingeckoStreamsManager, 11 | managerABI: manager.abi, 12 | streamABI: stream.abi, 13 | megaStreamABI: mega.abi, 14 | triggerMaxGas: 800000, 15 | gasPriceGwei: 10, // Gwei 16 | heartbeat: 60 * 1000, // 60 seconds 17 | }; 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Dev: 2 | - `$ git clone https://github.com/DOSNetwork/smart-contracts` 3 | - `$ npm install` 4 | - `$ npm -g install truffle` 5 | - `$ npm -g install ganache-cli` or installing its [graphic interface](https://truffleframework.com/ganache) 6 | - Required truffle/solcjs version: >= 0.5 7 | 8 | ### Compile: 9 | - `$ truffle compile` 10 | 11 | ### Deploy to local development network: 12 | - `$ ganache-cli` 13 | - `$ truffle migrate --reset` 14 | 15 | 16 | ### Deploy to rinkeby testnet without dryrun: 17 | - `$ truffle compile --all` 18 | - `$ truffle migrate --reset --network rinkeby --skip-dry-run` 19 | 20 | 21 | ### Deploy to mainnet without dryrun: 22 | - `$ truffle compile --all` 23 | - `$ truffle migrate --reset --network live --skip-dry-run` 24 | 25 | 26 | ### Test: 27 | - `$ ganache-cli -a 20`; // Config more than 10 test accounts 28 | - `$ truffle test` 29 | 30 | ### Deployed: 31 | See `deployed.json`. 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 DOS NETWORK 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /contracts/examples/CoinbaseEthUsd.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | import "../lib/StringUtils.sol"; 4 | import "../DOSOnChainSDK.sol"; 5 | 6 | // An example get latest ETH-USD price from Coinbase 7 | contract CoinbaseEthUsd is DOSOnChainSDK { 8 | using StringUtils for *; 9 | 10 | // Struct to hold parsed floating string "123.45" 11 | struct ethusd { 12 | uint integral; 13 | uint fractional; 14 | } 15 | uint queryId; 16 | string public price_str; 17 | ethusd public prices; 18 | 19 | event GetPrice(uint integral, uint fractional); 20 | 21 | constructor() public { 22 | // @dev: setup and then transfer DOS tokens into deployed contract 23 | // as oracle fees. 24 | // Unused fees can be reclaimed by calling DOSRefund() in the SDK. 25 | super.DOSSetup(); 26 | } 27 | 28 | function check() public { 29 | queryId = DOSQuery(30, "https://api.coinbase.com/v2/prices/ETH-USD/spot", "$.data.amount"); 30 | } 31 | 32 | function __callback__(uint id, bytes calldata result) external auth { 33 | require(queryId == id, "Unmatched response"); 34 | 35 | price_str = string(result); 36 | prices.integral = price_str.subStr(1).str2Uint(); 37 | uint delimit_idx = price_str.indexOf('.'); 38 | if (delimit_idx != result.length) { 39 | prices.fractional = price_str.subStr(delimit_idx + 1).str2Uint(); 40 | } 41 | emit GetPrice(prices.integral, prices.fractional); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /contracts/interfaces/StreamInterface.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | pragma experimental ABIEncoderV2; 3 | 4 | interface IStream { 5 | struct Observation { 6 | uint timestamp; 7 | uint price; 8 | } 9 | function description() external view returns (string memory); 10 | function decimal() external view returns (uint); 11 | function windowSize() external view returns (uint); 12 | function source() external view returns (string memory); 13 | function selector() external view returns (string memory); 14 | function deviation() external view returns (uint); 15 | function numPoints() external view returns(uint); 16 | function num24hPoints() external view returns(uint); 17 | function latestResult() external view returns (uint lastPrice, uint lastUpdatedTime); 18 | function result(uint idx) external view returns (uint price, uint timestamp); 19 | function results(uint startIdx, uint lastIdx) external view returns(Observation[] memory); 20 | function last24hResults() external view returns (Observation[] memory); 21 | function shouldUpdate(uint price) external view returns(bool); 22 | function megaUpdate(uint price) external returns(bool); 23 | function TWAP1Hour() external view returns (uint); 24 | function TWAP2Hour() external view returns (uint); 25 | function TWAP4Hour() external view returns (uint); 26 | function TWAP6Hour() external view returns (uint); 27 | function TWAP8Hour() external view returns (uint); 28 | function TWAP12Hour() external view returns (uint); 29 | function TWAP1Day() external view returns (uint); 30 | function TWAP1Week() external view returns (uint); 31 | } 32 | -------------------------------------------------------------------------------- /contracts/mocks/BN256Mock.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | import "../lib/BN256.sol"; 4 | 5 | // Exporting as public functions for javascript tests. 6 | contract BN256Mock { 7 | function P1() public pure returns (uint[2] memory) { 8 | return [ BN256.P1().x, BN256.P1().y ]; 9 | } 10 | 11 | function P2() public pure returns (uint[4] memory) { 12 | return [ 13 | BN256.P2().x[0], BN256.P2().x[1], 14 | BN256.P2().y[0], BN256.P2().y[1] 15 | ]; 16 | } 17 | 18 | function pointAdd(uint[2] memory p1, uint[2] memory p2) 19 | public 20 | returns (uint[2] memory) 21 | { 22 | BN256.G1Point memory sum = BN256.pointAdd(BN256.G1Point(p1[0], p1[1]), 23 | BN256.G1Point(p2[0], p2[1])); 24 | return [sum.x, sum.y]; 25 | } 26 | 27 | function scalarMul(uint[2] memory p1, uint s) 28 | public 29 | returns (uint[2] memory) 30 | { 31 | BN256.G1Point memory prod = 32 | BN256.scalarMul(BN256.G1Point(p1[0], p1[1]), s); 33 | return [prod.x, prod.y]; 34 | } 35 | 36 | function negate(uint[2] memory p) public pure returns (uint[2] memory) { 37 | BN256.G1Point memory neg = BN256.negate(BN256.G1Point(p[0], p[1])); 38 | return [neg.x, neg.y]; 39 | } 40 | 41 | function hashToG1(bytes memory data) public returns (uint[2] memory) { 42 | BN256.G1Point memory p1 = BN256.hashToG1(data); 43 | return [p1.x, p1.y]; 44 | } 45 | 46 | function pairingCheck(uint[2][] memory p1, uint[4][] memory p2) 47 | public 48 | returns (bool) 49 | { 50 | require(p1.length == p2.length); 51 | 52 | BN256.G1Point[] memory b_p1 = new BN256.G1Point[](p1.length); 53 | BN256.G2Point[] memory b_p2 = new BN256.G2Point[](p1.length); 54 | for (uint i = 0; i < p1.length; i++) { 55 | b_p1[i] = BN256.G1Point(p1[i][0], p1[i][1]); 56 | b_p2[i] = BN256.G2Point([p2[i][0], p2[i][1]], 57 | [p2[i][2], p2[i][3]]); 58 | } 59 | return BN256.pairingCheck(b_p1, b_p2); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /contracts/Ownable.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | /** 4 | * @title Ownable 5 | * @dev The Ownable contract has an owner address, and provides basic authorization control 6 | * functions, this simplifies the implementation of "user permissions". 7 | */ 8 | contract Ownable { 9 | address private _owner; 10 | 11 | event OwnershipRenounced(address indexed previousOwner); 12 | event OwnershipTransferred( 13 | address indexed previousOwner, 14 | address indexed newOwner 15 | ); 16 | 17 | /** 18 | * @dev The Ownable constructor sets the original `owner` of the contract to the sender 19 | * account. 20 | */ 21 | constructor() public { 22 | _owner = msg.sender; 23 | } 24 | 25 | /** 26 | * @return the address of the owner. 27 | */ 28 | function owner() public view returns(address) { 29 | return _owner; 30 | } 31 | 32 | /** 33 | * @dev Throws if called by any account other than the owner. 34 | */ 35 | modifier onlyOwner() { 36 | require(isOwner()); 37 | _; 38 | } 39 | 40 | /** 41 | * @return true if `msg.sender` is the owner of the contract. 42 | */ 43 | function isOwner() public view returns(bool) { 44 | return msg.sender == _owner; 45 | } 46 | 47 | /** 48 | * @dev Allows the current owner to relinquish control of the contract. 49 | * @notice Renouncing to ownership will leave the contract without an owner. 50 | * It will not be possible to call the functions with the `onlyOwner` 51 | * modifier anymore. 52 | */ 53 | function renounceOwnership() public onlyOwner { 54 | emit OwnershipRenounced(_owner); 55 | _owner = address(0); 56 | } 57 | 58 | /** 59 | * @dev Allows the current owner to transfer control of the contract to a newOwner. 60 | * @param newOwner The address to transfer ownership to. 61 | */ 62 | function transferOwnership(address newOwner) public onlyOwner { 63 | _transferOwnership(newOwner); 64 | } 65 | 66 | /** 67 | * @dev Transfers control of the contract to a newOwner. 68 | * @param newOwner The address to transfer ownership to. 69 | */ 70 | function _transferOwnership(address newOwner) internal { 71 | require(newOwner != address(0)); 72 | emit OwnershipTransferred(_owner, newOwner); 73 | _owner = newOwner; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /truffle-config.js: -------------------------------------------------------------------------------- 1 | const HDWalletProvider = require("@truffle/hdwallet-provider"); 2 | const assert = require('assert'); 3 | const infura_token = "8e609c76fce442f8a1735fbea9999747"; 4 | const mainnetInfura = `https://mainnet.infura.io/v3/${infura_token}`; 5 | const rinkebyInfura = `https://rinkeby.infura.io/v3/${infura_token}`; 6 | const okchainTest = 'https://exchaintest.okexcn.com'; 7 | const hecoTestnet = 'https://http-testnet.hecochain.com'; 8 | const heco = 'https://http-mainnet.hecochain.com'; 9 | const bscTestnet = 'https://data-seed-prebsc-1-s2.binance.org:8545/'; 10 | const bsc = 'https://bsc-dataseed.binance.org/'; 11 | const pk = process.env.PK; 12 | 13 | module.exports = { 14 | networks: { 15 | development: { 16 | host: "localhost", 17 | port: 8545, 18 | network_id: "*" // Match any network id 19 | }, 20 | rinkeby: { 21 | provider: () => new HDWalletProvider(pk, rinkebyInfura), 22 | network_id: 4, 23 | gas: 8000000 24 | }, 25 | okchainTest: { 26 | provider: () => new HDWalletProvider(pk, okchainTest), 27 | network_id: 65, 28 | gas: 8000000, 29 | gasPrice: 1e9 // 1 Gwei 30 | }, 31 | hecoTestnet: { 32 | provider: () => new HDWalletProvider(pk, hecoTestnet), 33 | network_id: 256, 34 | gas: 8000000, 35 | gasPrice: 1e9 // 1 Gwei 36 | }, 37 | heco: { 38 | provider: () => new HDWalletProvider(pk, heco), 39 | network_id: 128, 40 | gas: 8000000, 41 | gasPrice: 2e9 // 2 Gwei 42 | }, 43 | bscTestnet: { 44 | provider: () => new HDWalletProvider(pk, bscTestnet), 45 | networkCheckTimeout: 100000, 46 | timeoutBlocks: 2000, 47 | network_id: 97, 48 | gas: 8000000, 49 | gasPrice: 10e9 // 10 Gwei 50 | }, 51 | bsc: { 52 | provider: () => new HDWalletProvider(pk, bsc), 53 | networkCheckTimeout: 100000, 54 | timeoutBlocks: 2000, 55 | network_id: 56, 56 | gas: 8000000, 57 | gasPrice: 6e9 // 6 Gwei 58 | }, 59 | live: { 60 | provider: () => new HDWalletProvider(pk, mainetInfura), 61 | network_id: 1, 62 | gas: 8000000 63 | }, 64 | }, 65 | compilers: { 66 | solc: { 67 | version: "0.5.17", 68 | settings: { 69 | optimizer: { 70 | enabled: true, 71 | runs: 200 72 | }, 73 | } 74 | } 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /contracts/CoingeckoParserV2.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | import "./lib/SafeMath.sol"; 4 | import "./lib/StringUtils.sol"; 5 | 6 | // A simple parser to parse coingecko api data. 7 | // Coingecko data api: https://www.coingecko.com/en/api. 8 | // e.g. source: "https://api.coingecko.com/api/v3/simple/price?vs_currencies=usd&ids=ethereum,bitcoin,polkadot,huobi-token" 9 | // e.g. selector: "$..usd" 10 | // e.g. Return: "[48766,1524.21,13.99,34.64]" 11 | contract CoingeckoParserV2 { 12 | using StringUtils for *; 13 | using SafeMath for uint; 14 | 15 | string public constant description = "Coingecko API Data Parser V2"; 16 | uint private constant ten = 10; 17 | 18 | // e.g.: 19 | // floatStr2Uint("123.4567", 0) => 123 20 | // floatStr2Uint("123.4567", 2) => 12345 21 | // floatStr2Uint("123.4567", 8) => 12345670000 22 | function floatStr2Uint(string memory raw, uint decimal) public pure returns(uint) { 23 | uint integral = raw.str2Uint(); 24 | uint fractional = 0; 25 | uint dotIdx = raw.indexOf('.'); 26 | uint fracIdx = dotIdx + 1; 27 | if (dotIdx != bytes(raw).length && fracIdx < bytes(raw).length) { 28 | string memory fracStr = raw.subStr(fracIdx, decimal); 29 | fractional = fracStr.str2Uint(); 30 | if (decimal > bytes(fracStr).length) { 31 | fractional = fractional.mul(ten.pow(decimal - bytes(fracStr).length)); 32 | } 33 | } 34 | return integral.mul(ten.pow(decimal)).add(fractional); 35 | } 36 | 37 | function floatBytes2UintArray(bytes memory raw, uint decimal) public pure returns (uint[] memory) { 38 | uint len = raw.length; 39 | string[] memory s_arr = string(raw.subStr(1, len - 2)).split(','); 40 | uint[] memory uint_arr = new uint[](s_arr.length); 41 | for (uint i = 0; i < s_arr.length; i++) { 42 | uint_arr[i] = floatStr2Uint(s_arr[i], decimal); 43 | } 44 | return uint_arr; 45 | } 46 | 47 | // e.g.: 48 | // floatStrs2UintArray("[48766,1524.21,13.99,34.64]", 8) => [4876600000000,152421000000,1399000000,3464000000] 49 | // floatStrs2UintArray("[12.34]", 8) => [1234000000] 50 | function floatStrs2UintArray(string memory raw, uint decimal) public pure returns (uint[] memory) { 51 | return floatBytes2UintArray(bytes(raw), decimal); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /contracts/TestToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.5.0 <0.6.0; 2 | 3 | import "./lib/SafeMath.sol"; 4 | 5 | contract TestToken { 6 | using SafeMath for uint256; 7 | 8 | string public constant name = "TestToken"; 9 | string public constant symbol = "TTK"; 10 | uint8 public constant decimals = 18; 11 | uint256 private constant MAX_SUPPLY = 1e9 * 1e18; // 1 billion total supply 12 | uint256 private _supply = MAX_SUPPLY; 13 | mapping(address => uint256) balances; 14 | mapping(address => mapping (address => uint256)) allowed; 15 | 16 | event Approval(address indexed tokenOwner, address indexed spender, uint wad); 17 | event Transfer(address indexed from, address indexed to, uint wad); 18 | 19 | constructor() public { 20 | balances[msg.sender] = _supply; 21 | } 22 | 23 | function totalSupply() public view returns (uint256) { 24 | return _supply; 25 | } 26 | 27 | function balanceOf(address tokenOwner) public view returns (uint) { 28 | return balances[tokenOwner]; 29 | } 30 | 31 | function transfer(address receiver, uint wad) public returns (bool) { 32 | require(wad <= balances[msg.sender]); 33 | balances[msg.sender] = balances[msg.sender].sub(wad); 34 | balances[receiver] = balances[receiver].add(wad); 35 | emit Transfer(msg.sender, receiver, wad); 36 | return true; 37 | } 38 | 39 | function approve(address delegate, uint wad) public returns (bool) { 40 | allowed[msg.sender][delegate] = wad; 41 | emit Approval(msg.sender, delegate, wad); 42 | return true; 43 | } 44 | 45 | function allowance(address owner, address delegate) public view returns (uint) { 46 | return allowed[owner][delegate]; 47 | } 48 | 49 | function transferFrom(address owner, address buyer, uint wad) public returns (bool) { 50 | require(wad <= balances[owner]); 51 | require(wad <= allowed[owner][msg.sender]); 52 | if (owner != msg.sender && allowed[owner][msg.sender] != uint(-1)) { 53 | require(allowed[owner][msg.sender] >= wad, "token-insufficient-approval"); 54 | allowed[owner][msg.sender] = allowed[owner][msg.sender].sub(wad); 55 | } 56 | balances[owner] = balances[owner].sub(wad); 57 | balances[buyer] = balances[buyer].add(wad); 58 | emit Transfer(owner, buyer, wad); 59 | return true; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /contracts/DOSAddressBridge.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | import "./Ownable.sol"; 4 | 5 | contract DOSAddressBridge is Ownable { 6 | // Deployed DOSProxy contract address. 7 | address private _proxyAddress; 8 | // Deployed CommitReveal contract address. 9 | address private _commitrevealAddress; 10 | // Deployed DOSPayment contract address. 11 | address private _paymentAddress; 12 | // Deployed StakingGateway contract address. 13 | address private _stakingAddress; 14 | // BootStrap node lists. 15 | string private _bootStrapUrl; 16 | 17 | event ProxyAddressUpdated(address previousProxy, address newProxy); 18 | event CommitRevealAddressUpdated(address previousAddr, address newAddr); 19 | event PaymentAddressUpdated(address previousPayment, address newPayment); 20 | event StakingAddressUpdated(address previousStaking, address newStaking); 21 | event BootStrapUrlUpdated(string previousURL, string newURL); 22 | 23 | function getProxyAddress() public view returns (address) { 24 | return _proxyAddress; 25 | } 26 | 27 | function setProxyAddress(address newAddr) public onlyOwner { 28 | emit ProxyAddressUpdated(_proxyAddress, newAddr); 29 | _proxyAddress = newAddr; 30 | } 31 | 32 | function getCommitRevealAddress() public view returns (address) { 33 | return _commitrevealAddress; 34 | } 35 | 36 | function setCommitRevealAddress(address newAddr) public onlyOwner { 37 | emit CommitRevealAddressUpdated(_commitrevealAddress, newAddr); 38 | _commitrevealAddress = newAddr; 39 | } 40 | 41 | function getPaymentAddress() public view returns (address) { 42 | return _paymentAddress; 43 | } 44 | 45 | function setPaymentAddress(address newAddr) public onlyOwner { 46 | emit PaymentAddressUpdated(_paymentAddress, newAddr); 47 | _paymentAddress = newAddr; 48 | } 49 | 50 | function getStakingAddress() public view returns (address) { 51 | return _stakingAddress; 52 | } 53 | 54 | function setStakingAddress(address newAddr) public onlyOwner { 55 | emit StakingAddressUpdated(_stakingAddress, newAddr); 56 | _stakingAddress = newAddr; 57 | } 58 | 59 | function getBootStrapUrl() public view returns (string memory) { 60 | return _bootStrapUrl; 61 | } 62 | 63 | function setBootStrapUrl(string memory url) public onlyOwner { 64 | emit BootStrapUrlUpdated(_bootStrapUrl, url); 65 | _bootStrapUrl = url; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /contracts/mocks/StringUtilsMock.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | import "../lib/StringUtils.sol"; 4 | 5 | contract StringUtilsMock { 6 | function returnUINT256MAX() public pure returns(uint) { 7 | return ~uint(0); 8 | } 9 | 10 | function createByte() public pure returns(byte) { 11 | return '6'; 12 | } 13 | 14 | function byte2Uint(byte b) public pure returns(uint8) { 15 | return StringUtils.byte2Uint(b); 16 | } 17 | 18 | function hexByte2Uint(byte b) public pure returns(uint8) { 19 | return StringUtils.hexByte2Uint(b); 20 | } 21 | 22 | function str2Uint(string memory a) public pure returns(uint) { 23 | return StringUtils.str2Uint(a); 24 | } 25 | 26 | function hexStr2Uint(string memory a) public pure returns(uint) { 27 | return StringUtils.hexStr2Uint(a); 28 | } 29 | 30 | function str2Addr(string memory a) public pure returns(address) { 31 | return StringUtils.str2Addr(a); 32 | } 33 | 34 | function uint2HexStr(uint x) public pure returns(string memory) { 35 | return StringUtils.uint2HexStr(x); 36 | } 37 | 38 | function uint2Str(uint x) public pure returns(string memory) { 39 | return StringUtils.uint2Str(x); 40 | } 41 | 42 | function addr2Str(string memory a) public pure returns(string memory) { 43 | address x = StringUtils.str2Addr(a); 44 | return StringUtils.addr2Str(x); 45 | } 46 | 47 | function bytesConcat(bytes memory a, bytes memory b) public pure returns(bytes memory) { 48 | return StringUtils.bytesConcat(a,b); 49 | } 50 | 51 | function strConcat(string memory a, string memory b) public pure returns(string memory) { 52 | return StringUtils.strConcat(a,b); 53 | } 54 | 55 | function strCompare(string memory a, string memory b) public pure returns(int) { 56 | return StringUtils.strCompare(a, b); 57 | } 58 | 59 | function strEqual(string memory a, string memory b) public pure returns(bool) { 60 | return StringUtils.strEqual(a, b); 61 | } 62 | 63 | function indexOf(string memory haystack, string memory needle) public pure returns(uint) { 64 | return StringUtils.indexOf(haystack, needle); 65 | } 66 | 67 | function subStr(string memory a, uint start, uint len) public pure returns(string memory) { 68 | return StringUtils.subStr(a, start, len); 69 | } 70 | 71 | function subStr1(string memory a, uint start) public pure returns(string memory) { 72 | return StringUtils.subStr(a, start); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /contracts/lib/math.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.5.0 <0.6.0; 2 | 3 | library DSMath { 4 | function add(uint x, uint y) internal pure returns (uint z) { 5 | require((z = x + y) >= x, "ds-math-add-overflow"); 6 | } 7 | function sub(uint x, uint y) internal pure returns (uint z) { 8 | require((z = x - y) <= x, "ds-math-sub-underflow"); 9 | } 10 | function mul(uint x, uint y) internal pure returns (uint z) { 11 | require(y == 0 || (z = x * y) / y == x, "ds-math-mul-overflow"); 12 | } 13 | function div(uint x, uint y) internal pure returns (uint z) { 14 | // assert(y > 0); // Solidity automatically throws when dividing by 0 15 | z = x / y; 16 | // assert(x == y * z + x % y); // There is no case in which this doesn't hold 17 | } 18 | 19 | function min(uint x, uint y) internal pure returns (uint z) { 20 | return x <= y ? x : y; 21 | } 22 | function max(uint x, uint y) internal pure returns (uint z) { 23 | return x >= y ? x : y; 24 | } 25 | function imin(int x, int y) internal pure returns (int z) { 26 | return x <= y ? x : y; 27 | } 28 | function imax(int x, int y) internal pure returns (int z) { 29 | return x >= y ? x : y; 30 | } 31 | 32 | uint constant WAD = 10 ** 18; 33 | uint constant RAY = 10 ** 27; 34 | 35 | function wmul(uint x, uint y) internal pure returns (uint z) { 36 | z = add(mul(x, y), WAD / 2) / WAD; 37 | } 38 | function rmul(uint x, uint y) internal pure returns (uint z) { 39 | z = add(mul(x, y), RAY / 2) / RAY; 40 | } 41 | function wdiv(uint x, uint y) internal pure returns (uint z) { 42 | z = add(mul(x, WAD), y / 2) / y; 43 | } 44 | function rdiv(uint x, uint y) internal pure returns (uint z) { 45 | z = add(mul(x, RAY), y / 2) / y; 46 | } 47 | 48 | // This famous algorithm is called "exponentiation by squaring" 49 | // and calculates x^n with x as fixed-point and n as regular unsigned. 50 | // 51 | // It's O(log n), instead of O(n) for naive repeated multiplication. 52 | // 53 | // These facts are why it works: 54 | // 55 | // If n is even, then x^n = (x^2)^(n/2). 56 | // If n is odd, then x^n = x * x^(n-1), 57 | // and applying the equation for even x gives 58 | // x^n = x * (x^2)^((n-1) / 2). 59 | // 60 | // Also, EVM division is flooring and 61 | // floor[(n-1) / 2] = floor[n / 2]. 62 | // 63 | function rpow(uint x, uint n) internal pure returns (uint z) { 64 | z = n % 2 != 0 ? x : RAY; 65 | 66 | for (n /= 2; n != 0; n /= 2) { 67 | x = rmul(x, x); 68 | 69 | if (n % 2 != 0) { 70 | z = rmul(z, x); 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /test/bn256_test.js: -------------------------------------------------------------------------------- 1 | const BN256Mock = artifacts.require("BN256Mock"); 2 | 3 | contract("BN256 Test", async (accounts) => { 4 | let bn256; 5 | 6 | before(async () => { 7 | bn256 = await BN256Mock.new(); 8 | }) 9 | 10 | it("Test scalar multiplication", async () => { 11 | let p1 = await bn256.P1.call(); 12 | let p2 = await bn256.scalarMul.call(p1, 2); 13 | let prod1 = await bn256.scalarMul.call(p2, 3); 14 | let prod2 = await bn256.scalarMul.call(p1, 6); 15 | 16 | assert.equal(prod1[0].toString(10), prod2[0].toString(10), 17 | "After multiplication, x coordinate should equal"); 18 | assert.equal(prod1[1].toString(10), prod2[1].toString(10), 19 | "After multiplication, y coordinate should equal"); 20 | }); 21 | 22 | it("Test point addition", async () => { 23 | let p1 = await bn256.P1.call(); 24 | let pr = await bn256.scalarMul.call( 25 | p1, (Math.floor(Math.random() * Number.MAX_SAFE_INTEGER) + 1) ); 26 | 27 | let sum1 = await bn256.pointAdd.call(p1, pr); 28 | let sum2 = await bn256.pointAdd.call(pr, p1); 29 | 30 | 31 | assert.equal(sum1[0].toString(10), sum2[0].toString(10), 32 | "After addition, x coordinate value equals"); 33 | assert.equal(sum1[1].toString(10), sum2[1].toString(10), 34 | "After addition, y coordinate value equals"); 35 | }); 36 | 37 | it("Test negate", async () => { 38 | let p1 = await bn256.P1.call(); 39 | let pr = await bn256.scalarMul.call( 40 | p1, (Math.floor(Math.random() * Number.MAX_SAFE_INTEGER) + 1) ); 41 | let pr_n = await bn256.negate.call(pr); 42 | let sum = await bn256.pointAdd.call(pr, pr_n); 43 | 44 | assert.equal(sum[0].toNumber(), 0, "Pr + -Pr == 0"); 45 | assert.equal(sum[1].toNumber(), 0, "Pr + -Pr == 0"); 46 | }) 47 | 48 | it("Test basic pairing", async () => { 49 | let p1 = await bn256.P1.call(); 50 | let p1_n = await bn256.negate.call(p1); 51 | let p2 = await bn256.P2.call(); 52 | let pass = await bn256.pairingCheck.call([p1, p1_n], [p2, p2]); 53 | 54 | assert(pass, "Basic pairing check e({p1, p2}, {-p1, p2}) should be true"); 55 | }); 56 | 57 | it("Test complex pairing check", async () => { 58 | // Generated secret key / public key pair. 59 | let SK = web3.utils.toBN('0x250ebf796264728de1dc24d208c4cec4f813b1bcc2bb647ac8cf66206568db03'); 60 | let PK = [ 61 | web3.utils.toBN('0x25d7caf90ac28ba3cd8a96aff5c5bf004fc16d9bdcc2cead069e70f783397e5b'), 62 | web3.utils.toBN('0x04ef63f195409b451179767b06673758e621d9db71a058231623d1cb2e594460'), 63 | web3.utils.toBN('0x15729e3589dcb871cd46eb6774388aad867521dc07d1e0c0d9c99f444f93ca53'), 64 | web3.utils.toBN('0x15db87d74b02df70d62f7f8afe5811ade35ca08bdb2308b4153624083fcf580e'), 65 | ]; 66 | 67 | let msg = web3.utils.asciiToHex("test random bytes"); 68 | let hashed_msg = await bn256.hashToG1.call(msg); 69 | let sig = await bn256.scalarMul.call(hashed_msg, SK); 70 | let sig_n = await bn256.negate.call(sig); 71 | let G2 = await bn256.P2.call(); 72 | 73 | let pass = await bn256.pairingCheck.call([sig_n, hashed_msg], [G2, PK]); 74 | 75 | assert(pass, "Pairing check e({HM, PublicKey}, {-Sig, G2}) should be true"); 76 | }); 77 | 78 | }) 79 | -------------------------------------------------------------------------------- /contracts/examples/AskMeAnything.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | import "../DOSOnChainSDK.sol"; 4 | 5 | // A user contract asks anything from off-chain world through a url. 6 | contract AskMeAnything is DOSOnChainSDK { 7 | string public response; 8 | uint public random; 9 | // query_id -> valid_status 10 | mapping(uint => bool) private _valid; 11 | bool public repeatedCall = false; 12 | // Default timeout in seconds: Two blocks. 13 | uint public timeout = 14 * 2; 14 | string public lastQueriedUrl; 15 | string public lastQueriedSelector; 16 | uint public lastRequestedRandom; 17 | uint8 public lastQueryInternalSerial; 18 | 19 | event SetTimeout(uint previousTimeout, uint newTimeout); 20 | event QueryResponseReady(uint queryId, string result); 21 | event RequestSent(address indexed msgSender, uint8 internalSerial, bool succ, uint requestId); 22 | event RandomReady(uint requestId, uint generatedRandom); 23 | 24 | constructor() public { 25 | // @dev: setup and then transfer DOS tokens into deployed contract 26 | // as oracle fees. 27 | // Unused fees can be reclaimed by calling DOSRefund() in the SDK. 28 | super.DOSSetup(); 29 | } 30 | 31 | function setQueryMode(bool newMode) public onlyOwner { 32 | repeatedCall = newMode; 33 | } 34 | 35 | function setTimeout(uint newTimeout) public onlyOwner { 36 | emit SetTimeout(timeout, newTimeout); 37 | timeout = newTimeout; 38 | } 39 | 40 | // Ask me anything (AMA) off-chain through an api/url. 41 | function AMA(uint8 internalSerial, string memory url, string memory selector) public { 42 | lastQueriedUrl = url; 43 | lastQueriedSelector = selector; 44 | lastQueryInternalSerial = internalSerial; 45 | uint id = DOSQuery(timeout, url, selector); 46 | if (id != 0x0) { 47 | _valid[id] = true; 48 | emit RequestSent(msg.sender, internalSerial, true, id); 49 | } else { 50 | revert("Invalid query id."); 51 | } 52 | } 53 | 54 | // User-defined callback function handling query response. 55 | function __callback__(uint queryId, bytes calldata result) external auth { 56 | require(_valid[queryId], "Response with invalid request id!"); 57 | response = string(result); 58 | emit QueryResponseReady(queryId, response); 59 | delete _valid[queryId]; 60 | 61 | if (repeatedCall) { 62 | AMA(lastQueryInternalSerial, lastQueriedUrl, lastQueriedSelector); 63 | } 64 | } 65 | 66 | function requestSafeRandom(uint8 internalSerial) public { 67 | lastRequestedRandom = random; 68 | uint requestId = DOSRandom(now); 69 | _valid[requestId] = true; 70 | emit RequestSent(msg.sender, internalSerial, true, requestId); 71 | } 72 | 73 | // User-defined callback function handling newly generated secure 74 | // random number. 75 | function __callback__(uint requestId, uint generatedRandom) external auth { 76 | require(_valid[requestId], "Response with invalid request id!"); 77 | random = generatedRandom; 78 | emit RandomReady(requestId, generatedRandom); 79 | delete _valid[requestId]; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /contracts/lib/BN256.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | library BN256 { 4 | struct G1Point { 5 | uint x; 6 | uint y; 7 | } 8 | 9 | struct G2Point { 10 | uint[2] x; 11 | uint[2] y; 12 | } 13 | 14 | function P1() internal pure returns (G1Point memory) { 15 | return G1Point(1, 2); 16 | } 17 | 18 | function P2() internal pure returns (G2Point memory) { 19 | return G2Point( 20 | [11559732032986387107991004021392285783925812861821192530917403151452391805634, 21 | 10857046999023057135944570762232829481370756359578518086990519993285655852781], 22 | 23 | [4082367875863433681332203403145435568316851327593401208105741076214120093531, 24 | 8495653923123431417604973247489272438418190587263600148770280649306958101930] 25 | ); 26 | } 27 | 28 | function pointAdd(G1Point memory p1, G1Point memory p2) internal returns (G1Point memory r) { 29 | uint[4] memory input; 30 | input[0] = p1.x; 31 | input[1] = p1.y; 32 | input[2] = p2.x; 33 | input[3] = p2.y; 34 | assembly { 35 | if iszero(call(sub(gas, 2000), 0x6, 0, input, 0x80, r, 0x40)) { 36 | revert(0, 0) 37 | } 38 | } 39 | } 40 | 41 | function scalarMul(G1Point memory p, uint s) internal returns (G1Point memory r) { 42 | uint[3] memory input; 43 | input[0] = p.x; 44 | input[1] = p.y; 45 | input[2] = s; 46 | assembly { 47 | if iszero(call(sub(gas, 2000), 0x7, 0, input, 0x60, r, 0x40)) { 48 | revert(0, 0) 49 | } 50 | } 51 | } 52 | 53 | function negate(G1Point memory p) internal pure returns (G1Point memory) { 54 | if (p.x == 0 && p.y == 0) { 55 | return p; 56 | } 57 | uint q = 21888242871839275222246405745257275088696311157297823662689037894645226208583; 58 | return G1Point(p.x, q - p.y % q); 59 | } 60 | 61 | function hashToG1(bytes memory data) internal returns (G1Point memory) { 62 | uint256 h = uint256(keccak256(data)); 63 | return scalarMul(P1(), h); 64 | } 65 | 66 | function G2Equal(G2Point memory p1, G2Point memory p2) internal pure returns (bool) { 67 | return p1.x[0] == p2.x[0] && p1.x[1] == p2.x[1] && p1.y[0] == p2.y[0] && p1.y[1] == p2.y[1]; 68 | } 69 | 70 | // @return the result of computing the pairing check 71 | // check passes if e(p1[0], p2[0]) * .... * e(p1[n], p2[n]) == 1 72 | // E.g. pairing([p1, p1.negate()], [p2, p2]) should return true. 73 | function pairingCheck(G1Point[] memory p1, G2Point[] memory p2) internal returns (bool) { 74 | require(p1.length == p2.length); 75 | uint elements = p1.length; 76 | uint inputSize = elements * 6; 77 | uint[] memory input = new uint[](inputSize); 78 | 79 | for (uint i = 0; i < elements; i++) 80 | { 81 | input[i * 6 + 0] = p1[i].x; 82 | input[i * 6 + 1] = p1[i].y; 83 | input[i * 6 + 2] = p2[i].x[0]; 84 | input[i * 6 + 3] = p2[i].x[1]; 85 | input[i * 6 + 4] = p2[i].y[0]; 86 | input[i * 6 + 5] = p2[i].y[1]; 87 | } 88 | 89 | uint[1] memory out; 90 | bool success; 91 | assembly { 92 | success := call( 93 | sub(gas, 2000), 94 | 0x8, 95 | 0, 96 | add(input, 0x20), 97 | mul(inputSize, 0x20), 98 | out, 0x20 99 | ) 100 | } 101 | return success && (out[0] != 0); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /contracts/examples/SimpleDice.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | import "../DOSOnChainSDK.sol"; 4 | 5 | contract SimpleDice is DOSOnChainSDK { 6 | address payable public devAddress; 7 | uint public devContributed = 0; 8 | // 1% winning payout goes to developer account 9 | uint public developCut = 1; 10 | // precise to 4 digits after decimal point. 11 | uint public decimal = 4; 12 | // gameId => gameInfo 13 | mapping(uint => DiceInfo) public games; 14 | 15 | struct DiceInfo { 16 | uint rollUnder; // betted number, player wins if random < rollUnder 17 | uint amountBet; // amount in wei 18 | address payable player; // better address 19 | } 20 | 21 | event ReceivedBet( 22 | uint gameId, 23 | uint rollUnder, 24 | uint weiBetted, 25 | address better 26 | ); 27 | event PlayerWin(uint gameId, uint generated, uint betted, uint amountWin); 28 | event PlayerLose(uint gameId, uint generated, uint betted); 29 | 30 | modifier onlyDev { 31 | require(msg.sender == devAddress); 32 | _; 33 | } 34 | 35 | constructor() public { 36 | // @dev: setup and then transfer DOS tokens into deployed contract 37 | // as oracle fees. 38 | // Unused fees can be reclaimed by calling DOSRefund() in the SDK. 39 | super.DOSSetup(); 40 | 41 | // Convert address to payable address. 42 | devAddress = address(uint160(owner())); 43 | } 44 | 45 | function min(uint a, uint b) internal pure returns(uint) { 46 | return a < b ? a : b; 47 | } 48 | // Only receive bankroll funding from developer. 49 | function() external payable onlyDev { 50 | devContributed += msg.value; 51 | } 52 | // Only developer can withdraw the amount up to what he has contributed. 53 | function devWithdrawal() public onlyDev { 54 | uint withdrawalAmount = min(address(this).balance, devContributed); 55 | devContributed = 0; 56 | devAddress.transfer(withdrawalAmount); 57 | } 58 | 59 | // 100 / (rollUnder - 1) * (1 - 0.01) => 99 / (rollUnder - 1) 60 | // Not using SafeMath as this function cannot overflow anyway. 61 | function computeWinPayout(uint rollUnder) public view returns(uint) { 62 | return 99 * (10 ** decimal) / (rollUnder - 1); 63 | } 64 | 65 | // 100 / (rollUnder - 1) * 0.01 66 | function computeDeveloperCut(uint rollUnder) public view returns(uint) { 67 | return 10 ** decimal / (rollUnder - 1); 68 | } 69 | 70 | function play(uint rollUnder) public payable { 71 | // winChance within [1%, 95%] 72 | require(rollUnder >= 2 && rollUnder <= 96, "rollUnder should be in 2~96"); 73 | // Make sure contract has enough balance to cover payouts before game. 74 | // Not using SafeMath as I'm not expecting this demo contract's 75 | // balance to be very large. 76 | require(address(this).balance * (10 ** decimal) >= msg.value * computeWinPayout(rollUnder), 77 | "Game contract doesn't have enough balance, decrease rollUnder"); 78 | 79 | // Request a safe, unmanipulatable random number from DOS Network with 80 | // optional seed. 81 | uint gameId = DOSRandom(now); 82 | 83 | games[gameId] = DiceInfo(rollUnder, msg.value, msg.sender); 84 | // Emit event to notify Dapp frontend 85 | emit ReceivedBet(gameId, rollUnder, msg.value, msg.sender); 86 | } 87 | 88 | function __callback__(uint requestId, uint generatedRandom) external auth { 89 | address payable player = games[requestId].player; 90 | require(player != address(0x0)); 91 | 92 | uint gen_rnd = generatedRandom % 100 + 1; 93 | uint rollUnder = games[requestId].rollUnder; 94 | uint betted = games[requestId].amountBet; 95 | delete games[requestId]; 96 | 97 | if (gen_rnd < rollUnder) { 98 | // Player wins 99 | uint payout = betted * computeWinPayout(rollUnder) / (10 ** decimal); 100 | uint devPayout = betted * computeDeveloperCut(rollUnder) / (10 ** decimal); 101 | 102 | emit PlayerWin(requestId, gen_rnd, rollUnder, payout); 103 | player.transfer(payout); 104 | devAddress.transfer(devPayout); 105 | } else { 106 | // Lose 107 | emit PlayerLose(requestId, gen_rnd, rollUnder); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /contracts/CommitReveal.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | contract DOSAddressBridgeI { 4 | function getProxyAddress() public view returns(address); 5 | } 6 | 7 | contract CommitReveal { 8 | struct Participant { 9 | uint secret; 10 | bytes32 commitment; 11 | bool revealed; 12 | } 13 | 14 | struct Campaign { 15 | uint startBlock; 16 | uint commitDuration; // in blocks 17 | uint revealDuration; // in blocks 18 | uint revealThreshold; 19 | uint commitNum; 20 | uint revealNum; 21 | uint generatedRandom; 22 | mapping(address => Participant) participants; 23 | mapping(bytes32 => bool) commitments; 24 | } 25 | 26 | Campaign[] public campaigns; 27 | DOSAddressBridgeI public addressBridge; 28 | 29 | modifier checkCommit(uint _cid, bytes32 _commitment) { 30 | Campaign storage c = campaigns[_cid]; 31 | require(_cid != 0 && 32 | block.number >= c.startBlock && 33 | block.number < c.startBlock + c.commitDuration, 34 | "not-in-commit"); 35 | require(_commitment != "", "empty-commitment"); 36 | require(!c.commitments[_commitment], "duplicate-commitment"); 37 | _; 38 | } 39 | modifier checkReveal(uint _cid) { 40 | Campaign storage c = campaigns[_cid]; 41 | require(_cid != 0 && 42 | block.number >= c.startBlock + c.commitDuration && 43 | block.number < c.startBlock + c.commitDuration + c.revealDuration, 44 | "not-in-reveal"); 45 | _; 46 | } 47 | modifier checkFinish(uint _cid) { 48 | Campaign storage c = campaigns[_cid]; 49 | require(_cid != 0 && 50 | block.number >= c.startBlock + c.commitDuration + c.revealDuration, 51 | "commit-reveal-not-finished"); 52 | _; 53 | } 54 | modifier onlyFromProxy() { 55 | require(msg.sender == addressBridge.getProxyAddress(), "not-from-dos-proxy"); 56 | _; 57 | } 58 | 59 | event LogStartCommitReveal(uint cid, uint startBlock, uint commitDuration, uint revealDuration, uint revealThreshold); 60 | event LogCommit(uint cid, address from, bytes32 commitment); 61 | event LogReveal(uint cid, address from, uint secret); 62 | event LogRandom(uint cid, uint random); 63 | event LogRandomFailure(uint cid, uint commitNum, uint revealNum, uint revealThreshold); 64 | 65 | constructor(address _bridgeAddr) public { 66 | // campaigns[0] is not used. 67 | campaigns.length++; 68 | addressBridge = DOSAddressBridgeI(_bridgeAddr); 69 | } 70 | 71 | // Returns new campaignId. 72 | function startCommitReveal( 73 | uint _startBlock, 74 | uint _commitDuration, 75 | uint _revealDuration, 76 | uint _revealThreshold 77 | ) 78 | public 79 | onlyFromProxy 80 | returns(uint) 81 | { 82 | uint newCid = campaigns.length; 83 | campaigns.push(Campaign(_startBlock, _commitDuration, _revealDuration, _revealThreshold, 0, 0, 0)); 84 | emit LogStartCommitReveal(newCid, _startBlock, _commitDuration, _revealDuration, _revealThreshold); 85 | return newCid; 86 | } 87 | 88 | function commit(uint _cid, bytes32 _secretHash) public checkCommit(_cid, _secretHash) { 89 | Campaign storage c = campaigns[_cid]; 90 | c.commitments[_secretHash] = true; 91 | c.participants[msg.sender] = Participant(0, _secretHash, false); 92 | c.commitNum++; 93 | emit LogCommit(_cid, msg.sender, _secretHash); 94 | } 95 | 96 | function reveal(uint _cid, uint _secret) public checkReveal(_cid) { 97 | Campaign storage c = campaigns[_cid]; 98 | Participant storage p = c.participants[msg.sender]; 99 | require(!p.revealed && keccak256(abi.encodePacked(_secret)) == p.commitment, 100 | "revealed-secret-not-match-commitment"); 101 | p.secret = _secret; 102 | p.revealed = true; 103 | c.revealNum++; 104 | c.generatedRandom ^= _secret; 105 | emit LogReveal(_cid, msg.sender, _secret); 106 | } 107 | 108 | // Return value of 0 representing invalid random output. 109 | function getRandom(uint _cid) public checkFinish(_cid) returns (uint) { 110 | Campaign storage c = campaigns[_cid]; 111 | if (c.revealNum >= c.revealThreshold) { 112 | emit LogRandom(_cid, c.generatedRandom); 113 | return c.generatedRandom; 114 | } else{ 115 | emit LogRandomFailure(_cid, c.commitNum, c.revealNum, c.revealThreshold); 116 | return 0; 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /contracts/MegaStream.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | import "./DOSOnChainSDK.sol"; 4 | 5 | contract IParser { 6 | function floatBytes2UintArray(bytes memory raw, uint decimal) public view returns(uint[] memory); 7 | } 8 | 9 | contract IStreamsManager { 10 | function megaUpdate(uint[] calldata data) external returns(bool); 11 | } 12 | 13 | contract MegaStream is DOSOnChainSDK { 14 | uint public windowSize = 1200; // 20 minutes 15 | // Number of decimals the reported price data use. 16 | uint public decimal; 17 | uint public lastTime; 18 | string public megaDescription; 19 | string public megaSource; 20 | string public megaSelector; 21 | // Data parser, may be configured along with data source change 22 | address public parser; 23 | address public streamsManager; 24 | // Stream data is either updated once per windowSize or the deviation requirement is met, whichever comes first. 25 | // Anyone can trigger an update on windowSize expiration, but only governance approved ones can be deviation updater to get rid of sybil attacks. 26 | mapping(address => bool) private deviationGuardian; 27 | mapping(uint => bool) private _valid; 28 | 29 | event ParamsUpdated( 30 | string oldDescription, string newDescription, 31 | string oldSource, string newSource, 32 | string oldSelector, string newSelector, 33 | uint oldDecimal, uint newDecimal 34 | ); 35 | event WindowUpdated(uint oldWindow, uint newWindow); 36 | event ParserUpdated(address oldParser, address newParser); 37 | event ManagerUpdated(address oldParser, address newParser); 38 | event DataUpdated(uint timestamp, uint price); 39 | event PulledTrigger(address trigger, uint qId); 40 | event BulletCaught(uint qId); 41 | event AddGuardian(address guardian); 42 | event RemoveGuardian(address guardian); 43 | 44 | modifier isContract(address addr) { 45 | uint codeSize = 0; 46 | assembly { 47 | codeSize := extcodesize(addr) 48 | } 49 | require(codeSize > 0, "not-smart-contract"); 50 | _; 51 | } 52 | 53 | constructor( 54 | string memory _description, 55 | string memory _source, 56 | string memory _selector, 57 | uint _decimal, 58 | address _parser 59 | ) 60 | public 61 | isContract(_parser) 62 | { 63 | // @dev: setup and then transfer DOS tokens into deployed contract 64 | // as oracle fees. 65 | // Unused fees can be reclaimed by calling DOSRefund() function of SDK contract. 66 | super.DOSSetup(); 67 | megaDescription = _description; 68 | megaSource = _source; 69 | megaSelector = _selector; 70 | decimal = _decimal; 71 | parser = _parser; 72 | emit ParamsUpdated("", _description, "", _source, "", _selector, 0, _decimal); 73 | emit ParserUpdated(address(0), _parser); 74 | } 75 | 76 | function strEqual(string memory a, string memory b) private pure returns (bool) { 77 | return keccak256(abi.encodePacked(a)) == keccak256(abi.encodePacked(b)); 78 | } 79 | 80 | function updateWindowSize(uint newWindow) public onlyOwner { 81 | emit WindowUpdated(windowSize, newWindow); 82 | windowSize = newWindow; 83 | } 84 | function updateParams(string memory _description, string memory _source, string memory _selector, uint _decimal) public onlyOwner { 85 | emit ParamsUpdated( 86 | megaDescription, _description, 87 | megaSource, _source, 88 | megaSelector, _selector, 89 | decimal, _decimal 90 | ); 91 | if (!strEqual(megaDescription, _description)) megaDescription = _description; 92 | if (!strEqual(megaSource, _source)) megaSource = _source; 93 | if (!strEqual(megaSelector, _selector)) megaSelector = _selector; 94 | if (decimal != _decimal) decimal = _decimal; 95 | } 96 | function updateParser(address newParser) public onlyOwner isContract(newParser) { 97 | emit ParserUpdated(parser, newParser); 98 | parser = newParser; 99 | } 100 | function updateManager(address _manager) public onlyOwner isContract(_manager) { 101 | emit ManagerUpdated(streamsManager, _manager); 102 | streamsManager = _manager; 103 | } 104 | function addGuardian(address guardian) public onlyOwner { 105 | if (!deviationGuardian[guardian]) { 106 | deviationGuardian[guardian] = true; 107 | emit AddGuardian(guardian); 108 | } 109 | } 110 | function removeGuardian(address guardian) public onlyOwner { 111 | if (deviationGuardian[guardian]) { 112 | delete deviationGuardian[guardian]; 113 | emit RemoveGuardian(guardian); 114 | } 115 | } 116 | 117 | function pullTrigger() public { 118 | if(lastTime + windowSize >= block.timestamp && !deviationGuardian[msg.sender]) return; 119 | 120 | uint id = DOSQuery(30, megaSource, megaSelector); 121 | if (id != 0) { 122 | _valid[id] = true; 123 | emit PulledTrigger(msg.sender, id); 124 | } 125 | } 126 | 127 | function __callback__(uint id, bytes calldata result) external auth { 128 | require(_valid[id], "invalid-request-id"); 129 | uint[] memory prices = IParser(parser).floatBytes2UintArray(result, decimal); 130 | if (IStreamsManager(streamsManager).megaUpdate(prices)) emit BulletCaught(id); 131 | delete _valid[id]; 132 | lastTime = block.timestamp; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /contracts/DOSOnChainSDK.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | import "./Ownable.sol"; 4 | 5 | contract IProxy { 6 | function query(address, uint, string memory, string memory) public returns (uint); 7 | function requestRandom(address, uint) public returns (uint); 8 | } 9 | 10 | contract IPayment { 11 | function setPaymentMethod(address payer, address tokenAddr) public; 12 | function defaultTokenAddr() public returns(address); 13 | } 14 | 15 | contract IAddressBridge { 16 | function getProxyAddress() public view returns (address); 17 | function getPaymentAddress() public view returns (address); 18 | } 19 | 20 | contract IERC20 { 21 | function balanceOf(address who) public view returns (uint); 22 | function transfer(address to, uint value) public returns (bool); 23 | function approve(address spender, uint value) public returns (bool); 24 | } 25 | 26 | contract DOSOnChainSDK is Ownable { 27 | IProxy dosProxy; 28 | IAddressBridge dosAddrBridge = IAddressBridge(0x98A0E7026778840Aacd28B9c03137D32e06F5ff1); 29 | 30 | modifier resolveAddress { 31 | address proxyAddr = dosAddrBridge.getProxyAddress(); 32 | if (address(dosProxy) != proxyAddr) { 33 | dosProxy = IProxy(proxyAddr); 34 | } 35 | _; 36 | } 37 | 38 | modifier auth { 39 | // Filter out malicious __callback__ caller. 40 | require(msg.sender == dosAddrBridge.getProxyAddress(), "Unauthenticated response"); 41 | _; 42 | } 43 | 44 | // @dev: call setup function first and transfer DOS tokens into deployed contract as oracle fees. 45 | function DOSSetup() public onlyOwner { 46 | address paymentAddr = dosAddrBridge.getPaymentAddress(); 47 | address defaultToken = IPayment(dosAddrBridge.getPaymentAddress()).defaultTokenAddr(); 48 | IERC20(defaultToken).approve(paymentAddr, uint(-1)); 49 | IPayment(dosAddrBridge.getPaymentAddress()).setPaymentMethod(address(this), defaultToken); 50 | } 51 | 52 | // @dev: refund all unused fees to caller. 53 | function DOSRefund() public onlyOwner { 54 | address token = IPayment(dosAddrBridge.getPaymentAddress()).defaultTokenAddr(); 55 | uint amount = IERC20(token).balanceOf(address(this)); 56 | IERC20(token).transfer(msg.sender, amount); 57 | } 58 | 59 | // @dev: Call this function to get a unique queryId to differentiate 60 | // parallel requests. A return value of 0x0 stands for error and a 61 | // related event would be emitted. 62 | // @timeout: Estimated timeout in seconds specified by caller; e.g. 15. 63 | // Response is not guaranteed if processing time exceeds this. 64 | // @dataSource: Data source destination specified by caller. 65 | // E.g.: 'https://api.coinbase.com/v2/prices/ETH-USD/spot' 66 | // @selector: A selector expression provided by caller to filter out 67 | // specific data fields out of the raw response. The response 68 | // data format (json, xml/html, and more) is identified from 69 | // the selector expression. 70 | // E.g. Use "$.data.amount" to extract "194.22" out. 71 | // { 72 | // "data":{ 73 | // "base":"ETH", 74 | // "currency":"USD", 75 | // "amount":"194.22" 76 | // } 77 | // } 78 | // Check below documentation for details. 79 | // (https://dosnetwork.github.io/docs/#/contents/blockchains/ethereum?id=selector). 80 | function DOSQuery(uint timeout, string memory dataSource, string memory selector) 81 | internal 82 | resolveAddress 83 | returns (uint) 84 | { 85 | return dosProxy.query(address(this), timeout, dataSource, selector); 86 | } 87 | 88 | // @dev: Must override __callback__ to process a corresponding response. A 89 | // user-defined event could be added to notify the Dapp frontend that 90 | // the response is ready. 91 | // @queryId: A unique queryId returned by DOSQuery() for callers to 92 | // differentiate parallel responses. 93 | // @result: Response for the specified queryId. 94 | function __callback__(uint queryId, bytes calldata result) external { 95 | // To be overridden in the caller contract. 96 | } 97 | 98 | // @dev: Call this function to request either a fast but insecure random 99 | // number or a safe and secure random number delivered back 100 | // asynchronously through the __callback__ function. 101 | // Depending on the mode, the return value would be a random number 102 | // (for fast mode) or a requestId (for safe mode). 103 | // @seed: Optional random seed provided by caller. 104 | function DOSRandom(uint seed) 105 | internal 106 | resolveAddress 107 | returns (uint) 108 | { 109 | return dosProxy.requestRandom(address(this), seed); 110 | } 111 | 112 | // @dev: Must override __callback__ to process a corresponding random 113 | // number. A user-defined event could be added to notify the Dapp 114 | // frontend that a new secure random number is generated. 115 | // @requestId: A unique requestId returned by DOSRandom() for requester to 116 | // differentiate random numbers generated concurrently. 117 | // @generatedRandom: Generated secure random number for the specific 118 | // requestId. 119 | function __callback__(uint requestId, uint generatedRandom) external auth { 120 | // To be overridden in the caller contract. 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /contracts/DOSOnChainSDK-bsc.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | import "./Ownable.sol"; 4 | 5 | contract IProxy { 6 | function query(address, uint, string memory, string memory) public returns (uint); 7 | function requestRandom(address, uint) public returns (uint); 8 | } 9 | 10 | contract IPayment { 11 | function setPaymentMethod(address payer, address tokenAddr) public; 12 | function defaultTokenAddr() public returns(address); 13 | } 14 | 15 | contract IAddressBridge { 16 | function getProxyAddress() public view returns (address); 17 | function getPaymentAddress() public view returns (address); 18 | } 19 | 20 | contract IERC20 { 21 | function balanceOf(address who) public view returns (uint); 22 | function transfer(address to, uint value) public returns (bool); 23 | function approve(address spender, uint value) public returns (bool); 24 | } 25 | 26 | contract DOSOnChainSDK is Ownable { 27 | IProxy dosProxy; 28 | IAddressBridge dosAddrBridge = IAddressBridge(0x70157cf10404170EEc183043354D0a886Fa51d73); 29 | 30 | modifier resolveAddress { 31 | address proxyAddr = dosAddrBridge.getProxyAddress(); 32 | if (address(dosProxy) != proxyAddr) { 33 | dosProxy = IProxy(proxyAddr); 34 | } 35 | _; 36 | } 37 | 38 | modifier auth { 39 | // Filter out malicious __callback__ caller. 40 | require(msg.sender == dosAddrBridge.getProxyAddress(), "Unauthenticated response"); 41 | _; 42 | } 43 | 44 | // @dev: call setup function first and transfer DOS tokens into deployed contract as oracle fees. 45 | function DOSSetup() public onlyOwner { 46 | address paymentAddr = dosAddrBridge.getPaymentAddress(); 47 | address defaultToken = IPayment(dosAddrBridge.getPaymentAddress()).defaultTokenAddr(); 48 | IERC20(defaultToken).approve(paymentAddr, uint(-1)); 49 | IPayment(dosAddrBridge.getPaymentAddress()).setPaymentMethod(address(this), defaultToken); 50 | } 51 | 52 | // @dev: refund all unused fees to caller. 53 | function DOSRefund() public onlyOwner { 54 | address token = IPayment(dosAddrBridge.getPaymentAddress()).defaultTokenAddr(); 55 | uint amount = IERC20(token).balanceOf(address(this)); 56 | IERC20(token).transfer(msg.sender, amount); 57 | } 58 | 59 | // @dev: Call this function to get a unique queryId to differentiate 60 | // parallel requests. A return value of 0x0 stands for error and a 61 | // related event would be emitted. 62 | // @timeout: Estimated timeout in seconds specified by caller; e.g. 15. 63 | // Response is not guaranteed if processing time exceeds this. 64 | // @dataSource: Data source destination specified by caller. 65 | // E.g.: 'https://api.coinbase.com/v2/prices/ETH-USD/spot' 66 | // @selector: A selector expression provided by caller to filter out 67 | // specific data fields out of the raw response. The response 68 | // data format (json, xml/html, and more) is identified from 69 | // the selector expression. 70 | // E.g. Use "$.data.amount" to extract "194.22" out. 71 | // { 72 | // "data":{ 73 | // "base":"ETH", 74 | // "currency":"USD", 75 | // "amount":"194.22" 76 | // } 77 | // } 78 | // Check below documentation for details. 79 | // (https://dosnetwork.github.io/docs/#/contents/blockchains/ethereum?id=selector). 80 | function DOSQuery(uint timeout, string memory dataSource, string memory selector) 81 | internal 82 | resolveAddress 83 | returns (uint) 84 | { 85 | return dosProxy.query(address(this), timeout, dataSource, selector); 86 | } 87 | 88 | // @dev: Must override __callback__ to process a corresponding response. A 89 | // user-defined event could be added to notify the Dapp frontend that 90 | // the response is ready. 91 | // @queryId: A unique queryId returned by DOSQuery() for callers to 92 | // differentiate parallel responses. 93 | // @result: Response for the specified queryId. 94 | function __callback__(uint queryId, bytes calldata result) external { 95 | // To be overridden in the caller contract. 96 | } 97 | 98 | // @dev: Call this function to request either a fast but insecure random 99 | // number or a safe and secure random number delivered back 100 | // asynchronously through the __callback__ function. 101 | // Depending on the mode, the return value would be a random number 102 | // (for fast mode) or a requestId (for safe mode). 103 | // @seed: Optional random seed provided by caller. 104 | function DOSRandom(uint seed) 105 | internal 106 | resolveAddress 107 | returns (uint) 108 | { 109 | return dosProxy.requestRandom(address(this), seed); 110 | } 111 | 112 | // @dev: Must override __callback__ to process a corresponding random 113 | // number. A user-defined event could be added to notify the Dapp 114 | // frontend that a new secure random number is generated. 115 | // @requestId: A unique requestId returned by DOSRandom() for requester to 116 | // differentiate random numbers generated concurrently. 117 | // @generatedRandom: Generated secure random number for the specific 118 | // requestId. 119 | function __callback__(uint requestId, uint generatedRandom) external auth { 120 | // To be overridden in the caller contract. 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /contracts/DOSOnChainSDK-heco.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | import "./Ownable.sol"; 4 | 5 | contract IProxy { 6 | function query(address, uint, string memory, string memory) public returns (uint); 7 | function requestRandom(address, uint) public returns (uint); 8 | } 9 | 10 | contract IPayment { 11 | function setPaymentMethod(address payer, address tokenAddr) public; 12 | function defaultTokenAddr() public returns(address); 13 | } 14 | 15 | contract IAddressBridge { 16 | function getProxyAddress() public view returns (address); 17 | function getPaymentAddress() public view returns (address); 18 | } 19 | 20 | contract IERC20 { 21 | function balanceOf(address who) public view returns (uint); 22 | function transfer(address to, uint value) public returns (bool); 23 | function approve(address spender, uint value) public returns (bool); 24 | } 25 | 26 | contract DOSOnChainSDK is Ownable { 27 | IProxy dosProxy; 28 | IAddressBridge dosAddrBridge = IAddressBridge(0x9Ee7F642d3955ecf17D7223705DCb285dbA679fc); 29 | 30 | modifier resolveAddress { 31 | address proxyAddr = dosAddrBridge.getProxyAddress(); 32 | if (address(dosProxy) != proxyAddr) { 33 | dosProxy = IProxy(proxyAddr); 34 | } 35 | _; 36 | } 37 | 38 | modifier auth { 39 | // Filter out malicious __callback__ caller. 40 | require(msg.sender == dosAddrBridge.getProxyAddress(), "Unauthenticated response"); 41 | _; 42 | } 43 | 44 | // @dev: call setup function first and transfer DOS tokens into deployed contract as oracle fees. 45 | function DOSSetup() public onlyOwner { 46 | address paymentAddr = dosAddrBridge.getPaymentAddress(); 47 | address defaultToken = IPayment(dosAddrBridge.getPaymentAddress()).defaultTokenAddr(); 48 | IERC20(defaultToken).approve(paymentAddr, uint(-1)); 49 | IPayment(dosAddrBridge.getPaymentAddress()).setPaymentMethod(address(this), defaultToken); 50 | } 51 | 52 | // @dev: refund all unused fees to caller. 53 | function DOSRefund() public onlyOwner { 54 | address token = IPayment(dosAddrBridge.getPaymentAddress()).defaultTokenAddr(); 55 | uint amount = IERC20(token).balanceOf(address(this)); 56 | IERC20(token).transfer(msg.sender, amount); 57 | } 58 | 59 | // @dev: Call this function to get a unique queryId to differentiate 60 | // parallel requests. A return value of 0x0 stands for error and a 61 | // related event would be emitted. 62 | // @timeout: Estimated timeout in seconds specified by caller; e.g. 15. 63 | // Response is not guaranteed if processing time exceeds this. 64 | // @dataSource: Data source destination specified by caller. 65 | // E.g.: 'https://api.coinbase.com/v2/prices/ETH-USD/spot' 66 | // @selector: A selector expression provided by caller to filter out 67 | // specific data fields out of the raw response. The response 68 | // data format (json, xml/html, and more) is identified from 69 | // the selector expression. 70 | // E.g. Use "$.data.amount" to extract "194.22" out. 71 | // { 72 | // "data":{ 73 | // "base":"ETH", 74 | // "currency":"USD", 75 | // "amount":"194.22" 76 | // } 77 | // } 78 | // Check below documentation for details. 79 | // (https://dosnetwork.github.io/docs/#/contents/blockchains/ethereum?id=selector). 80 | function DOSQuery(uint timeout, string memory dataSource, string memory selector) 81 | internal 82 | resolveAddress 83 | returns (uint) 84 | { 85 | return dosProxy.query(address(this), timeout, dataSource, selector); 86 | } 87 | 88 | // @dev: Must override __callback__ to process a corresponding response. A 89 | // user-defined event could be added to notify the Dapp frontend that 90 | // the response is ready. 91 | // @queryId: A unique queryId returned by DOSQuery() for callers to 92 | // differentiate parallel responses. 93 | // @result: Response for the specified queryId. 94 | function __callback__(uint queryId, bytes calldata result) external { 95 | // To be overridden in the caller contract. 96 | } 97 | 98 | // @dev: Call this function to request either a fast but insecure random 99 | // number or a safe and secure random number delivered back 100 | // asynchronously through the __callback__ function. 101 | // Depending on the mode, the return value would be a random number 102 | // (for fast mode) or a requestId (for safe mode). 103 | // @seed: Optional random seed provided by caller. 104 | function DOSRandom(uint seed) 105 | internal 106 | resolveAddress 107 | returns (uint) 108 | { 109 | return dosProxy.requestRandom(address(this), seed); 110 | } 111 | 112 | // @dev: Must override __callback__ to process a corresponding random 113 | // number. A user-defined event could be added to notify the Dapp 114 | // frontend that a new secure random number is generated. 115 | // @requestId: A unique requestId returned by DOSRandom() for requester to 116 | // differentiate random numbers generated concurrently. 117 | // @generatedRandom: Generated secure random number for the specific 118 | // requestId. 119 | function __callback__(uint requestId, uint generatedRandom) external auth { 120 | // To be overridden in the caller contract. 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /contracts/DOSOnChainSDK-bsctest.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | import "./Ownable.sol"; 4 | 5 | contract IProxy { 6 | function query(address, uint, string memory, string memory) public returns (uint); 7 | function requestRandom(address, uint) public returns (uint); 8 | } 9 | 10 | contract IPayment { 11 | function setPaymentMethod(address payer, address tokenAddr) public; 12 | function defaultTokenAddr() public returns(address); 13 | } 14 | 15 | contract IAddressBridge { 16 | function getProxyAddress() public view returns (address); 17 | function getPaymentAddress() public view returns (address); 18 | } 19 | 20 | contract IERC20 { 21 | function balanceOf(address who) public view returns (uint); 22 | function transfer(address to, uint value) public returns (bool); 23 | function approve(address spender, uint value) public returns (bool); 24 | } 25 | 26 | contract DOSOnChainSDK is Ownable { 27 | IProxy dosProxy; 28 | IAddressBridge dosAddrBridge = IAddressBridge(0xb20BE4f55Aca452a60b8812F051c39C302161BE7); 29 | 30 | modifier resolveAddress { 31 | address proxyAddr = dosAddrBridge.getProxyAddress(); 32 | if (address(dosProxy) != proxyAddr) { 33 | dosProxy = IProxy(proxyAddr); 34 | } 35 | _; 36 | } 37 | 38 | modifier auth { 39 | // Filter out malicious __callback__ caller. 40 | require(msg.sender == dosAddrBridge.getProxyAddress(), "Unauthenticated response"); 41 | _; 42 | } 43 | 44 | // @dev: call setup function first and transfer DOS tokens into deployed contract as oracle fees. 45 | function DOSSetup() public onlyOwner { 46 | address paymentAddr = dosAddrBridge.getPaymentAddress(); 47 | address defaultToken = IPayment(dosAddrBridge.getPaymentAddress()).defaultTokenAddr(); 48 | IERC20(defaultToken).approve(paymentAddr, uint(-1)); 49 | IPayment(dosAddrBridge.getPaymentAddress()).setPaymentMethod(address(this), defaultToken); 50 | } 51 | 52 | // @dev: refund all unused fees to caller. 53 | function DOSRefund() public onlyOwner { 54 | address token = IPayment(dosAddrBridge.getPaymentAddress()).defaultTokenAddr(); 55 | uint amount = IERC20(token).balanceOf(address(this)); 56 | IERC20(token).transfer(msg.sender, amount); 57 | } 58 | 59 | // @dev: Call this function to get a unique queryId to differentiate 60 | // parallel requests. A return value of 0x0 stands for error and a 61 | // related event would be emitted. 62 | // @timeout: Estimated timeout in seconds specified by caller; e.g. 15. 63 | // Response is not guaranteed if processing time exceeds this. 64 | // @dataSource: Data source destination specified by caller. 65 | // E.g.: 'https://api.coinbase.com/v2/prices/ETH-USD/spot' 66 | // @selector: A selector expression provided by caller to filter out 67 | // specific data fields out of the raw response. The response 68 | // data format (json, xml/html, and more) is identified from 69 | // the selector expression. 70 | // E.g. Use "$.data.amount" to extract "194.22" out. 71 | // { 72 | // "data":{ 73 | // "base":"ETH", 74 | // "currency":"USD", 75 | // "amount":"194.22" 76 | // } 77 | // } 78 | // Check below documentation for details. 79 | // (https://dosnetwork.github.io/docs/#/contents/blockchains/ethereum?id=selector). 80 | function DOSQuery(uint timeout, string memory dataSource, string memory selector) 81 | internal 82 | resolveAddress 83 | returns (uint) 84 | { 85 | return dosProxy.query(address(this), timeout, dataSource, selector); 86 | } 87 | 88 | // @dev: Must override __callback__ to process a corresponding response. A 89 | // user-defined event could be added to notify the Dapp frontend that 90 | // the response is ready. 91 | // @queryId: A unique queryId returned by DOSQuery() for callers to 92 | // differentiate parallel responses. 93 | // @result: Response for the specified queryId. 94 | function __callback__(uint queryId, bytes calldata result) external { 95 | // To be overridden in the caller contract. 96 | } 97 | 98 | // @dev: Call this function to request either a fast but insecure random 99 | // number or a safe and secure random number delivered back 100 | // asynchronously through the __callback__ function. 101 | // Depending on the mode, the return value would be a random number 102 | // (for fast mode) or a requestId (for safe mode). 103 | // @seed: Optional random seed provided by caller. 104 | function DOSRandom(uint seed) 105 | internal 106 | resolveAddress 107 | returns (uint) 108 | { 109 | return dosProxy.requestRandom(address(this), seed); 110 | } 111 | 112 | // @dev: Must override __callback__ to process a corresponding random 113 | // number. A user-defined event could be added to notify the Dapp 114 | // frontend that a new secure random number is generated. 115 | // @requestId: A unique requestId returned by DOSRandom() for requester to 116 | // differentiate random numbers generated concurrently. 117 | // @generatedRandom: Generated secure random number for the specific 118 | // requestId. 119 | function __callback__(uint requestId, uint generatedRandom) external auth { 120 | // To be overridden in the caller contract. 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /contracts/DOSOnChainSDK-hecotest.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | import "./Ownable.sol"; 4 | 5 | contract IProxy { 6 | function query(address, uint, string memory, string memory) public returns (uint); 7 | function requestRandom(address, uint) public returns (uint); 8 | } 9 | 10 | contract IPayment { 11 | function setPaymentMethod(address payer, address tokenAddr) public; 12 | function defaultTokenAddr() public returns(address); 13 | } 14 | 15 | contract IAddressBridge { 16 | function getProxyAddress() public view returns (address); 17 | function getPaymentAddress() public view returns (address); 18 | } 19 | 20 | contract IERC20 { 21 | function balanceOf(address who) public view returns (uint); 22 | function transfer(address to, uint value) public returns (bool); 23 | function approve(address spender, uint value) public returns (bool); 24 | } 25 | 26 | contract DOSOnChainSDK is Ownable { 27 | IProxy dosProxy; 28 | IAddressBridge dosAddrBridge = IAddressBridge(0x797D0f474dDcAa8F4066A263684B540C074801b3); 29 | 30 | modifier resolveAddress { 31 | address proxyAddr = dosAddrBridge.getProxyAddress(); 32 | if (address(dosProxy) != proxyAddr) { 33 | dosProxy = IProxy(proxyAddr); 34 | } 35 | _; 36 | } 37 | 38 | modifier auth { 39 | // Filter out malicious __callback__ caller. 40 | require(msg.sender == dosAddrBridge.getProxyAddress(), "Unauthenticated response"); 41 | _; 42 | } 43 | 44 | // @dev: call setup function first and transfer DOS tokens into deployed contract as oracle fees. 45 | function DOSSetup() public onlyOwner { 46 | address paymentAddr = dosAddrBridge.getPaymentAddress(); 47 | address defaultToken = IPayment(dosAddrBridge.getPaymentAddress()).defaultTokenAddr(); 48 | IERC20(defaultToken).approve(paymentAddr, uint(-1)); 49 | IPayment(dosAddrBridge.getPaymentAddress()).setPaymentMethod(address(this), defaultToken); 50 | } 51 | 52 | // @dev: refund all unused fees to caller. 53 | function DOSRefund() public onlyOwner { 54 | address token = IPayment(dosAddrBridge.getPaymentAddress()).defaultTokenAddr(); 55 | uint amount = IERC20(token).balanceOf(address(this)); 56 | IERC20(token).transfer(msg.sender, amount); 57 | } 58 | 59 | // @dev: Call this function to get a unique queryId to differentiate 60 | // parallel requests. A return value of 0x0 stands for error and a 61 | // related event would be emitted. 62 | // @timeout: Estimated timeout in seconds specified by caller; e.g. 15. 63 | // Response is not guaranteed if processing time exceeds this. 64 | // @dataSource: Data source destination specified by caller. 65 | // E.g.: 'https://api.coinbase.com/v2/prices/ETH-USD/spot' 66 | // @selector: A selector expression provided by caller to filter out 67 | // specific data fields out of the raw response. The response 68 | // data format (json, xml/html, and more) is identified from 69 | // the selector expression. 70 | // E.g. Use "$.data.amount" to extract "194.22" out. 71 | // { 72 | // "data":{ 73 | // "base":"ETH", 74 | // "currency":"USD", 75 | // "amount":"194.22" 76 | // } 77 | // } 78 | // Check below documentation for details. 79 | // (https://dosnetwork.github.io/docs/#/contents/blockchains/ethereum?id=selector). 80 | function DOSQuery(uint timeout, string memory dataSource, string memory selector) 81 | internal 82 | resolveAddress 83 | returns (uint) 84 | { 85 | return dosProxy.query(address(this), timeout, dataSource, selector); 86 | } 87 | 88 | // @dev: Must override __callback__ to process a corresponding response. A 89 | // user-defined event could be added to notify the Dapp frontend that 90 | // the response is ready. 91 | // @queryId: A unique queryId returned by DOSQuery() for callers to 92 | // differentiate parallel responses. 93 | // @result: Response for the specified queryId. 94 | function __callback__(uint queryId, bytes calldata result) external { 95 | // To be overridden in the caller contract. 96 | } 97 | 98 | // @dev: Call this function to request either a fast but insecure random 99 | // number or a safe and secure random number delivered back 100 | // asynchronously through the __callback__ function. 101 | // Depending on the mode, the return value would be a random number 102 | // (for fast mode) or a requestId (for safe mode). 103 | // @seed: Optional random seed provided by caller. 104 | function DOSRandom(uint seed) 105 | internal 106 | resolveAddress 107 | returns (uint) 108 | { 109 | return dosProxy.requestRandom(address(this), seed); 110 | } 111 | 112 | // @dev: Must override __callback__ to process a corresponding random 113 | // number. A user-defined event could be added to notify the Dapp 114 | // frontend that a new secure random number is generated. 115 | // @requestId: A unique requestId returned by DOSRandom() for requester to 116 | // differentiate random numbers generated concurrently. 117 | // @generatedRandom: Generated secure random number for the specific 118 | // requestId. 119 | function __callback__(uint requestId, uint generatedRandom) external auth { 120 | // To be overridden in the caller contract. 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /migrations/2_deploy_dos_contracts.js: -------------------------------------------------------------------------------- 1 | const DOSAddressBridge = artifacts.require("./DOSAddressBridge.sol"); 2 | const CommitReveal = artifacts.require("./CommitReveal.sol"); 3 | const DOSProxy = artifacts.require("./DOSProxy.sol"); 4 | const DOSPayment = artifacts.require("./DOSPayment.sol"); 5 | const Staking = artifacts.require("./Staking.sol"); 6 | const ContractGateway = artifacts.require("./ContractGateway.sol"); 7 | 8 | const configs = { 9 | rinkeby: { 10 | DOSToken: '0x214e79c85744cd2ebbc64ddc0047131496871bee', 11 | DBToken: '0x9bfe8f5749d90eb4049ad94cc4de9b6c4c31f822', 12 | RewardsVault: '0xE222f441cb42bCFE8E46Fdecad0e633C70246BD3', 13 | GatewayAdmin: '0xebef930796883E0A1D2f8964AEd7a59FE64e68E6', 14 | BootstrapList: 'https://dashboard.dos.network/api/bootStrapRinkeby', 15 | }, 16 | mainnet: { 17 | DOSToken: '0x0A913beaD80F321E7Ac35285Ee10d9d922659cB7', 18 | DBToken: '0x9456d6a22c8bdFF613366d51e3d60402cB8cFd8F', 19 | RewardsVault: '0x76cEc0b88FD0F109C04F0475EBdF1648DF1c60B4', 20 | GatewayAdmin: '0x250f871e3ccafde7b5053f321241fd8bb67a54f8', 21 | BootstrapList: 'https://dashboard.dos.network/api/bootStrap', 22 | }, 23 | hecoTestnet: { 24 | DOSToken: '0x3bca354b33e0a0ca6487fb51d1150f6e9c0e0e5e', 25 | DBToken: '0x84c6be700f2db040ed1455ac980538003cda90dd', 26 | RewardsVault: '0xE222f441cb42bCFE8E46Fdecad0e633C70246BD3', 27 | GatewayAdmin: '0xebef930796883E0A1D2f8964AEd7a59FE64e68E6', 28 | BootstrapList: 'https://dashboard.dos.network/api/bootStrapHeco', 29 | }, 30 | heco: { 31 | DOSToken: '0xF50821F0A136A4514D476dC4Cc2a731e7728aFaF', 32 | DBToken: '0x1B7BEaa5107Ac5Fb2E8ADCAE2B64B0Ba1997EFd9', 33 | RewardsVault: '0xC25079a8A14FCA9a588616ebACD7b68745a3f709', 34 | GatewayAdmin: '0x78DBae2489CD0E961893788272AF2C85Fc03d418', 35 | BootstrapList: 'https://dashboard.dos.network/api/bootStrapHeco', 36 | }, 37 | okchainTest: { 38 | DOSToken: '0x51147d0bc5be0a9d487a412e59ece23bb699461a', 39 | DBToken: '0x7c013b34d07ab263233372a2f385460fdedd902a', 40 | RewardsVault: '0xE222f441cb42bCFE8E46Fdecad0e633C70246BD3', 41 | GatewayAdmin: '0xebef930796883E0A1D2f8964AEd7a59FE64e68E6', 42 | BootstrapList: 'https://dashboard.dos.network/api/bootStrapOkchain', 43 | }, 44 | bscTestnet: { 45 | DOSToken: '0x4eCECeCDCFC068643252b46F86bA1Dc30dccB1dD', 46 | DBToken: '0xCEF1EE1049903B727cc60d6990cd48567AA7D983', 47 | RewardsVault: '0xE222f441cb42bCFE8E46Fdecad0e633C70246BD3', 48 | GatewayAdmin: '0xebef930796883E0A1D2f8964AEd7a59FE64e68E6', 49 | BootstrapList: 'https://dashboard.dos.network/api/bootStrapBSC', 50 | }, 51 | bsc: { 52 | DOSToken: '0xDc0f0a5719c39764b011eDd02811BD228296887C', 53 | DBToken: '0x4250A5022C4372e3f16cfa47Ac6449C48eC719b6', 54 | RewardsVault: '0x76cEc0b88FD0F109C04F0475EBdF1648DF1c60B4', 55 | GatewayAdmin: '0x250f871e3ccafde7b5053f321241fd8bb67a54f8', 56 | BootstrapList: 'https://dashboard.dos.network/api/bootStrapBSC', 57 | }, 58 | } 59 | 60 | module.exports = function(deployer, network, accounts) { 61 | deployer.then(async () => { 62 | await deployer.deploy(DOSAddressBridge); 63 | let bridgeInstance = await DOSAddressBridge.deployed(); 64 | 65 | if (network === 'rinkeby' || network === 'live' || network === 'hecoTestnet' || 66 | network === 'heco' || network === 'okchainTest' || network === 'bscTestnet' || network === 'bsc') { 67 | await bridgeInstance.setBootStrapUrl(configs[network].BootstrapList); 68 | // Deploying CommitReveal contracts. 69 | await deployer.deploy(CommitReveal, DOSAddressBridge.address); 70 | await bridgeInstance.setCommitRevealAddress(CommitReveal.address); 71 | 72 | 73 | // Deploying DOSPayment implementation & proxy contracts. 74 | await deployer.deploy(DOSPayment, DOSAddressBridge.address, configs[network].RewardsVault, configs[network].DOSToken); 75 | await deployer.deploy(ContractGateway, DOSPayment.address); 76 | await bridgeInstance.setPaymentAddress(ContractGateway.address); 77 | let PaymentGateway = await ContractGateway.deployed(); 78 | await PaymentGateway.changeAdmin(configs[network].GatewayAdmin); 79 | // Pretend the proxy address is a Payment impl. This is ok as proxy will forward 80 | // all the calls to the Payment impl. 81 | PaymentGateway = await DOSPayment.at(ContractGateway.address); 82 | await PaymentGateway.initialize(DOSAddressBridge.address, configs[network].RewardsVault, configs[network].DOSToken); 83 | 84 | 85 | // Note: guardianFundsAddr to call approve(PaymentGateway.address) as part of initialization. 86 | 87 | // Deploying DOSProxy contract. 88 | await deployer.deploy(DOSProxy, DOSAddressBridge.address, configs[network].RewardsVault, configs[network].DOSToken); 89 | await bridgeInstance.setProxyAddress(DOSProxy.address); 90 | 91 | 92 | // Deploying Staking implementation & proxy contracts. 93 | await deployer.deploy(Staking, configs[network].DOSToken, configs[network].DBToken, configs[network].RewardsVault, DOSAddressBridge.address); 94 | await deployer.deploy(ContractGateway, Staking.address); 95 | await bridgeInstance.setStakingAddress(ContractGateway.address); 96 | let StakingGateway = await ContractGateway.deployed(); 97 | await StakingGateway.changeAdmin(configs[network].GatewayAdmin); 98 | // Pretend the proxy address is a Staking impl. This is ok as proxy will forward 99 | // all the calls to the Staking impl. 100 | StakingGateway = await Staking.at(ContractGateway.address); 101 | await StakingGateway.initialize(configs[network].DOSToken, configs[network].DBToken, configs[network].RewardsVault, DOSAddressBridge.address); 102 | 103 | 104 | // Note: stakingRewardsValut to call approve(StakingGateway.address) as part of initialization. 105 | } 106 | }); 107 | } 108 | -------------------------------------------------------------------------------- /contracts/lib/SafeMath.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | library SafeMath { 4 | /** 5 | * @dev Returns the addition of two unsigned integers, reverting on overflow. 6 | * 7 | * Counterpart to Solidity's `+` operator. 8 | * 9 | * Requirements: 10 | * - Addition cannot overflow. 11 | */ 12 | function add(uint256 a, uint256 b) internal pure returns (uint256) { 13 | uint256 c = a + b; 14 | require(c >= a, "SafeMath: addition overflow"); 15 | 16 | return c; 17 | } 18 | 19 | /** 20 | * @dev Returns the addition of two unsigned integers, reverting with custom message on overflow. 21 | * 22 | * Counterpart to Solidity's `+` operator. 23 | * 24 | * Requirements: 25 | * - Addition cannot overflow. 26 | */ 27 | function add(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { 28 | uint256 c = a + b; 29 | require(c >= a, errorMessage); 30 | 31 | return c; 32 | } 33 | 34 | /** 35 | * @dev Returns the subtraction of two unsigned integers, reverting on underflow (when the result is negative). 36 | * 37 | * Counterpart to Solidity's `-` operator. 38 | * 39 | * Requirements: 40 | * - Subtraction cannot underflow. 41 | */ 42 | function sub(uint256 a, uint256 b) internal pure returns (uint256) { 43 | return sub(a, b, "SafeMath: subtraction underflow"); 44 | } 45 | 46 | /** 47 | * @dev Returns the subtraction of two unsigned integers, reverting with custom message on underflow (when the result is negative). 48 | * 49 | * Counterpart to Solidity's `-` operator. 50 | * 51 | * Requirements: 52 | * - Subtraction cannot underflow. 53 | */ 54 | function sub(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { 55 | require(b <= a, errorMessage); 56 | uint256 c = a - b; 57 | 58 | return c; 59 | } 60 | 61 | /** 62 | * @dev Returns the multiplication of two unsigned integers, reverting on overflow. 63 | * 64 | * Counterpart to Solidity's `*` operator. 65 | * 66 | * Requirements: 67 | * - Multiplication cannot overflow. 68 | */ 69 | function mul(uint256 a, uint256 b) internal pure returns (uint256) { 70 | // Gas optimization: this is cheaper than requiring 'a' not being zero, but the 71 | // benefit is lost if 'b' is also tested. 72 | // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 73 | if (a == 0) { 74 | return 0; 75 | } 76 | 77 | uint256 c = a * b; 78 | require(c / a == b, "SafeMath: multiplication overflow"); 79 | 80 | return c; 81 | } 82 | 83 | /** 84 | * @dev Returns the multiplication of two unsigned integers, reverting on overflow. 85 | * 86 | * Counterpart to Solidity's `*` operator. 87 | * 88 | * Requirements: 89 | * - Multiplication cannot overflow. 90 | */ 91 | function mul(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { 92 | // Gas optimization: this is cheaper than requiring 'a' not being zero, but the 93 | // benefit is lost if 'b' is also tested. 94 | // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 95 | if (a == 0) { 96 | return 0; 97 | } 98 | 99 | uint256 c = a * b; 100 | require(c / a == b, errorMessage); 101 | 102 | return c; 103 | } 104 | 105 | /** 106 | * @dev Returns the integer division of two unsigned integers. 107 | * Reverts on division by zero. The result is rounded towards zero. 108 | * 109 | * Counterpart to Solidity's `/` operator. Note: this function uses a 110 | * `revert` opcode (which leaves remaining gas untouched) while Solidity 111 | * uses an invalid opcode to revert (consuming all remaining gas). 112 | * 113 | * Requirements: 114 | * - The divisor cannot be zero. 115 | */ 116 | function div(uint256 a, uint256 b) internal pure returns (uint256) { 117 | return div(a, b, "SafeMath: division by zero"); 118 | } 119 | 120 | /** 121 | * @dev Returns the integer division of two unsigned integers. 122 | * Reverts with custom message on division by zero. The result is rounded towards zero. 123 | * 124 | * Counterpart to Solidity's `/` operator. Note: this function uses a 125 | * `revert` opcode (which leaves remaining gas untouched) while Solidity 126 | * uses an invalid opcode to revert (consuming all remaining gas). 127 | * 128 | * Requirements: 129 | * - The divisor cannot be zero. 130 | */ 131 | function div(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { 132 | // Solidity only automatically asserts when dividing by 0 133 | require(b > 0, errorMessage); 134 | uint256 c = a / b; 135 | // assert(a == b * c + a % b); // There is no case in which this doesn't hold 136 | 137 | return c; 138 | } 139 | 140 | /** 141 | * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), 142 | * Reverts when dividing by zero. 143 | * 144 | * Counterpart to Solidity's `%` operator. This function uses a `revert` 145 | * opcode (which leaves remaining gas untouched) while Solidity uses an 146 | * invalid opcode to revert (consuming all remaining gas). 147 | * 148 | * Requirements: 149 | * - The divisor cannot be zero. 150 | */ 151 | function mod(uint256 a, uint256 b) internal pure returns (uint256) { 152 | return mod(a, b, "SafeMath: modulo by zero"); 153 | } 154 | 155 | /** 156 | * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), 157 | * Reverts with custom message when dividing by zero. 158 | * 159 | * Counterpart to Solidity's `%` operator. This function uses a `revert` 160 | * opcode (which leaves remaining gas untouched) while Solidity uses an 161 | * invalid opcode to revert (consuming all remaining gas). 162 | * 163 | * Requirements: 164 | * - The divisor cannot be zero. 165 | */ 166 | function mod(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { 167 | require(b != 0, errorMessage); 168 | return a % b; 169 | } 170 | 171 | // @dev Returns x^n. 172 | function pow(uint x, uint n) internal pure returns (uint z) { 173 | z = n % 2 != 0 ? x : 1; 174 | 175 | for (n /= 2; n != 0; n /= 2) { 176 | x = mul(x, x); 177 | 178 | if (n % 2 != 0) { 179 | z = mul(z, x); 180 | } 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /deployed.json: -------------------------------------------------------------------------------- 1 | { 2 | "mainnet":{ 3 | "DOSToken":"0x0A913beaD80F321E7Ac35285Ee10d9d922659cB7", 4 | "DBToken":"0x9456d6a22c8bdFF613366d51e3d60402cB8cFd8F", 5 | "DOSAddressBridge":"0x98a0e7026778840aacd28b9c03137d32e06f5ff1", 6 | "Commit Reveal":"0x144ed0555269628049f76da2adbdcdf3aa488e0e", 7 | "DOSProxy":"0x557B3CE8891D4ef5D6ad2A66445993a035FEdb5c", 8 | "DOSPayment Gateway":"0x7B8D5a37008382B2C7e8e15EcF3C2727e38A6aC6", 9 | "DOSPayment Implementation":"0x24286C5a340bF99EDB2d7e7D114477987d34816F", 10 | "Staking Gateway":"0x5DBeF8E9e83A17D4D1D4c65a1E26133EDAE851Dc", 11 | "Staking Implementation":"0x6a829E0EB032FA39D0444D29DFd80Bd3AE91C5B9" 12 | }, 13 | "rinkeby":{ 14 | "DOSToken":"0x214e79c85744cd2ebbc64ddc0047131496871bee", 15 | "DBToken":"0x9bfe8f5749d90eb4049ad94cc4de9b6c4c31f822", 16 | "DOSAddressBridge":"0xeE2e9f35c9F91571535173902E7e7B4E67deE32b", 17 | "Commit Reveal":"0x044D8D7028eC8Fc98247d072603F5316656EcfDe", 18 | "DOSProxy":"0xAb09D3A9998c918Ffa796F6449D8515e5C7DB8a2", 19 | "DOSPayment Gateway":"0x306d78A9Cf1116513220C908C5D950914D797682", 20 | "DOSPayment Implementation":"0x6b89f9C6bD11B14ae17DAfba4C578DdA527E7EF3", 21 | "Staking Gateway":"0x064fa6a739580a9bA8cfeFDc271fa5585BC274e3", 22 | "Staking Implementation":"0x9FAAebE59eaf3132c3cf42a947bAb11408D12296" 23 | }, 24 | "hecoTestnet":{ 25 | "DOSToken":"0x3bca354b33e0a0ca6487fb51d1150f6e9c0e0e5e", 26 | "DBToken":"0x84c6be700f2db040ed1455ac980538003cda90dd", 27 | "DOSAddressBridge":"0x797D0f474dDcAa8F4066A263684B540C074801b3", 28 | "Commit Reveal":"0x9DC5A7FE9cE5136Cfd2c4B2FfCAe38c7d2b1819A", 29 | "DOSProxy":"0x56e315c15506d4fdfc4bfcd895802c35f9dfd903", 30 | "DOSPayment Gateway":"0x4eCECeCDCFC068643252b46F86bA1Dc30dccB1dD", 31 | "DOSPayment Implementation":"0x7e6a95eb1d5f2f9c89b3e6eb78afd08770cc18d8", 32 | "Staking Gateway":"0xFd9B8A33756A9De4E2bBd9cc066dB912bb5c5499", 33 | "Staking Implementation":"0xAA06934e959efa9F8E41B8c6D3CA49a8Da0Ca37e", 34 | "CoingeckoParserV2" : "0xBF8295AF9c26618Fb48c4fEF38571268413147F0", 35 | "CoingeckoMegaStream": "0x8d2cf53d3c9635DC0f73B4Ca4cbf3191DBeC29b8", 36 | "CoingeckoStreamsManager": "0x51D657f6000190Ba785e30344E9987028c1CF025", 37 | "CoingeckoBTCUSDStream" : "0x2022737ddC95b15d55E452C9E8418899063fc196", 38 | "CoingeckoETHUSDStream" : "0x9E39eF90EfE2eF833a9bB1F149be17477cdFA4d0", 39 | "CoingeckoDOTUSDStream" : "0x4bBe16963C98553416114846e77696680F7cB812", 40 | "CoingeckoHTUSDStream" : "0x438A5CC6A2b5E14A81e196fa32356a3705db10Fa", 41 | "CoingeckoDOSUSDStream" : "0xfBc42FA54b3dc107f76978436b46542E3876229F" 42 | }, 43 | "heco":{ 44 | "DOSToken":"0xF50821F0A136A4514D476dC4Cc2a731e7728aFaF", 45 | "DBToken":"0x1B7BEaa5107Ac5Fb2E8ADCAE2B64B0Ba1997EFd9", 46 | "DOSAddressBridge":"0x9Ee7F642d3955ecf17D7223705DCb285dbA679fc", 47 | "Commit Reveal":"0x2a15a2Bc8F0bEc8316977178A72DFDB533565b74", 48 | "DOSProxy":"0x142fE126773Df5dE10148028D516A9DA8a1d71AD", 49 | "DOSPayment Gateway":"0xC3ffA80C80AcdA7a12F452df5dcd3afaf5Ebeed8", 50 | "DOSPayment Implementation":"0xCfE9F9230796ed9532FA790EF70d845585bc8caf", 51 | "Staking Gateway":"0xc1Ce1C11b485c7E227Cb066C75B6280533194Ad2", 52 | "Staking Implementation":"0xA3F8Cf8F4bEE4b0CD983653201300C8A29336720", 53 | "CoingeckoParserV2": "0xE1Ff4ebc4ECa883Bb615a8986aB0543E475E70e1", 54 | "CoingeckoMegaStream": "0xF0236DfB582B43fF2db7f390A6829cF22ABf9bB9", 55 | "CoingeckoStreamsManager": "0xc02DDAeb7644b9D2d92b96371f79A92FB6d306fe", 56 | "CoingeckoBTCUSDStream" : "0xEE12758Ea54F0Afa4268cc1Fc5BB70b53E24E8A5", 57 | "CoingeckoETHUSDStream" : "0x7ceDd198017b030F9009D800fa8e3DfEB48e098f", 58 | "CoingeckoDOTUSDStream" : "0xB01c89Effe80c29782d50CBE09160F20BA9Bc4F7", 59 | "CoingeckoHTUSDStream" : "0xB6D103f142A3f493f1d00478F64159591d82c92a", 60 | "CoingeckoDOSUSDStream" : "0xdB2831118407367d9116E509bC7AeED325c21B59", 61 | "CoingeckoFILUSDStream" : "0x0bC3E19138F8B0CaB31F369565e4fd5A90bD79f8", 62 | "CoingeckoHPTUSDStream" : "0x27CF3f3C05188Fb61696d0a6Aad1a1013126cA89" 63 | }, 64 | "okchainTest":{ 65 | "DOSToken":"0x51147d0bc5be0a9d487a412e59ece23bb699461a", 66 | "DBToken":"0x7c013b34d07ab263233372a2f385460fdedd902a", 67 | "DOSAddressBridge":"0xD1aFEa6D7745c9d168A77Abd80124A0592B0D48e", 68 | "Commit Reveal":"0x6510aA8EF75e409f8139084B5ec1D0Ef39759A91", 69 | "DOSProxy":"0x365b9F766a54d161f260bB806e6E97043Ee8668c", 70 | "DOSPayment Gateway":"0x7e6A95EB1D5F2f9c89B3E6eb78AFD08770cc18D8", 71 | "DOSPayment Implementation":"0x6863D706861d7ce7ADB1e1AD5AB54E1784bB670d", 72 | "Staking Gateway":"0xAA06934e959efa9F8E41B8c6D3CA49a8Da0Ca37e", 73 | "Staking Implementation":"0x9aEB6F0BD24962C44FdAD317DF9f5C6FD0d46C9E" 74 | }, 75 | "bscTestnet":{ 76 | "DOSToken":"0x4eCECeCDCFC068643252b46F86bA1Dc30dccB1dD", 77 | "DBToken":"0xCEF1EE1049903B727cc60d6990cd48567AA7D983", 78 | "DOSAddressBridge":"0xb20BE4f55Aca452a60b8812F051c39C302161BE7", 79 | "Commit Reveal":"0x686a79ff2054A5Edc632295A7278980138E6a134", 80 | "DOSProxy":"0x1A4936Ee97EaBc4E7B18201AAeF2971f77ab4617", 81 | "DOSPayment Gateway":"0x9B21770b41DD007f1C5131380286a6364D685912", 82 | "DOSPayment Implementation":"0x1fC4553CebE6841BFe63dB00382E3822Cd6AEf4a", 83 | "Staking Gateway":"0xC706701Fbd0eaFeBFD279d2bF4be61EACbeFfF8e", 84 | "Staking Implementation":"0xCd79875835039Fd4367d6807994af6Bc95681984", 85 | "CoingeckoParserV2": "0x47011aD8d19e445e82D2B2a42604C3eaAC1D262D", 86 | "CoingeckoMegaStream": "0xB65eAd4D23E4A7a407FACf02Aed3EB5A94b0A774", 87 | "CoingeckoStreamsManager": "0x7a61d1Ab09B69C61B25d8D113a963453A0f996D2", 88 | "CoingeckoBTCUSDStream" : "0x2086eB68cE992c8A53EB0bBe28E2C0E4A7D254B9", 89 | "CoingeckoETHUSDStream" : "0x1CA4AF463713974123093f593D5c0CC1668933E0", 90 | "CoingeckoBNBUSDStream" : "0xF4D5436175c5d81174395c4158981bCdb22e3055", 91 | "CoingeckoDOTUSDStream" : "0x7D433CB0ceF7f99F3051a99A2a2Be6A3D183Dd4F", 92 | "CoingeckoDOSUSDStream" : "0x1eE4Ba6e48E2adf834F41a8Fe51455D6A05B013f", 93 | "CoingeckoFILUSDStream" : "0x23464572923B24Ee8CEd6a304bb28D10a2e3919f" 94 | }, 95 | "bsc":{ 96 | "DOSToken":"0xDc0f0a5719c39764b011eDd02811BD228296887C", 97 | "DBToken":"0x4250A5022C4372e3f16cfa47Ac6449C48eC719b6", 98 | "DOSAddressBridge":"0x70157cf10404170EEc183043354D0a886Fa51d73", 99 | "Commit Reveal":"0xD5135970D1663FfD3cf96b69Dc390C10680768b1", 100 | "DOSProxy":"0x940EA430fE756d309B776349CB68EA6028B613d1", 101 | "DOSPayment Gateway":"0x27A09BA4c194C39A3898775BDEF15F444801F048", 102 | "DOSPayment Implementation":"0x98A0E7026778840Aacd28B9c03137D32e06F5ff1", 103 | "Staking Gateway":"0x8Aa8c0d911cf703C516210994De77812024497B6", 104 | "Staking Implementation":"0x4dd79f907f4D5d8952FEf1eFA0B5d0467c612Cb3" 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /test/stringutils_test.js: -------------------------------------------------------------------------------- 1 | const StringUtilsMock = artifacts.require("StringUtilsMock"); 2 | 3 | contract("StringUtils test", async() => { 4 | let stringUtils; 5 | before(async() => { 6 | stringUtils = await StringUtilsMock.new(); 7 | }) 8 | 9 | it("Test transfer byte to uint", async() => { 10 | let num = await stringUtils.createByte.call(); 11 | let numOverflow = web3.utils.toHex('A'); 12 | let result = await stringUtils.byte2Uint.call(num); 13 | let resultOverflow = await stringUtils.byte2Uint.call(numOverflow); 14 | assert.equal(result, 6, "transfer byte to uint"); 15 | assert.equal(resultOverflow, 10, "transfer overflow"); 16 | }) 17 | 18 | it("Test transfer hexByte to uint", async() => { 19 | let num = await stringUtils.createByte.call(); 20 | let char = web3.utils.toHex('F'); 21 | let charOverflow = web3.utils.toHex('G'); 22 | let result = await stringUtils.byte2Uint.call(num); 23 | let charResult = await stringUtils.hexByte2Uint.call(char); 24 | let charResultOverflow = await stringUtils.hexByte2Uint.call(charOverflow); 25 | assert.equal(result, 6, "transfer hex byte to uint"); 26 | assert.equal(charResult, 15, "transfer hexByte to uint"); 27 | assert.equal(charResultOverflow, 16, "transfer hexByte to uint"); 28 | }) 29 | 30 | it("Test decimalStr to uint", async() => { 31 | let stringDecimal = "846686978"; 32 | let stringChar = "678Aaaa"; 33 | let stringOverflow ="11579208923731619542357098500868790785326998466564056403945758400791312963993555555"; 34 | let stringDecimalResult = await stringUtils.str2Uint.call(stringDecimal); 35 | let stringCharResult = await stringUtils.str2Uint.call(stringChar); 36 | let stringDecimalOverflow = await stringUtils.str2Uint.call(stringOverflow); 37 | const UINT256MAX = await stringUtils.returnUINT256MAX.call(); 38 | assert.equal(stringDecimalResult, 846686978, "transfer a decimal string to uint" ); 39 | assert.equal(stringCharResult, 678,"transefer a char string to uint"); 40 | assert.equal(stringDecimalOverflow.toString(10), UINT256MAX.toString(10), "Overflow:transfer a decimal string to uint"); 41 | }) 42 | 43 | it("Test hexStr to uint", async() => { 44 | const UINT256MAX = await stringUtils.returnUINT256MAX.call(); 45 | let hexString0 = "d19Ab"; 46 | let hexString1 = "0xd19Ab"; 47 | let hexString2 = "0Xd19Ab"; 48 | let hexStringInvalid = "0x"; 49 | let hexStringOverflow = "0x11579208923A73161b9542357098500d86879078534545455454545454544545554444adadaadadaddad"; 50 | let hexStringResult0 = await stringUtils.hexStr2Uint.call(hexString0); 51 | let hexStringResult1 = await stringUtils.hexStr2Uint.call(hexString1); 52 | let hexStringResult2 = await stringUtils.hexStr2Uint.call(hexString2); 53 | let hexStringInvalidResult = await stringUtils.hexStr2Uint.call(hexStringInvalid); 54 | let hexStringResultOverflow = await stringUtils.hexStr2Uint.call(hexStringOverflow); 55 | assert.equal(hexStringResult0,858539,"transfer a hex string to uint"); 56 | assert.equal(hexStringResult1,858539,"transfer a hex string to uint"); 57 | assert.equal(hexStringResult2,858539,"transfer a hex string to uint"); 58 | assert.equal(hexStringInvalidResult,0,"transfer a hex string to uint"); 59 | assert.equal(hexStringResultOverflow.toString(10), UINT256MAX.toString(10), "transfer a hex string to uint"); 60 | }) 61 | 62 | it("Test 20-byte hex string to address", async() => { 63 | let hexString = "0x0e7ad63d2a305a7b9f46541c386aafbd2af6b263"; 64 | let hexStringResult = await stringUtils.str2Addr.call(hexString); 65 | assert.equal(hexStringResult, 0x0e7ad63d2a305a7b9f46541c386aafbd2af6b263, "transfer 20-byte hex string to uint"); 66 | }) 67 | 68 | it("Test address to string", async() => { 69 | let hexAddr = "0x0e7ad63d2a305a7b9f46541c386aafbd2af6b263"; 70 | let hexAddrResult = await stringUtils.addr2Str.call(hexAddr); 71 | assert.equal(hexAddrResult, "0x0e7ad63d2a305a7b9f46541c386aafbd2af6b263", "transfer address to string"); 72 | }) 73 | 74 | it("Test uint to hex string", async() => { 75 | let uintZero = 0; 76 | let uintValid = 12; 77 | let uintZeroResult = await stringUtils.uint2HexStr.call(uintZero); 78 | let uintValidResult = await stringUtils.uint2HexStr.call(uintValid); 79 | assert.equal(uintZeroResult, 0, "transfer uint to hex string"); 80 | assert.equal(uintValidResult, 'C', "transfer uint to hex string"); 81 | }) 82 | 83 | it("Test uint to string", async() => { 84 | let uintZero = 0; 85 | let uintValid = 12; 86 | let uintZeroResult = await stringUtils.uint2Str.call(uintZero); 87 | let uintValidResult = await stringUtils.uint2Str.call(uintValid); 88 | assert.equal(uintZeroResult, "0", "transfer uint to hex string"); 89 | assert.equal(uintValidResult, "12", "transfer uint to hex string"); 90 | }) 91 | 92 | it("Test strConcat and byteConcat", async() => { 93 | let aa = "Hello "; 94 | let bb = "world!"; 95 | let result = await stringUtils.strConcat.call(aa, bb); 96 | assert.equal(result, "Hello world!", "string concat"); 97 | }) 98 | 99 | it("Test strCompare and byteCompare", async() => { 100 | let aa = "abd"; 101 | let bb = "abcde"; 102 | let result = await stringUtils.strCompare.call(aa,bb); 103 | assert.equal(result,1,"string compare"); 104 | }) 105 | 106 | it("Test strEqual and byteEqual", async() => { 107 | let aa = "dosnetwork"; 108 | let bb = "dosnetwork"; 109 | let result = await stringUtils.strEqual.call(aa,bb); 110 | assert.equal(result,true,"string equal"); 111 | }) 112 | 113 | it("Test indexOf(string) and indexOf(bytes)", async() => { 114 | let haystack0 = "123"; 115 | let needle0 = ""; 116 | let haystack1 = ""; 117 | let needle1 = "45"; 118 | let haystack2 = "123"; 119 | let needle2 = "1234"; 120 | let haystack3 = "123.45"; 121 | let needle3 = "."; 122 | let result0 = await stringUtils.indexOf(haystack0,needle0); 123 | let result1 = await stringUtils.indexOf(haystack1,needle1); 124 | let result2 = await stringUtils.indexOf(haystack2,needle2); 125 | let result3 = await stringUtils.indexOf(haystack3,needle3); 126 | assert.equal(result0,0,"get index"); 127 | assert.equal(result1,haystack1.length,"get index"); 128 | assert.equal(result2,haystack2.length,"get index"); 129 | assert.equal(result3,3,"get index"); 130 | }) 131 | 132 | it("Test subStr(string,uint,uint) and subStr(bytes,uint,uint)", async() => { 133 | let a = "1234567890"; 134 | let start = 2; 135 | let len = 5; 136 | let result = await stringUtils.subStr.call(a, start, len); 137 | assert.equal(result, "34567", "get substring"); 138 | }) 139 | 140 | it("Test subStr(string,uint) and subStr(bytes,uint)", async() => { 141 | let num = "123.4567"; 142 | let result = await stringUtils.subStr1.call(num, 4); 143 | assert.equal(result, "4567", "get substring"); 144 | }) 145 | }) 146 | -------------------------------------------------------------------------------- /contracts/StreamsManager.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | pragma experimental ABIEncoderV2; 3 | 4 | import "./lib/StringUtils.sol"; 5 | import "./interfaces/StreamInterface.sol"; 6 | 7 | // StreamsManager manages group of data streams from the same meta data source (e.g. Coingecko, Coinbase, etc.) 8 | // Readable only by Data Stream UI or EOAs, not by dependant smart contracts / projects. 9 | contract StreamsManager { 10 | using StringUtils for *; 11 | 12 | string public name; 13 | address public governance; 14 | address public pendingGovernance; 15 | // Valid index starts from 1. 16 | address[] public _streams; 17 | // sorted streams according to stream.description() 18 | address[] private _sortedStreams; 19 | // stream => index in streams array 20 | mapping(address=>uint) public streamsIdx; 21 | 22 | event GovernanceProposed(address pendingGov); 23 | event GovernanceAccepted(address newGov); 24 | event StreamAdded(address stream, uint numStreams); 25 | event StreamAddressUpdated(address oldStreamAddr, address newStreamAddr); 26 | event StreamRemoved(address stream); 27 | 28 | modifier onlyGovernance { 29 | require(msg.sender == governance, "!governance"); 30 | _; 31 | } 32 | modifier accessible(address stream) { 33 | require(streamsIdx[stream] != 0 && stream == _streams[streamsIdx[stream]], "!exist"); 34 | require(msg.sender == tx.origin, "!accessible-by-non-eoa"); 35 | _; 36 | } 37 | modifier onlyMegaStream { 38 | require(msg.sender == _streams[0], "!from-megaStream"); 39 | _; 40 | } 41 | 42 | constructor(string memory _name, address megaStream) public { 43 | name = _name; 44 | governance = msg.sender; 45 | _streams.push(megaStream); 46 | streamsIdx[megaStream] = 0; 47 | } 48 | 49 | function setGovernance(address _governance) public onlyGovernance { 50 | pendingGovernance = _governance; 51 | emit GovernanceProposed(_governance); 52 | } 53 | function acceptGovernance() public { 54 | require(msg.sender == pendingGovernance, "!pendingGovernance"); 55 | governance = pendingGovernance; 56 | pendingGovernance = address(0); 57 | emit GovernanceAccepted(governance); 58 | } 59 | function quickSortBySelector(address[] memory arr, uint left, uint right) public view { 60 | if (left >= right) return; 61 | // p = the pivot element 62 | address p = arr[(left + right) / 2]; 63 | uint i = left; 64 | uint j = right; 65 | while (i < j) { 66 | while (IStream(arr[i]).selector().strCompare(IStream(p).selector()) < 0) ++i; 67 | // arr[j] > p means p still to the left, so j > 0 68 | while (IStream(arr[j]).selector().strCompare(IStream(p).selector()) > 0) --j; 69 | if (IStream(arr[i]).selector().strCompare(IStream(arr[j]).selector()) > 0) 70 | (arr[i], arr[j]) = (arr[j], arr[i]); 71 | else 72 | ++i; 73 | } 74 | 75 | // Note --j was only done when a[j] > p. So we know: a[j] == p, a[j] > p 76 | if (j > left) quickSortBySelector(arr, left, j - 1); // j > left, so j > 0 77 | quickSortBySelector(arr, j + 1, right); 78 | } 79 | function sortStreams() private { 80 | address[] memory s = new address[](_streams.length - 1); 81 | for (uint i = 1; i < _streams.length; i++) { 82 | s[i-1] = _streams[i]; 83 | } 84 | quickSortBySelector(s, 0, s.length - 1); 85 | _sortedStreams = s; 86 | } 87 | function sortedStreams() public view returns(address[] memory) { 88 | return _sortedStreams; 89 | } 90 | function addStream(address stream) public onlyGovernance { 91 | require(streamsIdx[stream] == 0, "existed"); 92 | _streams.push(stream); 93 | streamsIdx[stream] = _streams.length - 1; 94 | emit StreamAdded(stream, _streams.length - 1); 95 | sortStreams(); 96 | } 97 | function updateStream(address stream, address newStream) public onlyGovernance { 98 | require(streamsIdx[stream] != 0, "!exist"); 99 | require(streamsIdx[newStream] == 0, "existed"); 100 | _streams[streamsIdx[stream]] = newStream; 101 | streamsIdx[newStream] = streamsIdx[stream]; 102 | delete streamsIdx[stream]; 103 | emit StreamAddressUpdated(stream, newStream); 104 | sortStreams(); 105 | } 106 | function removeStream(address stream) public onlyGovernance { 107 | uint streamId = streamsIdx[stream]; 108 | require(streamId != 0, "!exist"); 109 | if (_streams.length > 2) { 110 | _streams[streamId] = _streams[_streams.length - 1]; 111 | streamsIdx[_streams[streamId]] = streamId; 112 | } 113 | _streams.length--; 114 | delete streamsIdx[stream]; 115 | emit StreamRemoved(stream); 116 | sortStreams(); 117 | } 118 | 119 | function megaUpdate(uint[] calldata data) external onlyMegaStream returns(bool) { 120 | bool ret = false; 121 | for (uint i = 0; i < data.length; i++) { 122 | ret = IStream(_sortedStreams[i]).megaUpdate(data[i]) || ret; 123 | } 124 | return ret; 125 | } 126 | 127 | function streams() public view returns(address[] memory) { 128 | return _streams; 129 | } 130 | 131 | function decimal(address stream) public view accessible(stream) returns(uint) { 132 | return IStream(stream).decimal(); 133 | } 134 | function windowSize(address stream) public view accessible(stream) returns(uint) { 135 | return IStream(stream).windowSize(); 136 | } 137 | function description(address stream) public view accessible(stream) returns(string memory) { 138 | return IStream(stream).description(); 139 | } 140 | function deviation(address stream) public view accessible(stream) returns(uint) { 141 | return IStream(stream).deviation(); 142 | } 143 | function numPoints(address stream) public view accessible(stream) returns(uint) { 144 | return IStream(stream).numPoints(); 145 | } 146 | function num24hPoints(address stream) public view accessible(stream) returns(uint) { 147 | return IStream(stream).num24hPoints(); 148 | } 149 | function latestResult(address stream) public view accessible(stream) returns(uint, uint) { 150 | return IStream(stream).latestResult(); 151 | } 152 | function last24hResults(address stream) public view accessible(stream) returns(IStream.Observation[] memory) { 153 | return IStream(stream).last24hResults(); 154 | } 155 | function results(address stream, uint startIdx, uint lastIdx) public view accessible(stream) returns(IStream.Observation[] memory) { 156 | return IStream(stream).results(startIdx, lastIdx); 157 | } 158 | function TWAP1Hour(address stream) public view accessible(stream) returns(uint) { 159 | return IStream(stream).TWAP1Hour(); 160 | } 161 | function TWAP2Hour(address stream) public view accessible(stream) returns(uint) { 162 | return IStream(stream).TWAP2Hour(); 163 | } 164 | function TWAP4Hour(address stream) public view accessible(stream) returns(uint) { 165 | return IStream(stream).TWAP4Hour(); 166 | } 167 | function TWAP6Hour(address stream) public view accessible(stream) returns(uint) { 168 | return IStream(stream).TWAP6Hour(); 169 | } 170 | function TWAP8Hour(address stream) public view accessible(stream) returns(uint) { 171 | return IStream(stream).TWAP8Hour(); 172 | } 173 | function TWAP12Hour(address stream) public view accessible(stream) returns(uint) { 174 | return IStream(stream).TWAP12Hour(); 175 | } 176 | function TWAP1Day(address stream) public view accessible(stream) returns(uint) { 177 | return IStream(stream).TWAP1Day(); 178 | } 179 | function TWAP1Week(address stream) public view accessible(stream) returns(uint) { 180 | return IStream(stream).TWAP1Week(); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /contracts/ContractGateway.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | /** 4 | * @title Proxy 5 | * @dev Implements delegation of calls to other contracts, with proper 6 | * forwarding of return values and bubbling of failures. 7 | * It defines a fallback function that delegates all calls to the address 8 | * returned by the abstract _implementation() internal function. 9 | */ 10 | contract Proxy { 11 | /** 12 | * @dev Fallback function. 13 | * Implemented entirely in `_fallback`. 14 | */ 15 | function () payable external { 16 | _fallback(); 17 | } 18 | 19 | /** 20 | * @return The Address of the implementation. 21 | */ 22 | function _implementation() internal view returns (address); 23 | 24 | /** 25 | * @dev Delegates execution to an implementation contract. 26 | * This is a low level function that doesn't return to its internal call site. 27 | * It will return to the external caller whatever the implementation returns. 28 | * @param implementation Address to delegate. 29 | */ 30 | function _delegate(address implementation) internal { 31 | assembly { 32 | // Copy msg.data. We take full control of memory in this inline assembly 33 | // block because it will not return to Solidity code. We overwrite the 34 | // Solidity scratch pad at memory position 0. 35 | calldatacopy(0, 0, calldatasize) 36 | 37 | // Call the implementation. 38 | // out and outsize are 0 because we don't know the size yet. 39 | let result := delegatecall(gas, implementation, 0, calldatasize, 0, 0) 40 | 41 | // Copy the returned data. 42 | returndatacopy(0, 0, returndatasize) 43 | 44 | switch result 45 | // delegatecall returns 0 on error. 46 | case 0 { revert(0, returndatasize) } 47 | default { return(0, returndatasize) } 48 | } 49 | } 50 | 51 | /** 52 | * @dev Function that is run as the first thing in the fallback function. 53 | * Can be redefined in derived contracts to add functionality. 54 | * Redefinitions must call super._willFallback(). 55 | */ 56 | function _willFallback() internal { 57 | } 58 | 59 | /** 60 | * @dev fallback implementation. 61 | * Extracted to enable manual triggering. 62 | */ 63 | function _fallback() internal { 64 | _willFallback(); 65 | _delegate(_implementation()); 66 | } 67 | } 68 | 69 | 70 | /** 71 | * @title UpgradeabilityProxy 72 | * @dev This contract implements a proxy that allows to change the 73 | * implementation address to which it will delegate. 74 | * Such a change is called an implementation upgrade. 75 | */ 76 | contract UpgradeabilityProxy is Proxy { 77 | /** 78 | * @dev Emitted when the implementation is upgraded. 79 | * @param implementation Address of the new implementation. 80 | */ 81 | event Upgraded(address implementation); 82 | 83 | /** 84 | * @dev Storage slot with the address of the current implementation. 85 | * This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1, and is 86 | * validated in the constructor. 87 | */ 88 | bytes32 private constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; 89 | 90 | /** 91 | * @dev Contract constructor. 92 | * @param _implementation Address of the initial implementation. 93 | */ 94 | constructor(address _implementation) public payable { 95 | assert(IMPLEMENTATION_SLOT == bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)); 96 | 97 | _setImplementation(_implementation); 98 | } 99 | 100 | function isContract(address addr) internal view returns (bool) { 101 | uint256 size; 102 | // solium-disable-next-line security/no-inline-assembly 103 | assembly { size := extcodesize(addr) } 104 | return size > 0; 105 | } 106 | 107 | /** 108 | * @dev Returns the current implementation. 109 | * @return Address of the current implementation 110 | */ 111 | function _implementation() internal view returns (address impl) { 112 | bytes32 slot = IMPLEMENTATION_SLOT; 113 | assembly { 114 | impl := sload(slot) 115 | } 116 | } 117 | 118 | /** 119 | * @dev Upgrades the proxy to a new implementation. 120 | * @param newImplementation Address of the new implementation. 121 | */ 122 | function _upgradeTo(address newImplementation) internal { 123 | _setImplementation(newImplementation); 124 | emit Upgraded(newImplementation); 125 | } 126 | 127 | /** 128 | * @dev Sets the implementation address of the proxy. 129 | * @param newImplementation Address of the new implementation. 130 | */ 131 | function _setImplementation(address newImplementation) private { 132 | require(isContract(newImplementation), "Cannot set a proxy implementation to a non-contract address"); 133 | 134 | bytes32 slot = IMPLEMENTATION_SLOT; 135 | 136 | assembly { 137 | sstore(slot, newImplementation) 138 | } 139 | } 140 | } 141 | 142 | 143 | /** 144 | * @title AdminUpgradeabilityProxy 145 | * @dev This contract combines an upgradeability proxy with an authorization 146 | * mechanism for administrative tasks. 147 | * All external functions in this contract must be guarded by the 148 | * `ifAdmin` modifier. See ethereum/solidity#3864 for a Solidity 149 | * feature proposal that would enable this to be done automatically. 150 | */ 151 | contract AdminUpgradeabilityProxy is UpgradeabilityProxy { 152 | /** 153 | * @dev Emitted when the administration has been transferred. 154 | * @param previousAdmin Address of the previous admin. 155 | * @param newAdmin Address of the new admin. 156 | */ 157 | event AdminChanged(address previousAdmin, address newAdmin); 158 | 159 | /** 160 | * @dev Storage slot with the admin of the contract. 161 | * This is the keccak-256 hash of "eip1967.proxy.admin" subtracted by 1, and is 162 | * validated in the constructor. 163 | */ 164 | bytes32 private constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; 165 | 166 | /** 167 | * @dev Modifier to check whether the `msg.sender` is the admin. 168 | * If it is, it will run the function. Otherwise, it will delegate the call 169 | * to the implementation. 170 | */ 171 | modifier ifAdmin() { 172 | if (msg.sender == _admin()) { 173 | _; 174 | } else { 175 | _fallback(); 176 | } 177 | } 178 | 179 | /** 180 | * Contract constructor. 181 | * It sets the `msg.sender` as the proxy administrator. 182 | * @param _implementation address of the initial implementation. 183 | */ 184 | constructor(address _implementation) UpgradeabilityProxy(_implementation) public { 185 | assert(ADMIN_SLOT == bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1)); 186 | 187 | _setAdmin(msg.sender); 188 | } 189 | 190 | /** 191 | * @return The address of the proxy admin. 192 | */ 193 | function admin() external ifAdmin returns (address) { 194 | return _admin(); 195 | } 196 | 197 | /** 198 | * @return The address of the implementation. 199 | */ 200 | function implementation() external ifAdmin returns (address) { 201 | return _implementation(); 202 | } 203 | 204 | /** 205 | * @dev Changes the admin of the proxy. 206 | * Only the current admin can call this function. 207 | * @param newAdmin Address to transfer proxy administration to. 208 | */ 209 | function changeAdmin(address newAdmin) external ifAdmin { 210 | require(newAdmin != address(0), "Cannot change the admin of a proxy to the zero address"); 211 | emit AdminChanged(_admin(), newAdmin); 212 | _setAdmin(newAdmin); 213 | } 214 | 215 | /** 216 | * @dev Upgrade the backing implementation of the proxy. 217 | * Only the admin can call this function. 218 | * @param newImplementation Address of the new implementation. 219 | */ 220 | function upgradeTo(address newImplementation) external ifAdmin { 221 | _upgradeTo(newImplementation); 222 | } 223 | 224 | /** 225 | * @dev Upgrade the backing implementation of the proxy and call a function 226 | * on the new implementation. 227 | * This is useful to initialize the proxied contract. 228 | * @param newImplementation Address of the new implementation. 229 | * @param data Data to send as msg.data in the low level call. 230 | * It should include the signature and the parameters of the function to be 231 | * called, as described in 232 | * https://solidity.readthedocs.io/en/develop/abi-spec.html#function-selector-and-argument-encoding. 233 | */ 234 | function upgradeToAndCall(address newImplementation, bytes calldata data) payable external ifAdmin { 235 | _upgradeTo(newImplementation); 236 | (bool success,) = newImplementation.delegatecall(data); 237 | require(success); 238 | } 239 | 240 | /** 241 | * @return The admin slot. 242 | */ 243 | function _admin() internal view returns (address adm) { 244 | bytes32 slot = ADMIN_SLOT; 245 | assembly { 246 | adm := sload(slot) 247 | } 248 | } 249 | 250 | /** 251 | * @dev Sets the address of the proxy admin. 252 | * @param newAdmin Address of the new proxy admin. 253 | */ 254 | function _setAdmin(address newAdmin) internal { 255 | bytes32 slot = ADMIN_SLOT; 256 | 257 | assembly { 258 | sstore(slot, newAdmin) 259 | } 260 | } 261 | 262 | /** 263 | * @dev Only fall back when the sender is not the admin. 264 | */ 265 | function _willFallback() internal { 266 | require(msg.sender != _admin(), "Cannot call fallback function from the proxy admin"); 267 | super._willFallback(); 268 | } 269 | } 270 | 271 | 272 | contract ContractGateway is AdminUpgradeabilityProxy { 273 | constructor(address _implementation) public AdminUpgradeabilityProxy(_implementation) { 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /scripts/guardian_bsc.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const BN = require('bignumber.js'); 3 | const fetch = require('node-fetch'); 4 | const jp = require('jsonpath'); 5 | const Web3 = require('web3'); 6 | const config = require('./config_bsctest'); 7 | const web3 = new Web3(new Web3.providers.HttpProvider(config.httpProvider)); 8 | const privateKey = '0x' + process.env.PK; 9 | // streams' state 10 | const states = []; 11 | var streamsManager = null; 12 | var megaStream = null; 13 | var errCnt = 0; 14 | 15 | async function init(debug) { 16 | assert(privateKey.length == 66, 17 | "Please export hex-formatted private key into env without leading '0x'"); 18 | 19 | streamsManager = new web3.eth.Contract(config.managerABI, config.coingeckoStreamsManagerAddr); 20 | streamsManager.address = config.coingeckoStreamsManagerAddr; 21 | let sortedStreams = await streamsManager.methods.sortedStreams().call(); 22 | if (sortedStreams.length == 0) { 23 | console.log('@@@@@@ No stream to watch, exit!'); 24 | process.exit(1); 25 | } 26 | 27 | let megaStreamAddr = await streamsManager.methods._streams(0).call(); 28 | megaStream = new web3.eth.Contract(config.megaStreamABI, megaStreamAddr); 29 | megaStream.address = megaStreamAddr; 30 | megaStream.decimal = await megaStream.methods.decimal().call(); 31 | 32 | for (let i = 0; i < sortedStreams.length; i++) { 33 | let stream = new web3.eth.Contract(config.streamABI, sortedStreams[i]); 34 | stream.address = sortedStreams[i]; 35 | let state = { 36 | stream: stream, 37 | source: await stream.methods.source().call(), 38 | selector: await stream.methods.selector().call(), 39 | windowSize: parseInt(await stream.methods.windowSize().call()), 40 | deviation: parseInt(await stream.methods.deviation().call()), 41 | decimal: parseInt(await stream.methods.decimal().call()), 42 | lastUpdated: 0, 43 | lastPrice: BN(0), 44 | } 45 | let len = parseInt(await stream.methods.numPoints().call()); 46 | if (len > 0) { 47 | let last = await stream.methods.latestResult().call(); 48 | state.lastPrice = BN(last._lastPrice); 49 | state.lastUpdated = parseInt(last._lastUpdatedTime); 50 | } 51 | states.push(state); 52 | } 53 | if (debug) console.log('+++++ streams inited ...'); 54 | } 55 | 56 | async function sync() { 57 | for (let i = 0; i < states.length; i++) { 58 | states[i].deviation = parseInt(await states[i].stream.methods.deviation().call()); 59 | let last = await states[i].stream.methods.latestResult().call(); 60 | states[i].lastPrice = BN(last._lastPrice); 61 | states[i].lastUpdated = parseInt(last._lastUpdatedTime); 62 | } 63 | } 64 | 65 | // Normalize selector string to equivalent format in case of special characters. 66 | // e.g. '$.huobi-token.usd' => '$["huobi-token"]["usd"]' 67 | function normalizeSelector(selector) { 68 | if (selector.indexOf('-') == -1) return selector; 69 | return selector 70 | .split('.') 71 | .map((val, i) => { 72 | if (i == 0) return val; 73 | return '[\"' + val + '\"]'; 74 | }) 75 | .join(''); 76 | } 77 | 78 | // Sort response json by object keys. This is to normalize the jsonpath 79 | // behavior between client software and this guardian bot. 80 | function normalizeResponseJson(respJson) { 81 | return Object.keys(respJson).sort().reduce(function (result, key) { 82 | result[key] = respJson[key]; 83 | return result; 84 | }, {}); 85 | } 86 | 87 | async function queryCoingeckoStreamsData(debug = false) { 88 | let ret = []; 89 | let resp = await fetch(config.coingeckoMegaSource); 90 | let respJson = await resp.json(); 91 | for (let i = 0; i < states.length; i++) { 92 | let data = jp.value(respJson, normalizeSelector(states[i].selector)); 93 | data = BN(data).times(BN(10).pow(states[i].decimal)); 94 | ret.push(data); 95 | if (debug) { 96 | console.log(`+++++ coingecko ${states[i].selector}: ${data}`); 97 | } 98 | } 99 | return ret; 100 | } 101 | 102 | async function queryCoingeckoMegaData(megaDecimal) { 103 | let resp = await fetch(config.coingeckoMegaSource); 104 | let respJson = await resp.json(); 105 | respJson = normalizeResponseJson(respJson); 106 | let data = jp.query(respJson, config.coingeckoMegaSelector); 107 | return data.map((val) => { 108 | return BN(val).times(BN(10).pow(megaDecimal)) 109 | }); 110 | } 111 | 112 | // Returns true if Bignumber p1 is beyond the upper/lower threshold of Bignumber p0. 113 | function deviated(p1, p0, threshold) { 114 | if (threshold == 0) return false; 115 | return p1.gt(BN(1000).plus(threshold).div(1000).times(p0)) || p1.lt(BN(1000).minus(threshold).div(1000).times(p0)); 116 | } 117 | 118 | function sleep(ms) { 119 | return new Promise((resolve, reject) => { 120 | setTimeout(() => { 121 | resolve(ms) 122 | }, ms) 123 | }) 124 | } 125 | 126 | async function pullTriggerStream(state, debug) { 127 | let callData = state.stream.methods.pullTrigger().encodeABI(); 128 | // let estimatedGas = await state.stream.methods.pullTrigger().estimateGas({gas: config.triggerMaxGas}); 129 | let txObj = await web3.eth.accounts.signTransaction({ 130 | to: state.stream.address, 131 | data: callData, 132 | value: '0', 133 | gas: config.triggerMaxGas 134 | }, privateKey); 135 | await web3.eth.sendSignedTransaction(txObj.rawTransaction) 136 | .on('confirmation', function(confirmationNumber, receipt) { 137 | // Fired for every confirmation up to the 12th confirmation (0-indexed). We treat 2 confirmations as finalized state. 138 | if (confirmationNumber == 1) { 139 | if (debug) { 140 | console.log(`+++++ ${state.selector} tx ${receipt.transactionHash} 2 confirmations, gasUsed ${receipt.gasUsed}`); 141 | } 142 | } 143 | }) 144 | .on('error', function(err, receipt) { 145 | if (debug) { 146 | console.log(`@@@@@ Error (${errCnt}): tx ${receipt.transactionHash} error, gasUsed ${receipt.gasUsed}`); 147 | } 148 | if (++errCnt > 5) { 149 | states = []; // re-Init 150 | errCnt = 0; 151 | } 152 | }); 153 | } 154 | 155 | async function pullTriggerMega(debug) { 156 | let callData = megaStream.methods.pullTrigger().encodeABI(); 157 | // let estimatedGas = await state.stream.methods.pullTrigger().estimateGas({gas: config.triggerMaxGas}); 158 | let txObj = await web3.eth.accounts.signTransaction({ 159 | to: megaStream.address, 160 | data: callData, 161 | value: '0', 162 | gas: config.triggerMaxGas, 163 | gasPrice: web3.utils.toWei(Number(config.gasPriceGwei).toString(), 'Gwei') 164 | }, privateKey); 165 | await web3.eth.sendSignedTransaction(txObj.rawTransaction) 166 | .on('confirmation', function(confirmationNumber, receipt) { 167 | // Fired for every confirmation up to the 12th confirmation (0-indexed). We treat 2 confirmations as finalized state. 168 | if (confirmationNumber == 1) { 169 | if (debug) { 170 | console.log(`+++++ tx ${receipt.transactionHash} 2 confirmations, gasUsed ${receipt.gasUsed}`); 171 | } 172 | } 173 | }) 174 | .on('error', function(err, receipt) { 175 | if (debug) { 176 | console.log(`@@@@@ Error (${errCnt}): tx ${receipt.transactionHash} error, gasUsed ${receipt.gasUsed}`); 177 | } 178 | if (++errCnt > 5) { 179 | states = []; // re-Init 180 | errCnt = 0; 181 | } 182 | }); 183 | } 184 | 185 | async function heartbeatStreams(debug = process.env.DEBUG) { 186 | if (states.length == 0) { 187 | await init(debug); 188 | } else { 189 | await sync(); 190 | } 191 | 192 | let data = await queryCoingeckoStreamsData(); 193 | for (let i = 0; i < states.length; i++) { 194 | let now = parseInt((new Date()).getTime() / 1000); 195 | let now_str = (new Date()).toTimeString().split(' ')[0]; 196 | if (i == 0 && debug) console.log(`----- heartbeatStreams ${now_str} ...`); 197 | let isDeviated = deviated(data[i], states[i].lastPrice, states[i].deviation); 198 | let isExpired = now > states[i].lastUpdated + states[i].windowSize; 199 | if (!isDeviated && !isExpired) { 200 | continue; 201 | } else if (isDeviated) { 202 | console.log(`+++++ Stream ${states[i].selector} ${now_str} d(${data[i]}), beyond last data (${states[i].lastPrice}) ±${states[i].deviation / 1000}, Deviation trigger`); 203 | } else if (isExpired) { 204 | console.log(`+++++ Stream ${states[i].selector} ${now_str} d(${data[i]}), last data (${states[i].lastPrice}) outdated, Timer trigger`); 205 | } 206 | await pullTriggerStream(states[i], debug); 207 | } 208 | setTimeout(heartbeatStreams, config.heartbeat); 209 | } 210 | 211 | async function heartbeatMega(debug = process.env.DEBUG) { 212 | if (states.length == 0) { 213 | await init(debug); 214 | } else { 215 | await sync(); 216 | } 217 | 218 | let data = await queryCoingeckoMegaData(megaStream.decimal); 219 | if (data.length != states.length) { 220 | console.log('@@@@@ Mega data & Mega query / selector mismatch, exit!'); 221 | process.exit(1); 222 | } 223 | 224 | let trigger = false; 225 | for (let i = 0; i < states.length; i++) { 226 | let now = parseInt((new Date()).getTime() / 1000); 227 | let now_str = (new Date()).toTimeString().split(' ')[0]; 228 | if (i == 0 && debug) console.log(`----- heartbeatMega ${now_str} ...`); 229 | let isDeviated = deviated(data[i], states[i].lastPrice, states[i].deviation); 230 | let isExpired = now > states[i].lastUpdated + states[i].windowSize; 231 | if (!isDeviated && !isExpired) { 232 | continue; 233 | } else if (isDeviated) { 234 | console.log(`+++++ Mega Stream[${i}] ${states[i].selector} ${now_str} d(${data[i]}), beyond last data (${states[i].lastPrice}) ±${states[i].deviation / 1000}, Deviation trigger`); 235 | trigger = true; 236 | } else if (isExpired) { 237 | console.log(`+++++ Mega Stream[${i}] ${states[i].selector} ${now_str} d(${data[i]}), last data (${states[i].lastPrice}) outdated, Timer trigger`); 238 | trigger = true; 239 | } 240 | } 241 | if (trigger) { 242 | await pullTriggerMega(debug); 243 | setTimeout(heartbeatMega, config.heartbeat); 244 | } else { 245 | setTimeout(heartbeatMega, config.heartbeat); 246 | } 247 | } 248 | 249 | function errorHandler(e) { 250 | console.log(e); 251 | let now = (new Date()).toTimeString().split(' ')[0]; 252 | console.log(`@@@@@ Error caught on ${now}, preparing for a restart...`); 253 | setTimeout(() => { 254 | process.exit(1) 255 | }, 3000) 256 | } 257 | 258 | process.on('uncaughtException', errorHandler); 259 | process.on('unhandledRejection', errorHandler); 260 | 261 | 262 | heartbeatMega(); 263 | -------------------------------------------------------------------------------- /scripts/guardian_heco.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const BN = require('bignumber.js'); 3 | const fetch = require('node-fetch'); 4 | const jp = require('jsonpath'); 5 | const Web3 = require('web3'); 6 | const config = require('./config_heco'); 7 | const web3 = new Web3(new Web3.providers.HttpProvider(config.httpProvider)); 8 | const privateKey = '0x' + process.env.PK; 9 | // streams' state 10 | const states = []; 11 | var streamsManager = null; 12 | var megaStream = null; 13 | var errCnt = 0; 14 | 15 | async function init(debug) { 16 | assert(privateKey.length == 66, 17 | "Please export hex-formatted private key into env without leading '0x'"); 18 | 19 | streamsManager = new web3.eth.Contract(config.managerABI, config.coingeckoStreamsManagerAddr); 20 | streamsManager.address = config.coingeckoStreamsManagerAddr; 21 | let sortedStreams = await streamsManager.methods.sortedStreams().call(); 22 | if (sortedStreams.length == 0) { 23 | console.log('@@@@@@ No stream to watch, exit!'); 24 | process.exit(1); 25 | } 26 | 27 | let megaStreamAddr = await streamsManager.methods._streams(0).call(); 28 | megaStream = new web3.eth.Contract(config.megaStreamABI, megaStreamAddr); 29 | megaStream.address = megaStreamAddr; 30 | megaStream.decimal = await megaStream.methods.decimal().call(); 31 | 32 | for (let i = 0; i < sortedStreams.length; i++) { 33 | let stream = new web3.eth.Contract(config.streamABI, sortedStreams[i]); 34 | stream.address = sortedStreams[i]; 35 | let state = { 36 | stream: stream, 37 | source: await stream.methods.source().call(), 38 | selector: await stream.methods.selector().call(), 39 | windowSize: parseInt(await stream.methods.windowSize().call()), 40 | deviation: parseInt(await stream.methods.deviation().call()), 41 | decimal: parseInt(await stream.methods.decimal().call()), 42 | lastUpdated: 0, 43 | lastPrice: BN(0), 44 | } 45 | let len = parseInt(await stream.methods.numPoints().call()); 46 | if (len > 0) { 47 | let last = await stream.methods.latestResult().call(); 48 | state.lastPrice = BN(last._lastPrice); 49 | state.lastUpdated = parseInt(last._lastUpdatedTime); 50 | } 51 | states.push(state); 52 | } 53 | if (debug) console.log('+++++ streams inited ...'); 54 | } 55 | 56 | async function sync() { 57 | for (let i = 0; i < states.length; i++) { 58 | states[i].deviation = parseInt(await states[i].stream.methods.deviation().call()); 59 | let last = await states[i].stream.methods.latestResult().call(); 60 | states[i].lastPrice = BN(last._lastPrice); 61 | states[i].lastUpdated = parseInt(last._lastUpdatedTime); 62 | } 63 | } 64 | 65 | // Normalize selector string to equivalent format in case of special characters. 66 | // e.g. '$.huobi-token.usd' => '$["huobi-token"]["usd"]' 67 | function normalizeSelector(selector) { 68 | if (selector.indexOf('-') == -1) return selector; 69 | return selector 70 | .split('.') 71 | .map((val, i) => { 72 | if (i == 0) return val; 73 | return '[\"' + val + '\"]'; 74 | }) 75 | .join(''); 76 | } 77 | 78 | // Sort response json by object keys. This is to normalize the jsonpath 79 | // behavior between client software and this guardian bot. 80 | function normalizeResponseJson(respJson) { 81 | return Object.keys(respJson).sort().reduce(function (result, key) { 82 | result[key] = respJson[key]; 83 | return result; 84 | }, {}); 85 | } 86 | 87 | async function queryCoingeckoStreamsData(debug = false) { 88 | let ret = []; 89 | let resp = await fetch(config.coingeckoMegaSource); 90 | let respJson = await resp.json(); 91 | for (let i = 0; i < states.length; i++) { 92 | let data = jp.value(respJson, normalizeSelector(states[i].selector)); 93 | data = BN(data).times(BN(10).pow(states[i].decimal)); 94 | ret.push(data); 95 | if (debug) { 96 | console.log(`+++++ coingecko ${states[i].selector}: ${data}`); 97 | } 98 | } 99 | return ret; 100 | } 101 | 102 | async function queryCoingeckoMegaData(megaDecimal) { 103 | let resp = await fetch(config.coingeckoMegaSource); 104 | let respJson = await resp.json(); 105 | respJson = normalizeResponseJson(respJson); 106 | let data = jp.query(respJson, config.coingeckoMegaSelector); 107 | return data.map((val) => { 108 | return BN(val).times(BN(10).pow(megaDecimal)) 109 | }); 110 | } 111 | 112 | // Returns true if Bignumber p1 is beyond the upper/lower threshold of Bignumber p0. 113 | function deviated(p1, p0, threshold) { 114 | if (threshold == 0) return false; 115 | return p1.gt(BN(1000).plus(threshold).div(1000).times(p0)) || p1.lt(BN(1000).minus(threshold).div(1000).times(p0)); 116 | } 117 | 118 | function sleep(ms) { 119 | return new Promise((resolve, reject) => { 120 | setTimeout(() => { 121 | resolve(ms) 122 | }, ms) 123 | }) 124 | } 125 | 126 | async function pullTriggerStream(state, debug) { 127 | let callData = state.stream.methods.pullTrigger().encodeABI(); 128 | // let estimatedGas = await state.stream.methods.pullTrigger().estimateGas({gas: config.triggerMaxGas}); 129 | let txObj = await web3.eth.accounts.signTransaction({ 130 | to: state.stream.address, 131 | data: callData, 132 | value: '0', 133 | gas: config.triggerMaxGas 134 | }, privateKey); 135 | await web3.eth.sendSignedTransaction(txObj.rawTransaction) 136 | .on('confirmation', function(confirmationNumber, receipt) { 137 | // Fired for every confirmation up to the 12th confirmation (0-indexed). We treat 2 confirmations as finalized state. 138 | if (confirmationNumber == 1) { 139 | if (debug) { 140 | console.log(`+++++ ${state.selector} tx ${receipt.transactionHash} 2 confirmations, gasUsed ${receipt.gasUsed}`); 141 | } 142 | } 143 | }) 144 | .on('error', function(err, receipt) { 145 | if (debug) { 146 | console.log(`@@@@@ Error (${errCnt}): tx ${receipt.transactionHash} error, gasUsed ${receipt.gasUsed}`); 147 | } 148 | if (++errCnt > 5) { 149 | states = []; // re-Init 150 | errCnt = 0; 151 | } 152 | }); 153 | } 154 | 155 | async function pullTriggerMega(debug) { 156 | let callData = megaStream.methods.pullTrigger().encodeABI(); 157 | // let estimatedGas = await state.stream.methods.pullTrigger().estimateGas({gas: config.triggerMaxGas}); 158 | let txObj = await web3.eth.accounts.signTransaction({ 159 | to: megaStream.address, 160 | data: callData, 161 | value: '0', 162 | gas: config.triggerMaxGas, 163 | gasPrice: web3.utils.toWei(Number(config.gasPriceGwei).toString(), 'Gwei') 164 | }, privateKey); 165 | await web3.eth.sendSignedTransaction(txObj.rawTransaction) 166 | .on('confirmation', function(confirmationNumber, receipt) { 167 | // Fired for every confirmation up to the 12th confirmation (0-indexed). We treat 2 confirmations as finalized state. 168 | if (confirmationNumber == 1) { 169 | if (debug) { 170 | console.log(`+++++ tx ${receipt.transactionHash} 2 confirmations, gasUsed ${receipt.gasUsed}`); 171 | } 172 | } 173 | }) 174 | .on('error', function(err, receipt) { 175 | if (debug) { 176 | console.log(`@@@@@ Error (${errCnt}): tx ${receipt.transactionHash} error, gasUsed ${receipt.gasUsed}`); 177 | } 178 | if (++errCnt > 5) { 179 | states = []; // re-Init 180 | errCnt = 0; 181 | } 182 | }); 183 | } 184 | 185 | async function heartbeatStreams(debug = process.env.DEBUG) { 186 | if (states.length == 0) { 187 | await init(debug); 188 | } else { 189 | await sync(); 190 | } 191 | 192 | let data = await queryCoingeckoStreamsData(); 193 | for (let i = 0; i < states.length; i++) { 194 | let now = parseInt((new Date()).getTime() / 1000); 195 | let now_str = (new Date()).toTimeString().split(' ')[0]; 196 | if (i == 0 && debug) console.log(`----- heartbeatStreams ${now_str} ...`); 197 | let isDeviated = deviated(data[i], states[i].lastPrice, states[i].deviation); 198 | let isExpired = now > states[i].lastUpdated + states[i].windowSize; 199 | if (!isDeviated && !isExpired) { 200 | continue; 201 | } else if (isDeviated) { 202 | console.log(`+++++ Stream ${states[i].selector} ${now_str} d(${data[i]}), beyond last data (${states[i].lastPrice}) ±${states[i].deviation / 1000}, Deviation trigger`); 203 | } else if (isExpired) { 204 | console.log(`+++++ Stream ${states[i].selector} ${now_str} d(${data[i]}), last data (${states[i].lastPrice}) outdated, Timer trigger`); 205 | } 206 | await pullTriggerStream(states[i], debug); 207 | } 208 | setTimeout(heartbeatStreams, config.heartbeat); 209 | } 210 | 211 | async function heartbeatMega(debug = process.env.DEBUG) { 212 | if (states.length == 0) { 213 | await init(debug); 214 | } else { 215 | await sync(); 216 | } 217 | 218 | let data = await queryCoingeckoMegaData(megaStream.decimal); 219 | if (data.length != states.length) { 220 | console.log('@@@@@ Mega data & Mega query / selector mismatch, exit!'); 221 | process.exit(1); 222 | } 223 | 224 | let trigger = false; 225 | for (let i = 0; i < states.length; i++) { 226 | let now = parseInt((new Date()).getTime() / 1000); 227 | let now_str = (new Date()).toTimeString().split(' ')[0]; 228 | if (i == 0 && debug) console.log(`----- heartbeatMega ${now_str} ...`); 229 | let isDeviated = deviated(data[i], states[i].lastPrice, states[i].deviation); 230 | let isExpired = now > states[i].lastUpdated + states[i].windowSize; 231 | if (!isDeviated && !isExpired) { 232 | continue; 233 | } else if (isDeviated) { 234 | console.log(`+++++ Mega Stream[${i}] ${states[i].selector} ${now_str} d(${data[i]}), beyond last data (${states[i].lastPrice}) ±${states[i].deviation / 1000}, Deviation trigger`); 235 | trigger = true; 236 | } else if (isExpired) { 237 | console.log(`+++++ Mega Stream[${i}] ${states[i].selector} ${now_str} d(${data[i]}), last data (${states[i].lastPrice}) outdated, Timer trigger`); 238 | trigger = true; 239 | } 240 | } 241 | if (trigger) { 242 | await pullTriggerMega(debug); 243 | setTimeout(heartbeatMega, config.heartbeat); 244 | } else { 245 | setTimeout(heartbeatMega, config.heartbeat); 246 | } 247 | } 248 | 249 | function errorHandler(e) { 250 | console.log(e); 251 | let now = (new Date()).toTimeString().split(' ')[0]; 252 | console.log(`@@@@@ Error caught on ${now}, preparing for a restart...`); 253 | setTimeout(() => { 254 | process.exit(1) 255 | }, 3000) 256 | } 257 | 258 | process.on('uncaughtException', errorHandler); 259 | process.on('unhandledRejection', errorHandler); 260 | 261 | 262 | heartbeatMega(); 263 | -------------------------------------------------------------------------------- /contracts/DOSPayment.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | contract ERC20 { 4 | function balanceOf(address who) public view returns (uint); 5 | function decimals() public view returns (uint); 6 | function transfer(address to, uint value) public returns (bool); 7 | function transferFrom(address from, address to, uint value) public returns (bool); 8 | } 9 | 10 | contract DOSAddressBridgeInterface { 11 | function getProxyAddress() public view returns (address); 12 | } 13 | 14 | contract DOSPayment { 15 | enum ServiceType { 16 | SystemRandom, 17 | UserRandom, 18 | UserQuery 19 | } 20 | 21 | struct FeeList { 22 | // ServiceType => serviceFee 23 | mapping(uint => uint) serviceFee; 24 | uint submitterCut; 25 | uint guardianFee; 26 | } 27 | 28 | struct Payment { 29 | address payer; 30 | address tokenAddr; 31 | uint serviceType; 32 | uint amount; 33 | } 34 | 35 | address public admin; 36 | // payer addr => payment token addr 37 | mapping(address => address) public paymentMethods; 38 | // tokenAddr => feeList 39 | mapping(address => FeeList) public feeLists; 40 | // requestID => Payment 41 | mapping(uint => Payment) public payments; 42 | // node address => {tokenAddr => amount} 43 | mapping(address => mapping(address => uint)) private _balances; 44 | 45 | uint constant public defaultSubmitterCut = 4; 46 | uint constant public defaultSystemRandomFee = 5 * 1e18; // 5 tokens 47 | uint constant public defaultUserRandomFee = 5 * 1e18; // 5 tokens 48 | uint constant public defaultUserQueryFee = 5 * 1e18; // 5 tokens 49 | uint constant public defaultGuardianFee = 5 * 1e18; // 5 tokens 50 | 51 | address public guardianFundsAddr; 52 | address public guardianFundsTokenAddr; 53 | address public bridgeAddr; 54 | address public defaultTokenAddr; 55 | 56 | event UpdatePaymentAdmin(address oldAdmin, address newAdmin); 57 | event LogChargeServiceFee(address payer, address tokenAddr, uint requestID, uint serviceType, uint fee); 58 | event LogRefundServiceFee(address payer, address tokenAddr, uint requestID, uint serviceType, uint fee); 59 | event LogRecordServiceFee(address nodeAddr, address tokenAddr, uint requestID, uint serviceType, uint fee, bool isSubmitter); 60 | event LogClaimGuardianFee(address nodeAddr, address tokenAddr, uint feeForSubmitter, address sender); 61 | 62 | modifier onlyFromProxy { 63 | require(msg.sender == DOSAddressBridgeInterface(bridgeAddr).getProxyAddress(), "not-from-dos-proxy"); 64 | _; 65 | } 66 | 67 | modifier onlySupportedToken(address tokenAddr) { 68 | require(isSupportedToken(tokenAddr), "not-supported-token-addr"); 69 | _; 70 | } 71 | 72 | modifier hasPayment(uint requestID) { 73 | require(payments[requestID].amount != 0, "no-fee-amount"); 74 | require(payments[requestID].payer != address(0x0), "no-payer-info"); 75 | require(payments[requestID].tokenAddr != address(0x0), "no-fee-token-info"); 76 | _; 77 | } 78 | 79 | constructor(address _bridgeAddr, address _guardianFundsAddr, address _tokenAddr) public { 80 | initialize(_bridgeAddr, _guardianFundsAddr, _tokenAddr); 81 | } 82 | 83 | function initialize(address _bridgeAddr, address _guardianFundsAddr, address _tokenAddr) public { 84 | require(bridgeAddr == address(0x0) && defaultTokenAddr == address(0x0), "already-initialized"); 85 | 86 | admin = msg.sender; 87 | bridgeAddr = _bridgeAddr; 88 | guardianFundsAddr = _guardianFundsAddr; 89 | guardianFundsTokenAddr = _tokenAddr; 90 | defaultTokenAddr = _tokenAddr; 91 | 92 | FeeList storage feeList = feeLists[_tokenAddr]; 93 | feeList.serviceFee[uint(ServiceType.SystemRandom)] = defaultSystemRandomFee; 94 | feeList.serviceFee[uint(ServiceType.UserRandom)] = defaultUserRandomFee; 95 | feeList.serviceFee[uint(ServiceType.UserQuery)] = defaultUserQueryFee; 96 | feeList.submitterCut = defaultSubmitterCut; 97 | feeList.guardianFee = defaultGuardianFee; 98 | } 99 | 100 | function isSupportedToken(address tokenAddr) public view returns(bool) { 101 | if (tokenAddr == address(0x0)) return false; 102 | if (feeLists[tokenAddr].serviceFee[uint(ServiceType.SystemRandom)] == 0) return false; 103 | if (feeLists[tokenAddr].serviceFee[uint(ServiceType.UserRandom)] == 0) return false; 104 | if (feeLists[tokenAddr].serviceFee[uint(ServiceType.UserQuery)] == 0) return false; 105 | return true; 106 | } 107 | 108 | modifier onlyAdmin { 109 | require(msg.sender == admin, "onlyAdmin"); 110 | _; 111 | } 112 | 113 | function setAdmin(address newAdmin) public onlyAdmin { 114 | require(newAdmin != address(0)); 115 | emit UpdatePaymentAdmin(admin, newAdmin); 116 | admin = newAdmin; 117 | } 118 | 119 | function setPaymentMethod(address payer, address tokenAddr) public onlySupportedToken(tokenAddr) { 120 | paymentMethods[payer] = tokenAddr; 121 | } 122 | 123 | function setServiceFee(address tokenAddr, uint serviceType, uint fee) public onlyAdmin { 124 | require(tokenAddr != address(0x0), "not-valid-token-addr"); 125 | feeLists[tokenAddr].serviceFee[serviceType] = fee; 126 | } 127 | 128 | function setGuardianFee(address tokenAddr, uint fee) public onlyAdmin { 129 | require(tokenAddr != address(0x0), "not-valid-token-addr"); 130 | feeLists[tokenAddr].guardianFee = fee; 131 | } 132 | 133 | function setFeeDividend(address tokenAddr, uint submitterCut) public onlyAdmin { 134 | require(tokenAddr != address(0x0), "not-valid-token-addr"); 135 | feeLists[tokenAddr].submitterCut = submitterCut; 136 | } 137 | 138 | function setGuardianFunds(address fundsAddr, address tokenAddr) public onlyAdmin onlySupportedToken(tokenAddr) { 139 | guardianFundsAddr = fundsAddr; 140 | guardianFundsTokenAddr = tokenAddr; 141 | } 142 | 143 | function getServiceTypeFee(address tokenAddr, uint serviceType) public view returns(uint) { 144 | require(tokenAddr != address(0x0) && feeLists[tokenAddr].guardianFee != 0 && feeLists[tokenAddr].submitterCut != 0, 145 | "not-valid-token-addr"); 146 | return feeLists[tokenAddr].serviceFee[serviceType]; 147 | } 148 | 149 | function hasServiceFee(address payer, uint serviceType) public view returns (bool) { 150 | if (payer == DOSAddressBridgeInterface(bridgeAddr).getProxyAddress()) return true; 151 | address tokenAddr = paymentMethods[payer]; 152 | // Get fee by tokenAddr and serviceType 153 | uint fee = feeLists[tokenAddr].serviceFee[serviceType]; 154 | return ERC20(tokenAddr).balanceOf(payer) >= fee; 155 | } 156 | 157 | function chargeServiceFee(address payer, uint requestID, uint serviceType) public onlyFromProxy { 158 | address tokenAddr = paymentMethods[payer]; 159 | // Get fee by tokenAddr and serviceType 160 | uint fee = feeLists[tokenAddr].serviceFee[serviceType]; 161 | payments[requestID] = Payment(payer, tokenAddr, serviceType, fee); 162 | emit LogChargeServiceFee(payer, tokenAddr, requestID, serviceType, fee); 163 | ERC20(tokenAddr).transferFrom(payer, address(this), fee); 164 | } 165 | 166 | function refundServiceFee(uint requestID) public onlyAdmin hasPayment(requestID) { 167 | uint fee = payments[requestID].amount; 168 | uint serviceType = payments[requestID].serviceType; 169 | address tokenAddr = payments[requestID].tokenAddr; 170 | address payer = payments[requestID].payer; 171 | delete payments[requestID]; 172 | emit LogRefundServiceFee(payer, tokenAddr, requestID, serviceType, fee); 173 | ERC20(tokenAddr).transfer(payer, fee); 174 | } 175 | 176 | function recordServiceFee(uint requestID, address submitter, address[] memory workers) public onlyFromProxy hasPayment(requestID) { 177 | address tokenAddr = payments[requestID].tokenAddr; 178 | uint feeUnit = payments[requestID].amount / 10; 179 | uint serviceType = payments[requestID].serviceType; 180 | delete payments[requestID]; 181 | 182 | FeeList storage feeList = feeLists[tokenAddr]; 183 | uint feeForSubmitter = feeUnit * feeList.submitterCut; 184 | _balances[submitter][tokenAddr] += feeForSubmitter; 185 | emit LogRecordServiceFee(submitter, tokenAddr, requestID, serviceType, feeForSubmitter, true); 186 | uint feeForWorker = feeUnit * (10 - feeList.submitterCut) / (workers.length - 1); 187 | for (uint i = 0; i < workers.length; i++) { 188 | if (workers[i] != submitter) { 189 | _balances[workers[i]][tokenAddr] += feeForWorker; 190 | emit LogRecordServiceFee(workers[i], tokenAddr, requestID, serviceType, feeForWorker, false); 191 | } 192 | } 193 | } 194 | 195 | function claimGuardianReward(address guardianAddr) public onlyFromProxy { 196 | require(guardianFundsAddr != address(0x0), "not-valid-guardian-fund-addr"); 197 | require(guardianFundsTokenAddr != address(0x0), "not-valid-guardian-token-addr"); 198 | uint fee = feeLists[guardianFundsTokenAddr].guardianFee; 199 | emit LogClaimGuardianFee(guardianAddr, guardianFundsTokenAddr, fee, msg.sender); 200 | ERC20(guardianFundsTokenAddr).transferFrom(guardianFundsAddr, guardianAddr,fee); 201 | } 202 | 203 | // @dev: node runners call to withdraw recorded service fees. 204 | function nodeClaim() public returns(uint) { 205 | return nodeClaim(msg.sender, defaultTokenAddr, msg.sender); 206 | } 207 | 208 | // @dev: node runners call to withdraw recorded service fees to specified address. 209 | function nodeClaim(address to) public returns(uint) { 210 | return nodeClaim(msg.sender, defaultTokenAddr, to); 211 | } 212 | 213 | function nodeClaim(address nodeAddr, address tokenAddr, address to) internal returns(uint) { 214 | uint amount = _balances[nodeAddr][tokenAddr]; 215 | if (amount != 0) { 216 | delete _balances[nodeAddr][tokenAddr]; 217 | ERC20(tokenAddr).transfer(to, amount); 218 | } 219 | return amount; 220 | } 221 | 222 | function nodeFeeBalance(address nodeAddr) public view returns (uint) { 223 | return nodeFeeBalance(nodeAddr, defaultTokenAddr); 224 | } 225 | 226 | function nodeFeeBalance(address nodeAddr, address tokenAddr) public view returns (uint) { 227 | return _balances[nodeAddr][tokenAddr]; 228 | } 229 | 230 | function paymentInfo(uint requestID) public view returns (address, uint) { 231 | Payment storage payment = payments[requestID]; 232 | return (payment.tokenAddr, payment.amount); 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /contracts/lib/StringUtils.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | // Util functions caller to process strings. 4 | library StringUtils { 5 | uint constant UINT256MAX = ~uint(0); 6 | 7 | // A decimal byte to uint. Return value of 10 indicating invalid input. 8 | function byte2Uint(byte b) internal pure returns(uint8) { 9 | if (b >= '0' && b <= '9') { 10 | return uint8(b) - 48; // '0' 11 | } 12 | // Indicating invalid input. 13 | return 10; 14 | } 15 | // Return value of 16 indicating invalid input. 16 | function hexByte2Uint(byte b) internal pure returns(uint8) { 17 | if (b >= '0' && b <= '9') { 18 | return uint8(b) - 48; // '0' 19 | } else if (b >= 'A' && b <= 'F') { 20 | return uint8(b) - 55; 21 | } else if (b >= 'a' && b <= 'f') { 22 | return uint8(b) - 87; 23 | } 24 | // Indicating invalid input. 25 | return 16; 26 | } 27 | 28 | /// StringToXXX helpers. 29 | 30 | // A decimal string (charset c in [0-9]) to uint. Like atoi(), 31 | // 1. processing stops once encountering character not in charset c. 32 | // 2. returns UINT256MAX when overflow. 33 | function str2Uint(string memory a) internal pure returns(uint) { 34 | return bytes2Uint(bytes(a)); 35 | } 36 | 37 | // A decimal bytes string (charset c in [0-9]) to uint. Like atoi(). 38 | function bytes2Uint(bytes memory b) internal pure returns(uint) { 39 | uint res = 0; 40 | for (uint i = 0; i < b.length; i++) { 41 | uint8 tmp = byte2Uint(b[i]); 42 | if (tmp >= 10) { 43 | return res; 44 | } else { 45 | // Overflow. 46 | if (res >= UINT256MAX / 10) { 47 | return UINT256MAX; 48 | } 49 | res = res * 10 + tmp; 50 | } 51 | } 52 | return res; 53 | } 54 | 55 | // Hex string (charset c in [0-9A-Za-z]) to uint. Like atoi(), 56 | // 1. processing stops once encountering character not in charset c. 57 | // 2. returns UINT256MAX when overflow. 58 | function hexStr2Uint(string memory a) internal pure returns(uint) { 59 | bytes memory b = bytes(a); 60 | uint res = 0; 61 | uint i = 0; 62 | if (b.length >= 2 && b[0] == '0' && (b[1] == 'x' || b[1] == 'X')) { 63 | i += 2; 64 | } 65 | for (; i < b.length; i++) { 66 | uint tmp = hexByte2Uint(b[i]); 67 | if (tmp >= 16) { 68 | return res; 69 | } else { 70 | // Overflow. 71 | if (res >= UINT256MAX / 16) { 72 | return UINT256MAX; 73 | } 74 | res = res * 16 + tmp; 75 | } 76 | } 77 | return res; 78 | } 79 | 80 | // Input: 20-byte hex string without or with 0x/0X prefix (40 characters or 42 characters) 81 | // Example: '0x0e7ad63d2a305a7b9f46541c386aafbd2af6b263' => address(0x0e7ad63d2a305a7b9f46541c386aafbd2af6b263) 82 | // address is of uint160. 83 | function str2Addr(string memory a) internal pure returns(address) { 84 | bytes memory b = bytes(a); 85 | require(b.length == 40 || b.length == 42, "Invalid input, should be 20-byte hex string"); 86 | uint i = 0; 87 | if (b.length == 42) { 88 | i += 2; 89 | } 90 | 91 | uint160 res = 0; 92 | for (; i < b.length; i += 2) { 93 | res *= 256; 94 | 95 | uint160 b1 = uint160(hexByte2Uint(b[i])); 96 | uint160 b2 = uint160(hexByte2Uint(b[i+1])); 97 | require(b1 < 16 && b2 < 16, "address string with invalid character"); 98 | 99 | res += b1 * 16 + b2; 100 | } 101 | return address(res); 102 | } 103 | 104 | /// XXXToString() helpers. 105 | 106 | // Example: 12 -> 'c' (without 0x/0X prefix). 107 | function uint2HexStr(uint x) internal pure returns(string memory) { 108 | if (x == 0) return '0'; 109 | 110 | uint j = x; 111 | uint len; 112 | while (j != 0) { 113 | len++; 114 | j /= 16; 115 | } 116 | 117 | bytes memory b = new bytes(len); 118 | uint k = len - 1; 119 | while (x != 0) { 120 | uint8 curr = uint8(x & 0xf); 121 | b[k--] = curr > 9 ? byte(55 + curr) : byte(48 + curr); 122 | x /= 16; 123 | } 124 | return string(b); 125 | } 126 | 127 | // Example: 12 -> "12" 128 | function uint2Str(uint x) internal pure returns(string memory) { 129 | if (x == 0) return '0'; 130 | 131 | uint j = x; 132 | uint len; 133 | while (j != 0) { 134 | len++; 135 | j /= 10; 136 | } 137 | 138 | bytes memory b = new bytes(len); 139 | uint k = len - 1; 140 | while (x != 0) { 141 | b[k--] = byte(uint8(48 + x % 10)); 142 | x /= 10; 143 | } 144 | return string(b); 145 | } 146 | 147 | // Example: address(0x0e7ad63d2a305a7b9f46541c386aafbd2af6b263) => '0x0e7ad63d2a305a7b9f46541c386aafbd2af6b263' 148 | function addr2Str(address _addr) internal pure returns(string memory) { 149 | bytes32 value = bytes32(uint256(_addr)); 150 | bytes memory charset = "0123456789abcdef"; 151 | 152 | bytes memory str = new bytes(42); 153 | str[0] = '0'; 154 | str[1] = 'x'; 155 | for (uint i = 0; i < 20; i++) { 156 | str[2+i*2] = charset[uint8(value[i + 12] >> 4)]; 157 | str[3+i*2] = charset[uint8(value[i + 12] & 0x0f)]; 158 | } 159 | return string(str); 160 | } 161 | 162 | /// bytes/string helpers. 163 | 164 | function bytesConcat(bytes memory a, bytes memory b) internal pure returns(bytes memory) { 165 | bytes memory concated = new bytes(a.length + b.length); 166 | uint i = 0; 167 | uint k = 0; 168 | while (i < a.length) { concated[k++] = a[i++]; } 169 | i = 0; 170 | while(i < b.length) { concated[k++] = b[i++]; } 171 | return concated; 172 | } 173 | 174 | function strConcat(string memory a, string memory b) internal pure returns(string memory) { 175 | bytes memory aa = bytes(a); 176 | bytes memory bb = bytes(b); 177 | return string(bytesConcat(aa, bb)); 178 | } 179 | 180 | function bytesCompare(bytes memory a, bytes memory b) internal pure returns(int) { 181 | uint len = a.length < b.length ? a.length : b.length; 182 | for (uint i = 0; i < len; i++) { 183 | if (a[i] < b[i]) { 184 | return -1; 185 | } else if (a[i] > b[i]) { 186 | return 1; 187 | } 188 | } 189 | if (a.length < b.length) { 190 | return -1; 191 | } else if (a.length > b.length) { 192 | return 1; 193 | } else { 194 | return 0; 195 | } 196 | } 197 | 198 | // "abd" > "abcde" 199 | function strCompare(string memory a, string memory b) internal pure returns(int) { 200 | bytes memory aa = bytes(a); 201 | bytes memory bb = bytes(b); 202 | return bytesCompare(aa, bb); 203 | } 204 | 205 | function bytesEqual(bytes memory a, bytes memory b) internal pure returns(bool) { 206 | return (a.length == b.length) && (bytesCompare(a, b) == 0); 207 | } 208 | 209 | function strEqual(string memory a, string memory b) internal pure returns(bool) { 210 | bytes memory aa = bytes(a); 211 | bytes memory bb = bytes(b); 212 | return bytesEqual(aa, bb); 213 | } 214 | 215 | // Return the index of needle's first occurrance in haystack; or return index of the first byte 216 | // after haystack if not found. 217 | // Useful in case of parsing float string "123.45". 218 | // Example: 219 | // indexOf('123', '') => 0 220 | // indexOf('', '45') => 1 221 | // indexOf('123', '1234') => 3 222 | // indexOf('12345', '34') => 2 223 | // indexOf('123.45', '.') => 3 224 | function indexOf(string memory haystack, string memory needle) internal pure returns(uint) { 225 | bytes memory b_haystack = bytes(haystack); 226 | bytes memory b_needle = bytes(needle); 227 | return indexOf(b_haystack, b_needle); 228 | } 229 | 230 | function indexOf(bytes memory haystack, bytes memory needle) internal pure returns(uint) { 231 | if (needle.length == 0) { 232 | return 0; 233 | } else if (haystack.length < needle.length) { 234 | return haystack.length; 235 | } 236 | // Instead of O(haystack.length x needle.length), saving gas using KMP: 237 | // O(haystack.length + needle.length) 238 | uint[] memory pi = new uint[](needle.length + 1); 239 | pi[1] = 0; 240 | uint k = 0; 241 | uint q = 0; 242 | // KMP pre-processing 243 | for(q = 2; q <= needle.length; q++) { 244 | while(k > 0 && needle[k] != needle[q-1]) { 245 | k = pi[k]; 246 | } 247 | if(needle[k] == needle[q-1]) { 248 | k++; 249 | } 250 | pi[q] = k; 251 | } 252 | // KMP matching 253 | q = 0; 254 | for(uint i = 0; i < haystack.length; i++) { 255 | while(q > 0 && needle[q] != haystack[i]) { 256 | q = pi[q]; 257 | } 258 | if(needle[q] == haystack[i]) { 259 | q++; 260 | } 261 | // Match 262 | if(q == needle.length) { 263 | return i - q + 1; 264 | } 265 | } 266 | // No match 267 | return haystack.length; 268 | } 269 | 270 | // subStr("123456789", 2, 5) => "34567" 271 | // subStr("123456789", 2, 100) => "3456789" 272 | // [start, start + len), index starting from 0. 273 | function subStr(bytes memory a, uint start, uint len) internal pure returns(bytes memory) { 274 | if (start >= a.length) return ""; 275 | 276 | if (a.length - start < len) len = a.length - start; 277 | bytes memory res = new bytes(len); 278 | for (uint i = 0; i < len; i++) { 279 | res[i] = a[start + i]; 280 | } 281 | return res; 282 | } 283 | 284 | // string num = "123.4567"; 285 | // subStr(num, indexOf(num, '.') + 1) => "4567" 286 | function subStr(bytes memory a, uint start) internal pure returns(bytes memory) { 287 | return subStr(a, start, a.length); 288 | } 289 | 290 | function subStr(string memory a, uint start, uint len) internal pure returns(string memory) { 291 | bytes memory aa = bytes(a); 292 | return string(subStr(aa, start, len)); 293 | } 294 | 295 | function subStr(string memory a, uint start) internal pure returns(string memory) { 296 | bytes memory aa = bytes(a); 297 | return string(subStr(aa, start)); 298 | } 299 | 300 | function count(bytes memory str, bytes memory delimiter) internal pure returns(uint) { 301 | uint cnt = 0; 302 | while(str.length > 0) { 303 | uint idx = indexOf(str, delimiter); 304 | if (idx != str.length) { 305 | cnt++; 306 | str = subStr(str, idx + delimiter.length); 307 | } else { 308 | return cnt; 309 | } 310 | } 311 | return cnt; 312 | } 313 | 314 | function count(string memory str, string memory delimiter) internal pure returns(uint) { 315 | return count(bytes(str), bytes(delimiter)); 316 | } 317 | 318 | function split(string memory str, string memory delimiter) internal pure returns(string[] memory) { 319 | bytes[] memory r = split(bytes(str), bytes(delimiter)); 320 | string[] memory ret = new string[](r.length); 321 | for (uint i = 0; i < r.length; i++) { 322 | ret[i] = string(r[i]); 323 | } 324 | return ret; 325 | } 326 | 327 | function split(bytes memory str, bytes memory delimiter) internal pure returns(bytes[] memory) { 328 | uint len = count(str, delimiter) + 1; 329 | bytes[] memory ret = new bytes[](len); 330 | for (uint i = 0; i < len; i++) { 331 | uint idx = indexOf(str, delimiter); 332 | ret[i] = subStr(str, 0, idx); 333 | str = subStr(str, idx + delimiter.length); 334 | } 335 | return ret; 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /test/dosproxy_test.js: -------------------------------------------------------------------------------- 1 | const DOSProxyMock = artifacts.require("DOSProxyMock"); 2 | const truffleAssert = require('truffle-assertions'); 3 | contract("DOSProxy Test", async accounts => { 4 | it("test unregister node from pendingNodeList", async () => { 5 | let dosproxy = await DOSProxyMock.new() 6 | let tx = await dosproxy.registerNewNode({ from: accounts[0] }); 7 | let tx2 = await dosproxy.registerNewNode({ from: accounts[1] }); 8 | truffleAssert.eventEmitted(tx, 'LogRegisteredNewPendingNode', (ev) => { 9 | return ev.node === accounts[0]; 10 | }); 11 | truffleAssert.eventEmitted(tx2, 'LogInsufficientPendingNode', (ev) => { 12 | return ev.numPendingNodes.toNumber() === web3.utils.toBN(2).toNumber(); 13 | }); 14 | var node = await dosproxy.pendingNodeList.call(accounts[0]); 15 | assert.equal(node, accounts[1], 16 | "After register, account 0 should point to account 1"); 17 | 18 | let tx3 = await dosproxy.unregisterNode({ from: accounts[0] }); 19 | var node = await dosproxy.pendingNodeList.call(accounts[0]); 20 | assert.equal(node, 0, 21 | "After register, account 0 should not be in the list"); 22 | truffleAssert.eventEmitted(tx3, 'LogUnRegisteredNewPendingNode', (ev) => { 23 | return ev.node === accounts[0]; 24 | }); 25 | }); 26 | it("test unregister node from pendingGroup", () => { 27 | let dosproxy; 28 | let groupid1; 29 | let groupid2; 30 | let groupid3; 31 | let group1Mem1; 32 | let group1Mem2; 33 | let group1Mem3; 34 | let group2Mem1; 35 | let group2Mem2; 36 | let group2Mem3; 37 | let group3Mem1; 38 | let group3Mem2; 39 | let group3Mem3; 40 | let numPendingNodes 41 | let numPendingGroups 42 | let workingGroupIdsLength 43 | let expiredWorkingGroupIdsLength 44 | let tx 45 | let tx2 46 | let tx3 47 | return DOSProxyMock.new() 48 | .then(async (instance) => { 49 | dosproxy = instance; 50 | await dosproxy.registerNewNode({ from: accounts[0] }); 51 | await dosproxy.registerNewNode({ from: accounts[1] }); 52 | await dosproxy.registerNewNode({ from: accounts[2] }); 53 | await dosproxy.registerNewNode({ from: accounts[3] }); 54 | await dosproxy.registerNewNode({ from: accounts[4] }); 55 | await dosproxy.registerNewNode({ from: accounts[5] }); 56 | await dosproxy.registerNewNode({ from: accounts[6] }); 57 | await dosproxy.registerNewNode({ from: accounts[7] }); 58 | tx = await dosproxy.registerNewNode({ from: accounts[8] }); 59 | await dosproxy.registerNewNode({ from: accounts[9] }); 60 | 61 | numPendingNodes = await dosproxy.numPendingNodes.call(); 62 | numPendingGroups = await dosproxy.numPendingGroups.call(); 63 | workingGroupIdsLength = await dosproxy.workingGroupIdsLength.call() 64 | expiredWorkingGroupIdsLength = await dosproxy.expiredWorkingGroupIdsLength.call() 65 | 66 | truffleAssert.eventNotEmitted(tx, 'LogError'); 67 | assert.equal(numPendingNodes.toNumber(), 10, 68 | "After register, numPendingNodes should be 10"); 69 | assert.equal(numPendingGroups.toNumber(), 0, 70 | "After register, numPendingGroups should be 0"); 71 | assert.equal(workingGroupIdsLength.toNumber(), 0, 72 | "Before signalBootstrap, length of workingGroupIds should be 0"); 73 | assert.equal(expiredWorkingGroupIdsLength.toNumber(), 0, 74 | "Before signalBootstrap, length of expiredWorkingGroupIds should be 0"); 75 | return numPendingNodes; 76 | }) 77 | .then(async (numPendingNodes) => { 78 | tx = await dosproxy.signalBootstrap(1,{ from: accounts[0] }); 79 | numPendingNodes = await dosproxy.numPendingNodes.call(); 80 | numPendingGroups = await dosproxy.numPendingGroups.call(); 81 | workingGroupIdsLength = await dosproxy.workingGroupIdsLength.call() 82 | expiredWorkingGroupIdsLength = await dosproxy.expiredWorkingGroupIdsLength.call() 83 | 84 | assert.equal(numPendingNodes.toNumber(), 1, 85 | "After signalBootstrap, numPendingNodes should be 1"); 86 | assert.equal(numPendingGroups.toNumber(), 3, 87 | "After signalBootstrap, numPendingGroups should be 3"); 88 | assert.equal(workingGroupIdsLength.toNumber(), 0, 89 | "After signalBootstrap, workingGroupIds length should be 0"); 90 | assert.equal(expiredWorkingGroupIdsLength.toNumber(), 0, 91 | "After signalBootstrap, length of expiredWorkingGroupIds should be 0"); 92 | return tx; 93 | }) 94 | .then(async (res) => { 95 | tx = await dosproxy.getPastEvents( 'LogGrouping', { fromBlock: 0, toBlock: 'latest' } ) 96 | assert.equal(tx.length, 3, 97 | "After signalBootstrap, length of LogGrouping should be 3"); 98 | groupid1 = tx[0].returnValues.groupId; 99 | group1Mem1 = tx[0].returnValues.nodeId[0]; 100 | group1Mem2 = tx[0].returnValues.nodeId[1]; 101 | group1Mem3 = tx[0].returnValues.nodeId[2]; 102 | groupid2 = tx[1].returnValues.groupId; 103 | group2Mem1 = tx[1].returnValues.nodeId[0]; 104 | group2Mem2 = tx[1].returnValues.nodeId[1]; 105 | group2Mem3 = tx[1].returnValues.nodeId[2]; 106 | groupid3 = tx[2].returnValues.groupId; 107 | group3Mem1 = tx[2].returnValues.nodeId[0]; 108 | group3Mem2 = tx[2].returnValues.nodeId[1]; 109 | group3Mem3 = tx[2].returnValues.nodeId[2]; 110 | return tx; 111 | }) 112 | .then(async (res) => { 113 | await dosproxy.unregisterNode({ from: group1Mem1 }); 114 | let tx = await dosproxy.unregisterNode({ from: group2Mem2 }); 115 | return tx 116 | }) 117 | .then(async (res) => { 118 | numPendingNodes = await dosproxy.numPendingNodes.call(); 119 | numPendingGroups = await dosproxy.numPendingGroups.call(); 120 | workingGroupIdsLength = await dosproxy.workingGroupIdsLength.call() 121 | expiredWorkingGroupIdsLength = await dosproxy.expiredWorkingGroupIdsLength.call() 122 | // console.log("numPendingNodes : ",numPendingNodes.toNumber()) 123 | // console.log("numPendingGroups : ",numPendingGroups.toNumber()) 124 | // console.log("workingGroupIdsLength : ",workingGroupIdsLength.toNumber()) 125 | // console.log("expiredWorkingGroupIdsLength : ",expiredWorkingGroupIdsLength.toNumber()) 126 | }); 127 | }); 128 | it("test unregister node from workingGroup", () => { 129 | let dosproxy; 130 | let groupid1; 131 | let groupid2; 132 | let groupid3; 133 | let group1Mem1; 134 | let group1Mem2; 135 | let group1Mem3; 136 | let group2Mem1; 137 | let group2Mem2; 138 | let group2Mem3; 139 | let group3Mem1; 140 | let group3Mem2; 141 | let group3Mem3; 142 | let numPendingNodes 143 | let numPendingGroups 144 | let workingGroupIdsLength 145 | let expiredWorkingGroupIdsLength 146 | let tx 147 | let tx2 148 | let tx3 149 | return DOSProxyMock.new() 150 | .then(async (instance) => { 151 | dosproxy = instance; 152 | await dosproxy.registerNewNode({ from: accounts[0] }); 153 | await dosproxy.registerNewNode({ from: accounts[1] }); 154 | await dosproxy.registerNewNode({ from: accounts[2] }); 155 | await dosproxy.registerNewNode({ from: accounts[3] }); 156 | await dosproxy.registerNewNode({ from: accounts[4] }); 157 | await dosproxy.registerNewNode({ from: accounts[5] }); 158 | await dosproxy.registerNewNode({ from: accounts[6] }); 159 | await dosproxy.registerNewNode({ from: accounts[7] }); 160 | tx = await dosproxy.registerNewNode({ from: accounts[8] }); 161 | await dosproxy.registerNewNode({ from: accounts[9] }); 162 | 163 | numPendingNodes = await dosproxy.numPendingNodes.call(); 164 | numPendingGroups = await dosproxy.numPendingGroups.call(); 165 | workingGroupIdsLength = await dosproxy.workingGroupIdsLength.call() 166 | expiredWorkingGroupIdsLength = await dosproxy.expiredWorkingGroupIdsLength.call() 167 | 168 | truffleAssert.eventNotEmitted(tx, 'LogError'); 169 | assert.equal(numPendingNodes.toNumber(), 10, 170 | "After register, numPendingNodes should be 10"); 171 | assert.equal(numPendingGroups.toNumber(), 0, 172 | "After register, numPendingGroups should be 0"); 173 | assert.equal(workingGroupIdsLength.toNumber(), 0, 174 | "Before signalBootstrap, length of workingGroupIds should be 0"); 175 | assert.equal(expiredWorkingGroupIdsLength.toNumber(), 0, 176 | "Before signalBootstrap, length of expiredWorkingGroupIds should be 0"); 177 | return numPendingNodes; 178 | }) 179 | .then(async (numPendingNodes) => { 180 | tx = await dosproxy.signalBootstrap(1,{ from: accounts[0] }); 181 | numPendingNodes = await dosproxy.numPendingNodes.call(); 182 | numPendingGroups = await dosproxy.numPendingGroups.call(); 183 | workingGroupIdsLength = await dosproxy.workingGroupIdsLength.call() 184 | expiredWorkingGroupIdsLength = await dosproxy.expiredWorkingGroupIdsLength.call() 185 | 186 | assert.equal(numPendingNodes.toNumber(), 1, 187 | "After signalBootstrap, numPendingNodes should be 1"); 188 | assert.equal(numPendingGroups.toNumber(), 3, 189 | "After signalBootstrap, numPendingGroups should be 3"); 190 | assert.equal(workingGroupIdsLength.toNumber(), 0, 191 | "After signalBootstrap, workingGroupIds length should be 0"); 192 | assert.equal(expiredWorkingGroupIdsLength.toNumber(), 0, 193 | "After signalBootstrap, length of expiredWorkingGroupIds should be 0"); 194 | return tx; 195 | }) 196 | .then(async (res) => { 197 | tx = await dosproxy.getPastEvents( 'LogGrouping', { fromBlock: 0, toBlock: 'latest' } ) 198 | assert.equal(tx.length, 3, 199 | "After signalBootstrap, length of LogGrouping should be 3"); 200 | groupid1 = tx[0].returnValues.groupId; 201 | group1Mem1 = tx[0].returnValues.nodeId[0]; 202 | group1Mem2 = tx[0].returnValues.nodeId[1]; 203 | group1Mem3 = tx[0].returnValues.nodeId[2]; 204 | groupid2 = tx[1].returnValues.groupId; 205 | group2Mem1 = tx[1].returnValues.nodeId[0]; 206 | group2Mem2 = tx[1].returnValues.nodeId[1]; 207 | group2Mem3 = tx[1].returnValues.nodeId[2]; 208 | groupid3 = tx[2].returnValues.groupId; 209 | group3Mem1 = tx[2].returnValues.nodeId[0]; 210 | group3Mem2 = tx[2].returnValues.nodeId[1]; 211 | group3Mem3 = tx[2].returnValues.nodeId[2]; 212 | return tx; 213 | }) 214 | .then(async (res) => { 215 | var gpubKey1 = []; 216 | for(var i=0;i<4;i++){ 217 | gpubKey1.push(web3.utils.toBN(1)); 218 | } 219 | await dosproxy.registerGroupPubKey(groupid1,gpubKey1,{ from: group1Mem1 }) 220 | tx = await dosproxy.registerGroupPubKey(groupid1,gpubKey1,{ from: group1Mem2 }) 221 | 222 | var gpubKey2 = []; 223 | for(var i=0;i<4;i++){ 224 | gpubKey2.push(web3.utils.toBN(2)); 225 | } 226 | await dosproxy.registerGroupPubKey(groupid2,gpubKey2,{ from: group2Mem2 }) 227 | tx2 = await dosproxy.registerGroupPubKey(groupid2,gpubKey2,{ from: group2Mem2 }) 228 | 229 | var gpubKey3 = []; 230 | for(var i=0;i<4;i++){ 231 | gpubKey3.push(web3.utils.toBN(3)); 232 | } 233 | await dosproxy.registerGroupPubKey(groupid3,gpubKey3,{ from: group3Mem1 }) 234 | tx3 = await dosproxy.registerGroupPubKey(groupid3,gpubKey3,{ from: group3Mem2 }) 235 | 236 | return tx3; 237 | }) 238 | .then(async (tx3) => { 239 | numPendingNodes = await dosproxy.numPendingNodes.call(); 240 | numPendingGroups = await dosproxy.numPendingGroups.call(); 241 | workingGroupIdsLength = await dosproxy.workingGroupIdsLength.call() 242 | expiredWorkingGroupIdsLength = await dosproxy.expiredWorkingGroupIdsLength.call() 243 | assert.equal(numPendingNodes.toNumber(), 1, 244 | "After registerGroupPubKey, numPendingNodes should be 1"); 245 | assert.equal(numPendingGroups.toNumber(), 0, 246 | "After registerGroupPubKey, numPendingGroups should be 0"); 247 | assert.equal(workingGroupIdsLength.toNumber(), 3, 248 | "After registerGroupPubKey, length of workingGroupIds should be 3"); 249 | assert.equal(expiredWorkingGroupIdsLength.toNumber(), 0, 250 | "After registerGroupPubKey, length of expiredWorkingGroupIds should be 0"); 251 | 252 | truffleAssert.eventNotEmitted(tx, 'LogError'); 253 | truffleAssert.eventEmitted(tx, 'LogPublicKeyAccepted', (ev) => { 254 | return ev.numWorkingGroups.toNumber() === 1; 255 | }); 256 | truffleAssert.eventEmitted(tx2, 'LogPublicKeyAccepted', (ev) => { 257 | return ev.numWorkingGroups.toNumber() === 2; 258 | }); 259 | truffleAssert.eventEmitted(tx3, 'LogPublicKeyAccepted', (ev) => { 260 | return ev.numWorkingGroups.toNumber() === 3; 261 | }); 262 | 263 | return tx3; 264 | }) 265 | .then(async (res) => { 266 | await dosproxy.unregisterNode({ from: accounts[0] }); 267 | await dosproxy.unregisterNode({ from: accounts[1] }); 268 | await dosproxy.unregisterNode({ from: accounts[2] }); 269 | await dosproxy.unregisterNode({ from: accounts[3] }); 270 | await dosproxy.unregisterNode({ from: accounts[4] }); 271 | await dosproxy.unregisterNode({ from: accounts[5] }); 272 | await dosproxy.unregisterNode({ from: accounts[6] }); 273 | await dosproxy.unregisterNode({ from: accounts[7] }); 274 | await dosproxy.unregisterNode({ from: accounts[8] }); 275 | let tx = await dosproxy.unregisterNode({ from: accounts[9] }); 276 | return tx 277 | }) 278 | .then(async (res) => { 279 | numPendingNodes = await dosproxy.numPendingNodes.call(); 280 | numPendingGroups = await dosproxy.numPendingGroups.call(); 281 | workingGroupIdsLength = await dosproxy.workingGroupIdsLength.call() 282 | expiredWorkingGroupIdsLength = await dosproxy.expiredWorkingGroupIdsLength.call() 283 | // console.log("numPendingNodes : ",numPendingNodes.toNumber()) 284 | // console.log("numPendingGroups : ",numPendingGroups.toNumber()) 285 | // console.log("workingGroupIdsLength : ",workingGroupIdsLength.toNumber()) 286 | // console.log("expiredWorkingGroupIdsLength : ",expiredWorkingGroupIdsLength.toNumber()) 287 | }); 288 | }); 289 | }) 290 | -------------------------------------------------------------------------------- /contracts/Stream.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | pragma experimental ABIEncoderV2; 3 | 4 | import "./lib/SafeMath.sol"; 5 | import "./DOSOnChainSDK.sol"; 6 | 7 | contract IParser { 8 | function floatBytes2UintArray(bytes memory raw, uint decimal) public view returns(uint[] memory); 9 | } 10 | 11 | contract Stream is DOSOnChainSDK { 12 | using SafeMath for uint; 13 | 14 | uint private constant ONEHOUR = 1 hours; 15 | uint private constant ONEDAY = 1 days; 16 | // overflow flag 17 | uint private constant UINT_MAX = uint(-1); 18 | uint public windowSize = 1200; // 20 minutes 19 | // e.g. ETH / USD 20 | string public description; 21 | string public source; 22 | string public selector; 23 | uint public sId; 24 | // Absolute price deviation percentage * 1000, i.e. 1 represents 1/1000 price change. 25 | uint public deviation = 5; 26 | // Number of decimals the reported price data use. 27 | uint public decimal; 28 | // Data parser, may be configured along with data source change 29 | address public parser; 30 | address public streamManager; 31 | bool public whitelistEnabled; 32 | // Reader whitelist 33 | mapping(address => bool) private whitelist; 34 | // Stream data is either updated once per windowSize or the deviation requirement is met, whichever comes first. 35 | // Anyone can trigger an update on windowSize expiration, but only governance approved ones can be deviation updater to get rid of sybil attacks. 36 | mapping(address => bool) private deviationGuardian; 37 | mapping(uint => bool) private _valid; 38 | 39 | struct Observation { 40 | uint timestamp; 41 | uint price; 42 | } 43 | Observation[] private observations; 44 | 45 | event ParamsUpdated( 46 | string oldDescription, string newDescription, 47 | string oldSource, string newSource, 48 | string oldSelector, string newSelector, 49 | uint oldDecmial, uint newDecimal 50 | ); 51 | event WindowUpdated(uint oldWindow, uint newWindow); 52 | event DeviationUpdated(uint oldDeviation, uint newDeviation); 53 | event ParserUpdated(address oldParser, address newParser); 54 | event DataUpdated(uint timestamp, uint price); 55 | event PulledTrigger(address trigger, uint qId); 56 | event BulletCaught(uint qId); 57 | event AddAccess(address reader); 58 | event RemoveAccess(address reader); 59 | event AccessStatusUpdated(bool oldStatus, bool newStatus); 60 | event AddGuardian(address guardian); 61 | event RemoveGuardian(address guardian); 62 | 63 | modifier accessible { 64 | require(!whitelistEnabled || hasAccess(msg.sender), "!accessible"); 65 | _; 66 | } 67 | 68 | modifier isContract(address addr) { 69 | uint codeSize = 0; 70 | assembly { 71 | codeSize := extcodesize(addr) 72 | } 73 | require(codeSize > 0, "not-smart-contract"); 74 | _; 75 | } 76 | 77 | modifier onlyUpdater { 78 | require(msg.sender == streamManager, "!updater"); 79 | _; 80 | } 81 | 82 | constructor( 83 | string memory _description, 84 | string memory _source, 85 | string memory _selector, 86 | uint _decimal, 87 | address _parser, 88 | address _manager 89 | ) 90 | public 91 | isContract(_parser) 92 | isContract(_manager) 93 | { 94 | // @dev: setup and then transfer DOS tokens into deployed contract 95 | // as oracle fees. 96 | // Unused fees can be reclaimed by calling DOSRefund() function of SDK contract. 97 | super.DOSSetup(); 98 | description = _description; 99 | source = _source; 100 | selector = _selector; 101 | decimal = _decimal; 102 | parser = _parser; 103 | streamManager = _manager; 104 | addReader(_manager); 105 | emit ParamsUpdated("", _description, "", _source, "", _selector, 0, _decimal); 106 | emit ParserUpdated(address(0), _parser); 107 | } 108 | 109 | function strEqual(string memory a, string memory b) private pure returns (bool) { 110 | return keccak256(abi.encodePacked(a)) == keccak256(abi.encodePacked(b)); 111 | } 112 | 113 | function updateParams(string memory _description, string memory _source, string memory _selector, uint _decimal, uint _sId) public onlyOwner { 114 | emit ParamsUpdated( 115 | description, _description, 116 | source, _source, 117 | selector, _selector, 118 | decimal, _decimal 119 | ); 120 | if (!strEqual(description, _description)) description = _description; 121 | if (!strEqual(source, _source)) source = _source; 122 | if (!strEqual(selector, _selector)) selector = _selector; 123 | if (decimal != _decimal) decimal = _decimal; 124 | if (sId != _sId) sId = _sId; 125 | } 126 | // This will erase all observed data! 127 | function updateWindowSize(uint newWindow) public onlyOwner { 128 | emit WindowUpdated(windowSize, newWindow); 129 | windowSize = newWindow; 130 | observations.length = 0; 131 | } 132 | function updateDeviation(uint newDeviation) public onlyOwner { 133 | require(newDeviation >= 0 && newDeviation <= 1000, "should-be-in-0-1000"); 134 | emit DeviationUpdated(deviation, newDeviation); 135 | deviation = newDeviation; 136 | } 137 | function updateParser(address newParser) public onlyOwner isContract(newParser) { 138 | emit ParserUpdated(parser, newParser); 139 | parser = newParser; 140 | } 141 | function addReader(address reader) public onlyOwner { 142 | if (!whitelist[reader]) { 143 | whitelist[reader] = true; 144 | emit AddAccess(reader); 145 | } 146 | } 147 | function removeReader(address reader) public onlyOwner { 148 | if (whitelist[reader]) { 149 | delete whitelist[reader]; 150 | emit RemoveAccess(reader); 151 | } 152 | } 153 | function toggleAccessStatus() public onlyOwner { 154 | emit AccessStatusUpdated(whitelistEnabled, !whitelistEnabled); 155 | whitelistEnabled = !whitelistEnabled; 156 | } 157 | function addGuardian(address guardian) public onlyOwner { 158 | if (!deviationGuardian[guardian]) { 159 | deviationGuardian[guardian] = true; 160 | emit AddGuardian(guardian); 161 | } 162 | } 163 | function removeGuardian(address guardian) public onlyOwner { 164 | if (deviationGuardian[guardian]) { 165 | delete deviationGuardian[guardian]; 166 | emit RemoveGuardian(guardian); 167 | } 168 | } 169 | 170 | function hasAccess(address reader) public view returns(bool) { 171 | return whitelist[reader] || reader == tx.origin; 172 | } 173 | 174 | function numPoints() public view returns(uint) { 175 | return observations.length; 176 | } 177 | function num24hPoints() public view returns(uint) { 178 | uint idx = binarySearch(ONEDAY); 179 | if (idx == UINT_MAX) return observations.length; 180 | return observations.length - idx; 181 | } 182 | 183 | // Observation[] is sorted by timestamp in ascending order. Return the maximum index {i}, satisfying that: observations[i].timestamp <= observations[end].timestamp.sub(timedelta) 184 | // Return UINT_MAX if not enough data points. 185 | function binarySearch(uint timedelta) public view returns (uint) { 186 | if (observations.length == 0) return uint(-1); 187 | 188 | int index = -1; 189 | int l = 0; 190 | int r = int(observations.length.sub(1)); 191 | uint key = observations[uint(r)].timestamp.sub(timedelta); 192 | while (l <= r) { 193 | int m = (l + r) / 2; 194 | uint m_val = observations[uint(m)].timestamp; 195 | if (m_val <= key) { 196 | index = m; 197 | l = m + 1; 198 | } else { 199 | r = m - 1; 200 | } 201 | } 202 | return uint(index); 203 | } 204 | 205 | function stale(uint age) public view returns(bool) { 206 | uint lastTime = observations.length > 0 ? observations[observations.length - 1].timestamp : 0; 207 | return block.timestamp > lastTime.add(age); 208 | } 209 | 210 | function pullTrigger() public { 211 | if(!stale(windowSize) && !deviationGuardian[msg.sender]) return; 212 | 213 | uint id = DOSQuery(30, source, selector); 214 | if (id != 0) { 215 | _valid[id] = true; 216 | emit PulledTrigger(msg.sender, id); 217 | } 218 | } 219 | 220 | function __callback__(uint id, bytes calldata result) external auth { 221 | require(_valid[id], "invalid-request-id"); 222 | uint[] memory priceData = IParser(parser).floatBytes2UintArray(result, decimal); 223 | if (update(priceData[sId])) emit BulletCaught(id); 224 | delete _valid[id]; 225 | } 226 | 227 | function shouldUpdate(uint price) public view returns(bool) { 228 | uint lastPrice = observations.length > 0 ? observations[observations.length - 1].price : 0; 229 | uint delta = price > lastPrice ? (price - lastPrice) : (lastPrice - price); 230 | return stale(windowSize) || (deviation > 0 && delta >= lastPrice.mul(deviation).div(1000)); 231 | } 232 | 233 | function update(uint price) private returns(bool) { 234 | if (shouldUpdate(price)) { 235 | observations.push(Observation(block.timestamp, price)); 236 | emit DataUpdated(block.timestamp, price); 237 | return true; 238 | } 239 | return false; 240 | } 241 | 242 | function megaUpdate(uint price) public onlyUpdater returns(bool) { 243 | return update(price); 244 | } 245 | 246 | // @dev Returns any specific historical data point. 247 | // Accessible by whitelisted contracts or EOA user. 248 | function result(uint idx) public view accessible returns (uint _price, uint _timestamp) { 249 | require(idx < observations.length); 250 | return (observations[idx].price, observations[idx].timestamp); 251 | } 252 | 253 | // @dev Returns data [observations[startIdx], observations[lastIdx]], inclusive. 254 | function results(uint startIdx, uint lastIdx) public view accessible returns (Observation[] memory) { 255 | require(startIdx <= lastIdx && lastIdx < observations.length); 256 | Observation[] memory ret = new Observation[](lastIdx - startIdx + 1); 257 | for (uint i = startIdx; i <= lastIdx; i++) { 258 | ret[i - startIdx] = observations[i]; 259 | } 260 | return ret; 261 | } 262 | function last24hResults() public view accessible returns (Observation[] memory) { 263 | if (observations.length == 0) return (new Observation[](0)); 264 | 265 | uint lastIdx = observations.length - 1; 266 | uint startIdx = binarySearch(ONEDAY); 267 | if (startIdx == UINT_MAX) startIdx = 0; 268 | return results(startIdx, lastIdx); 269 | } 270 | 271 | // @dev Returns the most freshed (latest reported) data point. 272 | // Accessible by whitelisted contracts or EOA. 273 | // Return latest reported price & timestamp data. 274 | function latestResult() public view accessible returns (uint _lastPrice, uint _lastUpdatedTime) { 275 | require(observations.length > 0); 276 | Observation storage last = observations[observations.length - 1]; 277 | return (last.price, last.timestamp); 278 | } 279 | 280 | // @dev Returns time-weighted average price (TWAP) of (observations[start] : observations[end]). 281 | // Accessible by whitelisted contracts or EOA. 282 | function twapResult(uint start) public view accessible returns (uint) { 283 | require(start < observations.length, "index-overflow"); 284 | 285 | uint end = observations.length - 1; 286 | uint cumulativePrice = 0; 287 | for (uint i = start; i < end; i++) { 288 | cumulativePrice = cumulativePrice.add(observations[i].price.mul(observations[i+1].timestamp.sub(observations[i].timestamp))); 289 | } 290 | uint timeElapsed = observations[end].timestamp.sub(observations[start].timestamp); 291 | return cumulativePrice.div(timeElapsed); 292 | } 293 | 294 | // @dev Below are a series of inhouse TWAP functions for the ease of developers. 295 | // Accessible by whitelisted contracts or EOA user. 296 | // More TWAP functions can be built by the above twapResult(startIdx) function. 297 | function TWAP1Hour() public view accessible returns (uint) { 298 | require(!stale(ONEHOUR), "1h-outdated-data"); 299 | uint idx = binarySearch(ONEHOUR); 300 | require(idx != UINT_MAX, "not-enough-observation-data-for-1h"); 301 | return twapResult(idx); 302 | } 303 | 304 | function TWAP2Hour() public view accessible returns (uint) { 305 | require(!stale(ONEHOUR * 2), "2h-outdated-data"); 306 | uint idx = binarySearch(ONEHOUR * 2); 307 | require(idx != UINT_MAX, "not-enough-observation-data-for-2h"); 308 | return twapResult(idx); 309 | } 310 | 311 | function TWAP4Hour() public view accessible returns (uint) { 312 | require(!stale(ONEHOUR * 4), "4h-outdated-data"); 313 | uint idx = binarySearch(ONEHOUR * 4); 314 | require(idx != UINT_MAX, "not-enough-observation-data-for-4h"); 315 | return twapResult(idx); 316 | } 317 | 318 | function TWAP6Hour() public view accessible returns (uint) { 319 | require(!stale(ONEHOUR * 6), "6h-outdated-data"); 320 | uint idx = binarySearch(ONEHOUR * 6); 321 | require(idx != UINT_MAX, "not-enough-observation-data-for-6h"); 322 | return twapResult(idx); 323 | } 324 | 325 | function TWAP8Hour() public view accessible returns (uint) { 326 | require(!stale(ONEHOUR * 8), "8h-outdated-data"); 327 | uint idx = binarySearch(ONEHOUR * 8); 328 | require(idx != UINT_MAX, "not-enough-observation-data-for-8h"); 329 | return twapResult(idx); 330 | } 331 | 332 | function TWAP12Hour() public view accessible returns (uint) { 333 | require(!stale(ONEHOUR * 12), "12h-outdated-data"); 334 | uint idx = binarySearch(ONEHOUR * 12); 335 | require(idx != UINT_MAX, "not-enough-observation-data-for-12h"); 336 | return twapResult(idx); 337 | } 338 | 339 | function TWAP1Day() public view accessible returns (uint) { 340 | require(!stale(ONEDAY), "1d-outdated-data"); 341 | uint idx = binarySearch(ONEDAY); 342 | require(idx != UINT_MAX, "not-enough-observation-data-for-1d"); 343 | return twapResult(idx); 344 | } 345 | 346 | function TWAP1Week() public view accessible returns (uint) { 347 | require(!stale(ONEDAY * 7), "1w-outdated-data"); 348 | uint idx = binarySearch(ONEDAY * 7); 349 | require(idx != UINT_MAX, "not-enough-observation-data-for-1week"); 350 | return twapResult(idx); 351 | } 352 | } 353 | --------------------------------------------------------------------------------