├── .circleci └── config.yml ├── .gitignore ├── .prettierrc ├── .soliumignore ├── .soliumrc.json ├── README.md ├── contracts ├── MarbleSubscriber.sol ├── OracleToken.sol ├── Polaris.sol ├── interface │ ├── IPokable.sol │ ├── IPolaris.sol │ ├── IUniswapExchange.sol │ └── IUniswapFactory.sol └── test │ ├── MockERC20.sol │ ├── MockPoker.sol │ ├── MockSubscriber.sol │ ├── MockUniswapExchange.sol │ └── MockUniswapFactory.sol ├── globals.d.ts ├── package.json ├── scripts └── deploy.ts ├── test ├── integration │ ├── polaris.spec.ts │ └── scenarios │ │ ├── getDestAmount.spec.ts │ │ ├── poke.spec.ts │ │ └── subscribers.spec.ts └── utils │ ├── chai_setup.ts │ ├── constants.ts │ ├── deployer.ts │ └── types.ts ├── truffle.js ├── tsconfig.json ├── tslint.json └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/node:10.9.0 11 | 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/mongo:3.4.4 16 | 17 | working_directory: ~/polaris 18 | 19 | steps: 20 | - checkout 21 | 22 | # Download and cache dependencies 23 | - restore_cache: 24 | key: module-cache-{{ checksum "yarn.lock" }} 25 | - run: 26 | name: Fetch Dependencies 27 | command: yarn 28 | - save_cache: 29 | key: module-cache-{{ checksum "yarn.lock" }} 30 | paths: 31 | - node_modules 32 | - run: 33 | name: Run Tests 34 | command: yarn rebuild_and_test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | /.changelog 10 | 11 | # Dependency directories 12 | node_modules/ 13 | 14 | # Optional npm cache directory 15 | .npm 16 | 17 | # Optional eslint cache 18 | .eslintcache 19 | 20 | # Output of 'npm pack' 21 | *.tgz 22 | 23 | # dotenv environment variables file 24 | .env 25 | 26 | # build directories 27 | lib/ 28 | build/ 29 | 30 | # VSCode file 31 | .vscode -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "printWidth": 120, 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /.soliumignore: -------------------------------------------------------------------------------- 1 | contracts/common/interface/**/* -------------------------------------------------------------------------------- /.soliumrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solium:recommended", 3 | "plugins": ["security"], 4 | "rules": { 5 | "quotes": ["error", "double"], 6 | "indentation": ["error", 4] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CircleCI](https://circleci.com/gh/marbleprotocol/polaris/tree/master.svg?style=svg)](https://circleci.com/gh/marbleprotocol/polaris/tree/master) 2 | 3 | # ✨ Polaris 4 | 5 | Polaris is an on-chain decentralized price oracle for ERC20 tokens. It calculates the median of historical checkpoints on Uniswap for a price that is both accurate and resistant to manipulation. 6 | 7 | Polaris.sol: [0x440a803b42a78d93a1fe5da29a9fb37ecf193786](https://etherscan.io/address/0x440a803b42a78d93a1fe5da29a9fb37ecf193786) 8 | ``` 9 | [{"constant":true,"inputs":[],"name":"MONTHLY_SUBSCRIPTION_FEE","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"token","type":"address"}],"name":"willRewardCheckpoint","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"MAX_TIME_SINCE_LAST_CHECKPOINT","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"uniswap","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"token","type":"address"}],"name":"getMedianizer","outputs":[{"components":[{"name":"tail","type":"uint8"},{"name":"pendingStartTimestamp","type":"uint256"},{"name":"latestTimestamp","type":"uint256"},{"components":[{"name":"ethReserve","type":"uint256"},{"name":"tokenReserve","type":"uint256"}],"name":"prices","type":"tuple[]"},{"components":[{"name":"ethReserve","type":"uint256"},{"name":"tokenReserve","type":"uint256"}],"name":"pending","type":"tuple[]"},{"components":[{"name":"ethReserve","type":"uint256"},{"name":"tokenReserve","type":"uint256"}],"name":"median","type":"tuple"}],"name":"","type":"tuple"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"token","type":"address"}],"name":"subscribe","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":true,"inputs":[],"name":"ETHER","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"MAX_CHECKPOINTS","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"ONE_MONTH_IN_SECONDS","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"CHECKPOINT_REWARD","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"oracleTokens","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"token","type":"address"},{"name":"amount","type":"uint256"}],"name":"unsubscribe","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"token","type":"address"},{"name":"who","type":"address"}],"name":"collect","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"token","type":"address"},{"name":"who","type":"address"}],"name":"getOwedAmount","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"},{"name":"","type":"address"}],"name":"accounts","outputs":[{"name":"balance","type":"uint256"},{"name":"collectionTimestamp","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"token","type":"address"}],"name":"poke","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"MIN_PRICE_CHANGE","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"src","type":"address"},{"name":"dest","type":"address"},{"name":"srcAmount","type":"uint256"}],"name":"getDestAmount","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"token","type":"address"},{"name":"who","type":"address"}],"name":"getAccount","outputs":[{"components":[{"name":"balance","type":"uint256"},{"name":"collectionTimestamp","type":"uint256"}],"name":"","type":"tuple"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"PENDING_PERIOD","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"_uniswap","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"name":"token","type":"address"},{"indexed":false,"name":"ethReserve","type":"uint256"},{"indexed":false,"name":"tokenReserve","type":"uint256"}],"name":"NewMedian","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"token","type":"address"},{"indexed":true,"name":"subscriber","type":"address"},{"indexed":false,"name":"amount","type":"uint256"}],"name":"Subscribe","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"token","type":"address"},{"indexed":true,"name":"subscriber","type":"address"},{"indexed":false,"name":"amount","type":"uint256"}],"name":"Unsubscribe","type":"event"}] 10 | ``` 11 | 12 | MarbleSubscriber.sol: [0xf25acad904cb436bc3f970fc0a05ede486430cc9](https://etherscan.io/address/0xf25acad904cb436bc3f970fc0a05ede486430cc9) 13 | ``` 14 | [{"constant":true,"inputs":[{"name":"token","type":"address"}],"name":"getMedianizer","outputs":[{"components":[{"name":"tail","type":"uint8"},{"name":"pendingStartTimestamp","type":"uint256"},{"name":"latestTimestamp","type":"uint256"},{"components":[{"name":"ethReserve","type":"uint256"},{"name":"tokenReserve","type":"uint256"}],"name":"prices","type":"tuple[]"},{"components":[{"name":"ethReserve","type":"uint256"},{"name":"tokenReserve","type":"uint256"}],"name":"pending","type":"tuple[]"},{"components":[{"name":"ethReserve","type":"uint256"},{"name":"tokenReserve","type":"uint256"}],"name":"median","type":"tuple"}],"name":"","type":"tuple"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"asset","type":"address"}],"name":"subscribe","outputs":[{"name":"","type":"uint256"}],"payable":true,"stateMutability":"payable","type":"function"},{"constant":true,"inputs":[],"name":"oracle","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"src","type":"address"},{"name":"dest","type":"address"},{"name":"srcAmount","type":"uint256"}],"name":"getDestAmount","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"_oracle","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"}] 15 | ``` 16 | 17 | OracleToken.sol: [0xf62e77385b751b7e3fabf0441932fd9ba46abbd3](https://etherscan.io/address/0xf62e77385b751b7e3fabf0441932fd9ba46abbd3) 18 | ``` 19 | [{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"spender","type":"address"},{"name":"value","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"from","type":"address"},{"name":"to","type":"address"},{"name":"value","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"spender","type":"address"},{"name":"addedValue","type":"uint256"}],"name":"increaseAllowance","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"oracle","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"spender","type":"address"},{"name":"subtractedValue","type":"uint256"}],"name":"decreaseAllowance","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"to","type":"address"},{"name":"value","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"owner","type":"address"},{"name":"spender","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"token","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"_token","type":"address"}],"payable":true,"stateMutability":"payable","type":"constructor"},{"payable":true,"stateMutability":"payable","type":"fallback"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"},{"indexed":true,"name":"spender","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"constant":false,"inputs":[{"name":"to","type":"address"},{"name":"amount","type":"uint256"}],"name":"mint","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"amount","type":"uint256"}],"name":"redeem","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"}] 20 | ``` 21 | 22 | ### Incentives 23 | 24 | * Earn a reward by calling `poke` on the price oracle when one of the following is true: 25 | * The current price is >1% different from the median price 26 | * The current price is >1% different from the median of pokes in the last 3 minutes 27 | * The current price is >1% different from the last poke 28 | * It has been >3 hours since the last price checkpoint 29 | * A valid poke mints one oracle token to the poker. Each underlying token (e.g., DAI) has a corresponding oracle token. 30 | * Smart contract protocols must subscribe to read prices from the price oracle. The monthly subscription fee is 5 ether per token. Marble will launch the first paying subscriber for the ETH-DAI price oracle. 31 | * Burn oracle tokens to collect subscription fees. 32 | 33 | ### Collect Subscription Fees 34 | 35 | ``` 36 | // i.e. - Collect fees from MarbleSubscriber and send proceeds to DAI OracleToken 37 | // token : DAI - 0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359 38 | // who : MarbleSubscriber - 0xf25acaD904cb436bc3f970FC0A05EdE486430cC9 39 | collect(0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359, 0xf25acaD904cb436bc3f970FC0A05EdE486430cC9) 40 | 41 | // Redeem fees using DAI oracle tokens at address - 0xf62e77385b751b7e3fabf0441932fd9ba46abbd3 42 | // amount : 1 PLRS 43 | redeem(1000000000000000000) 44 | ``` 45 | 46 | ### Reading the price 47 | 48 | #### getDestAmount 49 | ``` 50 | /** 51 | * @notice This uses the x * y = k bonding curve to determine the destination amount based on the medianized price. 52 | * 𝝙x = (𝝙y * x) / (y + 𝝙y) 53 | * @dev Get the amount of destination token, based on a given amount of source token. 54 | * @param src The address of the source token. 55 | * @param dest The address of the destination token. 56 | * @param srcAmount The amount of the source token. 57 | * @return The amount of destination token. 58 | */ 59 | 60 | // i.e. - Getting the amount of DAI for 1 ETH 61 | // src : ETH - 0x000000000000000000000000000000000000000 62 | // dest : DAI - 0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359 63 | // srcAmount : 1 ETH in wei 64 | getDestAmount(0x0000000000000000000000000000000000000000, 0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359, 1000000000000000000) 65 | ``` 66 | 67 | #### getMedianizer 68 | ``` 69 | /** 70 | * @dev Get price data for a given token. 71 | * @param token The address of the token to query. 72 | * @return The price data struct. 73 | */ 74 | 75 | // i.e. - Getting all the raw oracle data for DAI 76 | // token: DAI - 0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359 77 | getMedianizer(0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359) 78 | ``` 79 | 80 | ### Install dependencies 81 | ``` 82 | yarn 83 | ``` 84 | 85 | ### Compile smart contracts 86 | ``` 87 | yarn compile 88 | ``` 89 | 90 | ### Run tests 91 | ``` 92 | yarn test 93 | ``` 94 | 95 | ### Compile and test 96 | ``` 97 | yarn rebuild_and_test 98 | ``` 99 | -------------------------------------------------------------------------------- /contracts/MarbleSubscriber.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.5.2; 2 | pragma experimental ABIEncoderV2; 3 | 4 | import { IPolaris } from "./interface/IPolaris.sol"; 5 | 6 | 7 | contract MarbleSubscriber { 8 | 9 | IPolaris public oracle; 10 | 11 | constructor(address _oracle) public { 12 | oracle = IPolaris(_oracle); 13 | } 14 | 15 | function subscribe(address asset) public payable returns (uint) { 16 | oracle.subscribe.value(msg.value)(asset); 17 | } 18 | 19 | function getDestAmount(address src, address dest, uint srcAmount) public view returns (uint) { 20 | return oracle.getDestAmount(src, dest, srcAmount); 21 | } 22 | 23 | function getMedianizer(address token) public view returns (IPolaris.Medianizer memory) { 24 | return oracle.getMedianizer(token); 25 | } 26 | } -------------------------------------------------------------------------------- /contracts/OracleToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.5.2; 2 | 3 | import { ERC20 } from "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol"; 4 | 5 | 6 | contract OracleToken is ERC20 { 7 | string public name = "Polaris Token"; 8 | string public symbol = "PLRS"; 9 | uint8 public decimals = 18; 10 | address public oracle; 11 | address public token; 12 | 13 | constructor(address _token) public payable { 14 | oracle = msg.sender; 15 | token = _token; 16 | } 17 | 18 | function () external payable {} 19 | 20 | function mint(address to, uint amount) public returns (bool) { 21 | require(msg.sender == oracle, "OracleToken::mint: Only Oracle can call mint"); 22 | _mint(to, amount); 23 | return true; 24 | } 25 | 26 | function redeem(uint amount) public { 27 | uint ethAmount = address(this).balance.mul(amount).div(totalSupply()); 28 | _burn(msg.sender, amount); 29 | msg.sender.transfer(ethAmount); 30 | } 31 | } -------------------------------------------------------------------------------- /contracts/Polaris.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.5.2; 2 | pragma experimental ABIEncoderV2; 3 | 4 | import { IERC20 } from "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; 5 | import { Math } from "openzeppelin-solidity/contracts/math/Math.sol"; 6 | import { SafeMath } from "openzeppelin-solidity/contracts/math/SafeMath.sol"; 7 | 8 | import { IUniswapExchange } from "./interface/IUniswapExchange.sol"; 9 | import { IUniswapFactory } from "./interface/IUniswapFactory.sol"; 10 | 11 | import { OracleToken } from "./OracleToken.sol"; 12 | 13 | 14 | contract Polaris { 15 | using Math for uint; 16 | using SafeMath for uint; 17 | 18 | event NewMedian(address indexed token, uint ethReserve, uint tokenReserve); 19 | event Subscribe(address indexed token, address indexed subscriber, uint amount); 20 | event Unsubscribe(address indexed token, address indexed subscriber, uint amount); 21 | 22 | uint8 public constant MAX_CHECKPOINTS = 15; 23 | 24 | // Reward for a successful poke, in oracle tokens 25 | uint public constant CHECKPOINT_REWARD = 1e18; 26 | 27 | // Conditions for checkpoint reward 28 | uint public constant MIN_PRICE_CHANGE = .01e18; // 1% 29 | uint public constant MAX_TIME_SINCE_LAST_CHECKPOINT = 3 hours; 30 | 31 | uint public constant PENDING_PERIOD = 3.5 minutes; 32 | 33 | address public constant ETHER = address(0); 34 | 35 | // Monthly subscription fee to subscribe to a single oracle 36 | uint public constant MONTHLY_SUBSCRIPTION_FEE = 5 ether; 37 | uint public constant ONE_MONTH_IN_SECONDS = 30 days; 38 | 39 | IUniswapFactory public uniswap; 40 | 41 | struct Account { 42 | uint balance; 43 | uint collectionTimestamp; 44 | } 45 | 46 | struct Checkpoint { 47 | uint ethReserve; 48 | uint tokenReserve; 49 | } 50 | 51 | struct Medianizer { 52 | uint8 tail; 53 | uint pendingStartTimestamp; 54 | uint latestTimestamp; 55 | Checkpoint[] prices; 56 | Checkpoint[] pending; 57 | Checkpoint median; 58 | } 59 | 60 | // Token => Subscriber => Account 61 | mapping (address => mapping (address => Account)) public accounts; 62 | 63 | // Token => Oracle Token (reward for poking) 64 | mapping (address => OracleToken) public oracleTokens; 65 | 66 | // Token => Medianizer 67 | mapping (address => Medianizer) private medianizers; 68 | 69 | constructor(IUniswapFactory _uniswap) public { 70 | uniswap = _uniswap; 71 | } 72 | 73 | /** 74 | * @dev Subscribe to read the price of a given token (e.g, DAI). 75 | * @param token The address of the token to subscribe to. 76 | */ 77 | function subscribe(address token) public payable { 78 | Account storage account = accounts[token][msg.sender]; 79 | _collect(token, account); 80 | account.balance = account.balance.add(msg.value); 81 | require(account.balance >= MONTHLY_SUBSCRIPTION_FEE, "Polaris::subscribe: Account balance is below the minimum"); 82 | emit Subscribe(token, msg.sender, msg.value); 83 | } 84 | 85 | /** 86 | * @dev Unsubscribe to a given token (e.g, DAI). 87 | * @param token The address of the token to unsubscribe from. 88 | * @param amount The requested amount to withdraw, in wei. 89 | * @return The actual amount withdrawn, in wei. 90 | */ 91 | function unsubscribe(address token, uint amount) public returns (uint) { 92 | Account storage account = accounts[token][msg.sender]; 93 | _collect(token, account); 94 | uint maxWithdrawAmount = account.balance.sub(MONTHLY_SUBSCRIPTION_FEE); 95 | uint actualWithdrawAmount = amount.min(maxWithdrawAmount); 96 | account.balance = account.balance.sub(actualWithdrawAmount); 97 | msg.sender.transfer(actualWithdrawAmount); 98 | emit Unsubscribe(token, msg.sender, actualWithdrawAmount); 99 | } 100 | 101 | /** 102 | * @dev Collect subscription fees from a subscriber. 103 | * @param token The address of the subscribed token to collect fees from. 104 | * @param who The address of the subscriber. 105 | */ 106 | function collect(address token, address who) public { 107 | Account storage account = accounts[token][who]; 108 | _collect(token, account); 109 | } 110 | 111 | /** 112 | * @dev Add a new price checkpoint. 113 | * @param token The address of the token to checkpoint. 114 | */ 115 | function poke(address token) public { 116 | require(_isHuman(), "Polaris::poke: Poke must be called by an externally owned account"); 117 | OracleToken oracleToken = oracleTokens[token]; 118 | 119 | // Get the current reserves from Uniswap 120 | Checkpoint memory checkpoint = _newCheckpoint(token); 121 | 122 | if (address(oracleToken) == address(0)) { 123 | _initializeMedianizer(token, checkpoint); 124 | } else { 125 | Medianizer storage medianizer = medianizers[token]; 126 | 127 | require(medianizer.latestTimestamp != block.timestamp, "Polaris::poke: Cannot poke more than once per block"); 128 | 129 | // See if checkpoint should be rewarded 130 | if (_willRewardCheckpoint(token, checkpoint)) { 131 | oracleToken.mint(msg.sender, CHECKPOINT_REWARD); 132 | } 133 | 134 | // If pending checkpoints are old, reset pending checkpoints 135 | if (block.timestamp.sub(medianizer.pendingStartTimestamp) > PENDING_PERIOD || medianizer.pending.length == MAX_CHECKPOINTS) { 136 | medianizer.pending.length = 0; 137 | medianizer.tail = (medianizer.tail + 1) % MAX_CHECKPOINTS; 138 | medianizer.pendingStartTimestamp = block.timestamp; 139 | } 140 | 141 | medianizer.latestTimestamp = block.timestamp; 142 | 143 | // Add the checkpoint to the pending array 144 | medianizer.pending.push(checkpoint); 145 | 146 | // Add the pending median to the prices array 147 | medianizer.prices[medianizer.tail] = _medianize(medianizer.pending); 148 | 149 | // Find and store the prices median 150 | medianizer.median = _medianize(medianizer.prices); 151 | 152 | emit NewMedian(token, medianizer.median.ethReserve, medianizer.median.tokenReserve); 153 | } 154 | } 155 | 156 | /** 157 | * @dev Get price data for a given token. 158 | * @param token The address of the token to query. 159 | * @return The price data struct. 160 | */ 161 | function getMedianizer(address token) public view returns (Medianizer memory) { 162 | require(_isSubscriber(accounts[token][msg.sender]) || _isHuman(), "Polaris::getMedianizer: Not subscribed"); 163 | return medianizers[token]; 164 | } 165 | 166 | /** 167 | * @notice This uses the x * y = k bonding curve to determine the destination amount based on the medianized price. 168 | * 𝝙x = (𝝙y * x) / (y + 𝝙y) 169 | * @dev Get the amount of destination token, based on a given amount of source token. 170 | * @param src The address of the source token. 171 | * @param dest The address of the destination token. 172 | * @param srcAmount The amount of the source token. 173 | * @return The amount of destination token. 174 | */ 175 | function getDestAmount(address src, address dest, uint srcAmount) public view returns (uint) { 176 | if (!_isHuman()) { 177 | require(src == ETHER || _isSubscriber(accounts[src][msg.sender]), "Polaris::getDestAmount: Not subscribed"); 178 | require(dest == ETHER || _isSubscriber(accounts[dest][msg.sender]), "Polaris::getDestAmount: Not subscribed"); 179 | } 180 | 181 | if (src == dest) { 182 | return srcAmount; 183 | } else if (src == ETHER) { 184 | Checkpoint memory median = medianizers[dest].median; 185 | return srcAmount.mul(median.tokenReserve).div(median.ethReserve.add(srcAmount)); 186 | } else if (dest == ETHER) { 187 | Checkpoint memory median = medianizers[src].median; 188 | return srcAmount.mul(median.ethReserve).div(median.tokenReserve.add(srcAmount)); 189 | } else { 190 | Checkpoint memory srcMedian = medianizers[src].median; 191 | Checkpoint memory destMedian = medianizers[dest].median; 192 | 193 | uint ethAmount = srcAmount.mul(srcMedian.ethReserve).div(srcMedian.tokenReserve.add(srcAmount)); 194 | return ethAmount.mul(destMedian.ethReserve).div(destMedian.tokenReserve.add(ethAmount)); 195 | } 196 | } 197 | 198 | /** 199 | * @dev Determine whether a given checkpoint would be rewarded with newly minted oracle tokens. 200 | * @param token The address of the token to query checkpoint for. 201 | * @return True if given checkpoint satisfies any of the following: 202 | * Less than required checkpoints exist to calculate a valid median 203 | * Exceeds max time since last checkpoint 204 | * Exceeds minimum price change from median AND no pending checkpoints 205 | * Exceeds minimum percent change from pending checkpoints median 206 | * Exceeds minimum percent change from last checkpoint 207 | */ 208 | function willRewardCheckpoint(address token) public view returns (bool) { 209 | Checkpoint memory checkpoint = _newCheckpoint(token); 210 | return _willRewardCheckpoint(token, checkpoint); 211 | } 212 | 213 | /** 214 | * @dev Get the account for a given subscriber of a token feed. 215 | * @param token The token to query the account of the given subscriber. 216 | * @param who The subscriber to query the account of the given token feed. 217 | * @return The account of the subscriber of the given token feed. 218 | */ 219 | function getAccount(address token, address who) public view returns (Account memory) { 220 | return accounts[token][who]; 221 | } 222 | 223 | /** 224 | * @dev Get the owed amount for a given subscriber of a token feed. 225 | * @param token The token to query the owed amount of the given subscriber. 226 | * @param who The subscriber to query the owed amount for the given token feed. 227 | * @return The owed amount of the subscriber of the given token feed. 228 | */ 229 | function getOwedAmount(address token, address who) public view returns (uint) { 230 | Account storage account = accounts[token][who]; 231 | return _getOwedAmount(account); 232 | } 233 | 234 | /** 235 | * @dev Update the subscriber balance of a given token feed. 236 | * @param token The token to collect subscription revenues for. 237 | * @param account The subscriber account to collect subscription revenues from. 238 | */ 239 | function _collect(address token, Account storage account) internal { 240 | if (account.balance == 0) { 241 | account.collectionTimestamp = block.timestamp; 242 | return; 243 | } 244 | 245 | uint owedAmount = _getOwedAmount(account); 246 | OracleToken oracleToken = oracleTokens[token]; 247 | 248 | // If the subscriber does not have enough, collect the remaining balance 249 | if (owedAmount >= account.balance) { 250 | address(oracleToken).transfer(account.balance); 251 | account.balance = 0; 252 | } else { 253 | address(oracleToken).transfer(owedAmount); 254 | account.balance = account.balance.sub(owedAmount); 255 | } 256 | 257 | account.collectionTimestamp = block.timestamp; 258 | } 259 | 260 | /** 261 | * @dev Initialize the medianizer 262 | * @param token The token to initialize the medianizer for. 263 | * @param checkpoint The new checkpoint to initialize the medianizer with. 264 | */ 265 | function _initializeMedianizer(address token, Checkpoint memory checkpoint) internal { 266 | address payable exchange = uniswap.getExchange(token); 267 | require(exchange != address(0), "Polaris::_initializeMedianizer: Token must exist on Uniswap"); 268 | 269 | OracleToken oracleToken = new OracleToken(token); 270 | oracleTokens[token] = oracleToken; 271 | // Reward additional oracle tokens for the first poke to compensate for extra gas costs 272 | oracleToken.mint(msg.sender, CHECKPOINT_REWARD.mul(10)); 273 | 274 | Medianizer storage medianizer = medianizers[token]; 275 | medianizer.pending.push(checkpoint); 276 | medianizer.median = checkpoint; 277 | medianizer.latestTimestamp = block.timestamp; 278 | medianizer.pendingStartTimestamp = block.timestamp; 279 | 280 | // Hydrate prices queue 281 | for (uint i = 0; i < MAX_CHECKPOINTS; i++) { 282 | medianizer.prices.push(checkpoint); 283 | } 284 | } 285 | 286 | /** 287 | * @dev Find the median given an array of checkpoints. 288 | * @param checkpoints The array of checkpoints to find the median. 289 | * @return The median checkpoint within the given array. 290 | */ 291 | function _medianize(Checkpoint[] memory checkpoints) internal pure returns (Checkpoint memory) { 292 | // To minimize complexity, return the higher of the two middle checkpoints in even-sized arrays instead of the average. 293 | uint k = checkpoints.length.div(2); 294 | uint left = 0; 295 | uint right = checkpoints.length.sub(1); 296 | 297 | while (left < right) { 298 | uint pivotIndex = left.add(right).div(2); 299 | Checkpoint memory pivotCheckpoint = checkpoints[pivotIndex]; 300 | 301 | (checkpoints[pivotIndex], checkpoints[right]) = (checkpoints[right], checkpoints[pivotIndex]); 302 | uint storeIndex = left; 303 | for (uint i = left; i < right; i++) { 304 | if (_isLessThan(checkpoints[i], pivotCheckpoint)) { 305 | (checkpoints[storeIndex], checkpoints[i]) = (checkpoints[i], checkpoints[storeIndex]); 306 | storeIndex++; 307 | } 308 | } 309 | 310 | (checkpoints[storeIndex], checkpoints[right]) = (checkpoints[right], checkpoints[storeIndex]); 311 | if (storeIndex < k) { 312 | left = storeIndex.add(1); 313 | } else { 314 | right = storeIndex; 315 | } 316 | } 317 | 318 | return checkpoints[k]; 319 | } 320 | 321 | /** 322 | * @dev Determine if checkpoint x is less than checkpoint y. 323 | * @param x The first checkpoint for comparison. 324 | * @param y The second checkpoint for comparison. 325 | * @return True if x is less than y. 326 | */ 327 | function _isLessThan(Checkpoint memory x, Checkpoint memory y) internal pure returns (bool) { 328 | return x.ethReserve.mul(y.tokenReserve) < y.ethReserve.mul(x.tokenReserve); 329 | } 330 | 331 | /** 332 | * @dev Check if msg.sender is an externally owned account. 333 | * @return True if msg.sender is an externally owned account, false if smart contract. 334 | */ 335 | function _isHuman() internal view returns (bool) { 336 | return msg.sender == tx.origin; 337 | } 338 | 339 | /** 340 | * @dev Get the reserve values of a Uniswap exchange for a given token. 341 | * @param token The token to query the reserve values for. 342 | * @return A checkpoint holding the appropriate reserve values. 343 | */ 344 | function _newCheckpoint(address token) internal view returns (Checkpoint memory) { 345 | address payable exchange = uniswap.getExchange(token); 346 | return Checkpoint({ 347 | ethReserve: exchange.balance, 348 | tokenReserve: IERC20(token).balanceOf(exchange) 349 | }); 350 | } 351 | 352 | /** 353 | * @dev Get subscriber status of a given account for a given token. 354 | * @param account The account to query. 355 | * @return True if subscribed. 356 | */ 357 | function _isSubscriber(Account storage account) internal view returns (bool) { 358 | // Strict inequality to return false for users who never subscribed and owe zero. 359 | return account.balance > _getOwedAmount(account); 360 | } 361 | 362 | /** 363 | * @dev Get amount owed by an account. Accrued amount minus collections. 364 | * @param account The account to query. 365 | * @return Amount owed. 366 | */ 367 | function _getOwedAmount(Account storage account) internal view returns (uint) { 368 | if (account.collectionTimestamp == 0) return 0; 369 | 370 | uint timeElapsed = block.timestamp.sub(account.collectionTimestamp); 371 | return MONTHLY_SUBSCRIPTION_FEE.mul(timeElapsed).div(ONE_MONTH_IN_SECONDS); 372 | } 373 | 374 | /** 375 | * @dev Determine whether a given checkpoint would be rewarded with newly minted oracle tokens. 376 | * @param token The address of the token to query checkpoint for. 377 | * @param checkpoint The checkpoint to test for reward of oracle tokens. 378 | * @return True if given checkpoint satisfies any of the following: 379 | * Less than required checkpoints exist to calculate a valid median 380 | * Exceeds max time since last checkpoint 381 | * Exceeds minimum price change from median AND no pending checkpoints 382 | * Exceeds minimum percent change from pending checkpoints median 383 | * Exceeds minimum percent change from last checkpoint 384 | */ 385 | function _willRewardCheckpoint(address token, Checkpoint memory checkpoint) internal view returns (bool) { 386 | Medianizer memory medianizer = medianizers[token]; 387 | 388 | return ( 389 | medianizer.prices.length < MAX_CHECKPOINTS || 390 | block.timestamp.sub(medianizer.latestTimestamp) >= MAX_TIME_SINCE_LAST_CHECKPOINT || 391 | (block.timestamp.sub(medianizer.pendingStartTimestamp) >= PENDING_PERIOD && _percentChange(medianizer.median, checkpoint) >= MIN_PRICE_CHANGE) || 392 | _percentChange(medianizer.prices[medianizer.tail], checkpoint) >= MIN_PRICE_CHANGE || 393 | _percentChange(medianizer.pending[medianizer.pending.length.sub(1)], checkpoint) >= MIN_PRICE_CHANGE 394 | ); 395 | } 396 | 397 | /** 398 | * @dev Get the percent change between two checkpoints. 399 | * @param x The first checkpoint. 400 | * @param y The second checkpoint. 401 | * @return The absolute value of the percent change, with 18 decimals of precision (e.g., .01e18 = 1%). 402 | */ 403 | function _percentChange(Checkpoint memory x, Checkpoint memory y) internal pure returns (uint) { 404 | uint a = x.ethReserve.mul(y.tokenReserve); 405 | uint b = y.ethReserve.mul(x.tokenReserve); 406 | uint diff = a > b ? a.sub(b) : b.sub(a); 407 | return diff.mul(10 ** 18).div(a); 408 | } 409 | 410 | } -------------------------------------------------------------------------------- /contracts/interface/IPokable.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.5.2; 2 | 3 | 4 | contract IPokable { 5 | function poke(address token) public; 6 | } -------------------------------------------------------------------------------- /contracts/interface/IPolaris.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.5.2; 2 | pragma experimental ABIEncoderV2; 3 | 4 | 5 | contract IPolaris { 6 | struct Checkpoint { 7 | uint ethReserve; 8 | uint tokenReserve; 9 | } 10 | 11 | struct Medianizer { 12 | uint8 tail; 13 | uint pendingStartTimestamp; 14 | uint latestTimestamp; 15 | Checkpoint[] prices; 16 | Checkpoint[] pending; 17 | Checkpoint median; 18 | } 19 | function subscribe(address token) public payable; 20 | function unsubscribe(address token, uint amount) public returns (uint actualAmount); 21 | function getMedianizer(address token) public view returns (Medianizer memory); 22 | function getDestAmount(address src, address dest, uint srcAmount) public view returns (uint); 23 | } -------------------------------------------------------------------------------- /contracts/interface/IUniswapExchange.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.5.2; 2 | 3 | 4 | contract IUniswapExchange { 5 | // Address of ERC20 token sold on this exchange 6 | function tokenAddress() external view returns (address token); 7 | // Address of Uniswap Factory 8 | function factoryAddress() external view returns (address factory); 9 | // Provide Liquidity 10 | function addLiquidity(uint256 min_liquidity, uint256 max_tokens, uint256 deadline) external payable returns (uint256); 11 | function removeLiquidity(uint256 amount, uint256 min_eth, uint256 min_tokens, uint256 deadline) external returns (uint256, uint256); 12 | // Get Prices 13 | function getEthToTokenInputPrice(uint256 eth_sold) external view returns (uint256 tokens_bought); 14 | function getEthToTokenOutputPrice(uint256 tokens_bought) external view returns (uint256 eth_sold); 15 | function getTokenToEthInputPrice(uint256 tokens_sold) external view returns (uint256 eth_bought); 16 | function getTokenToEthOutputPrice(uint256 eth_bought) external view returns (uint256 tokens_sold); 17 | // Trade ETH to ERC20 18 | function ethToTokenSwapInput(uint256 min_tokens, uint256 deadline) external payable returns (uint256 tokens_bought); 19 | function ethToTokenTransferInput(uint256 min_tokens, uint256 deadline, address recipient) external payable returns (uint256 tokens_bought); 20 | function ethToTokenSwapOutput(uint256 tokens_bought, uint256 deadline) external payable returns (uint256 eth_sold); 21 | function ethToTokenTransferOutput(uint256 tokens_bought, uint256 deadline, address recipient) external payable returns (uint256 eth_sold); 22 | // Trade ERC20 to ETH 23 | function tokenToEthSwapInput(uint256 tokens_sold, uint256 min_eth, uint256 deadline) external returns (uint256 eth_bought); 24 | function tokenToEthTransferInput(uint256 tokens_sold, uint256 min_tokens, uint256 deadline, address recipient) external returns (uint256 eth_bought); 25 | function tokenToEthSwapOutput(uint256 eth_bought, uint256 max_tokens, uint256 deadline) external returns (uint256 tokens_sold); 26 | function tokenToEthTransferOutput(uint256 eth_bought, uint256 max_tokens, uint256 deadline, address recipient) external returns (uint256 tokens_sold); 27 | // Trade ERC20 to ERC20 28 | function tokenToTokenSwapInput(uint256 tokens_sold, uint256 min_tokens_bought, uint256 min_eth_bought, uint256 deadline, address token_addr) external returns (uint256 tokens_bought); 29 | function tokenToTokenTransferInput(uint256 tokens_sold, uint256 min_tokens_bought, uint256 min_eth_bought, uint256 deadline, address recipient, address token_addr) external returns (uint256 tokens_bought); 30 | function tokenToTokenSwapOutput(uint256 tokens_bought, uint256 max_tokens_sold, uint256 max_eth_sold, uint256 deadline, address token_addr) external returns (uint256 tokens_sold); 31 | function tokenToTokenTransferOutput(uint256 tokens_bought, uint256 max_tokens_sold, uint256 max_eth_sold, uint256 deadline, address recipient, address token_addr) external returns (uint256 tokens_sold); 32 | // Trade ERC20 to Custom Pool 33 | function tokenToExchangeSwapInput(uint256 tokens_sold, uint256 min_tokens_bought, uint256 min_eth_bought, uint256 deadline, address exchange_addr) external returns (uint256 tokens_bought); 34 | function tokenToExchangeTransferInput(uint256 tokens_sold, uint256 min_tokens_bought, uint256 min_eth_bought, uint256 deadline, address recipient, address exchange_addr) external returns (uint256 tokens_bought); 35 | function tokenToExchangeSwapOutput(uint256 tokens_bought, uint256 max_tokens_sold, uint256 max_eth_sold, uint256 deadline, address exchange_addr) external returns (uint256 tokens_sold); 36 | function tokenToExchangeTransferOutput(uint256 tokens_bought, uint256 max_tokens_sold, uint256 max_eth_sold, uint256 deadline, address recipient, address exchange_addr) external returns (uint256 tokens_sold); 37 | // ERC20 comaptibility for liquidity tokens 38 | bytes32 public name; 39 | bytes32 public symbol; 40 | uint256 public decimals; 41 | function transfer(address _to, uint256 _value) external returns (bool); 42 | function transferFrom(address _from, address _to, uint256 value) external returns (bool); 43 | function approve(address _spender, uint256 _value) external returns (bool); 44 | function allowance(address _owner, address _spender) external view returns (uint256); 45 | function balanceOf(address _owner) external view returns (uint256); 46 | // Never use 47 | function setup(address token_addr) external; 48 | } -------------------------------------------------------------------------------- /contracts/interface/IUniswapFactory.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.5.2; 2 | 3 | 4 | contract IUniswapFactory { 5 | // Public Variables 6 | address public exchangeTemplate; 7 | uint256 public tokenCount; 8 | // Create Exchange 9 | function createExchange(address token) external returns (address payable exchange); 10 | // Get Exchange and Token Info 11 | function getExchange(address token) external view returns (address payable exchange); 12 | function getToken(address exchange) external view returns (address token); 13 | function getTokenWithId(uint256 tokenId) external view returns (address token); 14 | } -------------------------------------------------------------------------------- /contracts/test/MockERC20.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.5.2; 2 | 3 | import { ERC20 } from "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol"; 4 | 5 | contract MockERC20 is ERC20 { 6 | 7 | event Issue(address token, address to, uint amount); 8 | 9 | function issueTo(address who, uint amount) public { 10 | _mint(who, amount); 11 | } 12 | 13 | } -------------------------------------------------------------------------------- /contracts/test/MockPoker.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.5.2; 2 | 3 | import { IPokable } from "../interface/IPokable.sol"; 4 | 5 | 6 | contract MockPoker { 7 | 8 | IPokable public oracle; 9 | 10 | constructor(IPokable _oracle) public { 11 | oracle = _oracle; 12 | } 13 | 14 | function poke(address token) public { 15 | oracle.poke(token); 16 | } 17 | } -------------------------------------------------------------------------------- /contracts/test/MockSubscriber.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.5.2; 2 | pragma experimental ABIEncoderV2; 3 | 4 | import { IPolaris } from "../interface/IPolaris.sol"; 5 | 6 | 7 | contract MockSubscriber { 8 | 9 | IPolaris public oracle; 10 | 11 | constructor(address _oracle) public { 12 | oracle = IPolaris(_oracle); 13 | } 14 | 15 | // Accept Ether upon unsubscribe 16 | function () external payable {} 17 | 18 | function subscribe(address asset) public payable returns (uint) { 19 | oracle.subscribe.value(msg.value)(asset); 20 | } 21 | 22 | function unsubscribe(address asset, uint amount) public returns (uint) { 23 | return oracle.unsubscribe(asset, amount); 24 | } 25 | 26 | function getDestAmount(address src, address dest, uint srcAmount) public view returns (uint) { 27 | return oracle.getDestAmount(src, dest, srcAmount); 28 | } 29 | 30 | function getMedianizer(address token) public view returns (IPolaris.Medianizer memory) { 31 | return oracle.getMedianizer(token); 32 | } 33 | } -------------------------------------------------------------------------------- /contracts/test/MockUniswapExchange.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.5.2; 2 | pragma experimental ABIEncoderV2; 3 | 4 | import { IERC20 } from "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; 5 | import { SafeMath } from "openzeppelin-solidity/contracts/math/SafeMath.sol"; 6 | 7 | 8 | contract MockUniswapExchange { 9 | using SafeMath for uint; 10 | 11 | bytes32 public name; 12 | bytes32 public symbol; 13 | uint256 public decimals; 14 | uint256 public totalSupply; 15 | 16 | uint public valueInEth = 1000000000000000000; // default price 17 | 18 | mapping (address => uint256) private balances; 19 | mapping (address => mapping (address => uint256)) private allowances; 20 | address private token; 21 | address private factory; 22 | 23 | // Accept Ether 24 | function () external payable {} 25 | 26 | function setup(address _token) external { 27 | factory = msg.sender; 28 | token = _token; 29 | // name = "0x556e697377617020563100000000000000000000000000000000000000000000"; 30 | // symbol = "0x554e492d56310000000000000000000000000000000000000000000000000000"; 31 | decimals = 18; 32 | } 33 | 34 | function setValueInEth(uint _valueInEth) external { 35 | valueInEth = _valueInEth; 36 | } 37 | 38 | function removeEth(uint _valueInEth) external { 39 | msg.sender.transfer(_valueInEth); 40 | } 41 | 42 | function removeTokens(uint _tokenAmount) external { 43 | IERC20(token).transfer(msg.sender, _tokenAmount); 44 | } 45 | 46 | // Address of ERC20 token sold on this exchange 47 | function tokenAddress() external view returns (address) { 48 | return token; 49 | } 50 | 51 | // Address of Uniswap Factory 52 | function factoryAddress() external view returns (address) { 53 | return factory; 54 | } 55 | 56 | // Get Prices 57 | function getEthToTokenInputPrice(uint256 eth_sold) public view returns (uint256 tokens_bought) { 58 | return eth_sold.mul(10 ** 18).div(valueInEth); 59 | } 60 | 61 | function getEthToTokenOutputPrice(uint256 tokens_bought) public view returns (uint256 eth_sold) { 62 | return tokens_bought.mul(valueInEth).div(10 ** 18); 63 | } 64 | 65 | function getTokenToEthInputPrice(uint256 tokens_sold) public view returns (uint256 eth_bought) { 66 | return tokens_sold.mul(valueInEth).div(10 ** 18); 67 | } 68 | 69 | function getTokenToEthOutputPrice(uint256 eth_bought) public view returns (uint256 tokens_sold) { 70 | return eth_bought.mul(10 ** 18).div(valueInEth); 71 | } 72 | 73 | function ethToTokenSwapInput(uint256 min_tokens, uint256 deadline) external payable returns (uint256 tokens_bought) { 74 | tokens_bought = getEthToTokenInputPrice(msg.value); 75 | require(tokens_bought >= min_tokens, "MockUniswap::ethToTokenSwapInput: isn't greater than min_tokens"); 76 | IERC20(token).transfer(msg.sender, tokens_bought); 77 | 78 | // Silence compiler warning 79 | deadline; 80 | } 81 | 82 | function ethToTokenTransferInput(uint256 min_tokens, uint256 deadline, address payable recipient) external payable returns (uint256 tokens_bought) { 83 | tokens_bought = getEthToTokenInputPrice(msg.value); 84 | require(tokens_bought >= min_tokens, "MockUniswap::ethToTokenTransferInput: isn't greater than min_tokens"); 85 | IERC20(token).transfer(recipient, tokens_bought); 86 | 87 | // Silence compiler warning 88 | deadline; 89 | } 90 | 91 | function ethToTokenSwapOutput(uint256 tokens_bought, uint256 deadline) external payable returns (uint256 eth_sold) { 92 | eth_sold = getEthToTokenOutputPrice(tokens_bought); 93 | require(eth_sold == msg.value, "MockUniswap::ethToTokenSwapOutput: msg.value doesn't equal the eth_sold"); 94 | IERC20(token).transfer(msg.sender, tokens_bought); 95 | 96 | // Silence compiler warning 97 | deadline; 98 | } 99 | 100 | function ethToTokenTransferOutput(uint256 tokens_bought, uint256 deadline, address payable recipient) external payable returns (uint256 eth_sold) { 101 | eth_sold = getEthToTokenOutputPrice(tokens_bought); 102 | require(eth_sold == msg.value, "MockUniswap::ethToTokenTransferOutput: msg.value doesn't equal the eth_sold"); 103 | IERC20(token).transfer(recipient, tokens_bought); 104 | 105 | // Silence compiler warning 106 | deadline; 107 | } 108 | 109 | function tokenToEthSwapInput(uint256 tokens_sold, uint256 min_eth, uint256 deadline) external returns (uint256 eth_bought) { 110 | eth_bought = getTokenToEthInputPrice(tokens_sold); 111 | require(eth_bought >= min_eth, "MockUniswap::tokenToEthSwapInput: isn't greater than min_eth"); 112 | IERC20(token).transferFrom(msg.sender, address(this), tokens_sold); 113 | msg.sender.transfer(eth_bought); 114 | 115 | // Silence compiler warning 116 | deadline; 117 | } 118 | 119 | function tokenToEthTransferInput(uint256 tokens_sold, uint256 min_eth, uint256 deadline, address payable recipient) external returns (uint256 eth_bought) { 120 | eth_bought = getTokenToEthInputPrice(tokens_sold); 121 | require(eth_bought >= min_eth, "MockUniswap::tokenToEthTransferInput: isn't greater than min_eth"); 122 | IERC20(token).transferFrom(msg.sender, address(this), tokens_sold); 123 | recipient.transfer(eth_bought); 124 | 125 | // Silence compiler warning 126 | deadline; 127 | } 128 | 129 | function tokenToEthSwapOutput(uint256 eth_bought, uint256 max_tokens, uint256 deadline) external returns (uint256 tokens_sold) { 130 | tokens_sold = getTokenToEthOutputPrice(eth_bought); 131 | require(tokens_sold <= max_tokens, "MockUniswap::tokenToEthSwapOutput: tokens_sold is greater than max_tokens"); 132 | IERC20(token).transferFrom(msg.sender, address(this), tokens_sold); 133 | msg.sender.transfer(eth_bought); 134 | 135 | // Silence compiler warning 136 | deadline; 137 | } 138 | 139 | function tokenToEthTransferOutput(uint256 eth_bought, uint256 max_tokens, uint256 deadline, address payable recipient) external returns (uint256 tokens_sold) { 140 | tokens_sold = getTokenToEthOutputPrice(eth_bought); 141 | require(tokens_sold <= max_tokens, "MockUniswap::tokenToEthTransferOutput: tokens_sold is greater than max_tokens"); 142 | IERC20(token).transferFrom(msg.sender, address(this), tokens_sold); 143 | recipient.transfer(eth_bought); 144 | 145 | // Silence compiler warning 146 | deadline; 147 | } 148 | } -------------------------------------------------------------------------------- /contracts/test/MockUniswapFactory.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.5.2; 2 | pragma experimental ABIEncoderV2; 3 | 4 | import { MockUniswapExchange } from "./MockUniswapExchange.sol"; 5 | 6 | 7 | contract MockUniswapFactory { 8 | // Public Variables 9 | address public exchangeTemplate; 10 | uint256 public tokenCount; 11 | 12 | mapping (address => address) private exchangeToToken; 13 | mapping (address => address) private tokenToExchange; 14 | mapping (uint256 => address) private idToToken; 15 | 16 | function createExchange(address token) external returns (address) { 17 | MockUniswapExchange exchange = new MockUniswapExchange(); 18 | exchange.setup(token); 19 | tokenToExchange[token] = address(exchange); 20 | exchangeToToken[address(exchange)] = token; 21 | uint256 tokenId = tokenCount + 1; 22 | tokenCount = tokenId; 23 | idToToken[tokenId] = token; 24 | return address(exchange); 25 | } 26 | 27 | function getExchange(address token) external view returns (address) { 28 | return tokenToExchange[token]; 29 | } 30 | 31 | function getToken(address exchange) external view returns (address) { 32 | return exchangeToToken[exchange]; 33 | } 34 | 35 | function getTokenWithId(uint256 tokenId) external view returns (address) { 36 | return idToToken[tokenId]; 37 | } 38 | } -------------------------------------------------------------------------------- /globals.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.json' { 2 | const json: any; 3 | /* tslint:disable */ 4 | export default json; 5 | /* tslint:enable */ 6 | } 7 | 8 | declare module 'web3-eth-abi' { 9 | export function encodeParameter(type: any, parameter: any): string; 10 | export function encodeParameters(typesArray: string[], parameters: any[]): string; 11 | export function encodeFunctionSignature( 12 | functionName: string | { name: string; type: string; inputs: any[] } 13 | ): string; 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "polaris", 3 | "version": "0.0.1", 4 | "description": "Polaris - A decentralized price oracle for ERC20 tokens", 5 | "directories": { 6 | "test": "test" 7 | }, 8 | "scripts": { 9 | "build": "yarn pre_build && tsc", 10 | "chain": "ganache-cli -p 8545 -i 50 -l 100000000 -e 100 -m 'concert load couple harbor equip island argue ramp clarify fence smart topic'", 11 | "clean": "shx rm -rf lib build", 12 | "compile": "cake compile", 13 | "copy_artifacts": "copyfiles './build/artifacts/**/*' ./lib;", 14 | "deploy": "tsc && node 'lib/scripts/deploy'", 15 | "fork": "npm run chain -- --fork https://mainnet.infura.io/@$block", 16 | "lint": "solium -d contracts/lend/ --fix", 17 | "pre_build": "run-s compile copy_artifacts", 18 | "rebuild_and_test": "run-s clean build test", 19 | "test": "tsc && cake test -t 'lib/test/integration'", 20 | "tslint": "tslint --project . --exclude **/generated_contract_wrappers/**/* --exclude **/lib/**/*", 21 | "watch_without_deps": "yarn pre_build && tsc -w" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/marbleprotocol/polaris.git" 26 | }, 27 | "license": "ISC", 28 | "bugs": { 29 | "url": "https://github.com/marbleprotocol/polaris/issues" 30 | }, 31 | "homepage": "https://github.com/marbleprotocol/polaris#readme", 32 | "dependencies": { 33 | "@0x/base-contract": "^3.0.8", 34 | "@0x/tslint-config": "^1.0.10", 35 | "@types/dotenv": "^6.1.0", 36 | "@types/web3-provider-engine": "^14.0.0", 37 | "dotenv": "^6.2.0", 38 | "ethereum-types": "^1.1.2", 39 | "ethereumjs-util": "^5.1.1", 40 | "ethers": "^4.0.15", 41 | "ganache-cli": "^6.1.8", 42 | "lodash": "^4.17.11", 43 | "openzeppelin-solidity": "git://github.com/marbleprotocol/openzeppelin-solidity.git#cb21987", 44 | "rlp": "^2.2.2", 45 | "web3": "^1.0.0-beta.36", 46 | "web3-eth-abi": "^1.0.0-beta.36", 47 | "web3-provider-engine": "^14.1.0" 48 | }, 49 | "devDependencies": { 50 | "@0x/sol-compiler": "^1.1.14", 51 | "@0x/typescript-typings": "^3.0.4", 52 | "@0x/utils": "^2.0.6", 53 | "@0x/web3-wrapper": "^3.1.6", 54 | "@marbleprotocol/cake": "^0.0.5", 55 | "@marbleprotocol/dev-utils": "^0.0.6", 56 | "@types/chai": "^4.1.4", 57 | "@types/lodash": "^4.14.118", 58 | "@types/mocha": "^5.2.5", 59 | "@types/node": "^8.0.53", 60 | "@types/web3-eth-abi": "^1.0.0", 61 | "chai": "^4.1.2", 62 | "chai-as-promised": "^7.1.1", 63 | "chai-bignumber": "^2.0.1", 64 | "copyfiles": "^1.2.0", 65 | "dirty-chai": "^2.0.1", 66 | "mocha": "^4.1.0", 67 | "npm-run-all": "^4.1.2", 68 | "shx": "^0.2.2", 69 | "solium": "^1.1.8", 70 | "tslint": "5.11.0", 71 | "typescript": "2.9.2" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /scripts/deploy.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FakeGasEstimateSubprovider, 3 | RPCSubprovider, 4 | PrivateKeyWalletSubprovider, 5 | Web3ProviderEngine 6 | } from '@0x/subproviders'; 7 | import { Web3Wrapper } from '@0x/web3-wrapper'; 8 | import * as dotenv from 'dotenv'; 9 | 10 | import { MarbleSubscriberContract, PolarisContract } from '../build/wrappers'; 11 | 12 | dotenv.load(); 13 | 14 | const infura = 'https://mainnet.infura.io/'; 15 | const provider = new Web3ProviderEngine(); 16 | const privateKey = process.env.PRIVATE_KEY as string; 17 | provider.addProvider(new PrivateKeyWalletSubprovider(privateKey)); 18 | provider.addProvider(new RPCSubprovider(infura)); 19 | provider.addProvider(new FakeGasEstimateSubprovider(8000000)); 20 | provider.start(); 21 | 22 | (async () => { 23 | try { 24 | const web3Wrapper = new Web3Wrapper(provider); 25 | const [deployer] = await web3Wrapper.getAvailableAddressesAsync(); 26 | const txDefaults = { from: deployer, gas: 6500000 }; 27 | 28 | const uniswapFactoryAddr = '0xc0a47dfe034b400b47bdad5fecda2621de6c4d95'; 29 | 30 | const oracle = await PolarisContract.deployAsync(provider, txDefaults, uniswapFactoryAddr); 31 | await MarbleSubscriberContract.deployAsync(provider, txDefaults, oracle.address); 32 | } catch (e) { 33 | console.log(e); 34 | } 35 | process.exit(); 36 | })(); 37 | -------------------------------------------------------------------------------- /test/integration/polaris.spec.ts: -------------------------------------------------------------------------------- 1 | import { ganacheProvider, BlockchainLifecycle } from '@marbleprotocol/dev-utils'; 2 | import * as _ from 'lodash'; 3 | 4 | import { deployPriceOracle } from '../utils/deployer'; 5 | import { TX_DEFAULTS } from '../utils/constants'; 6 | 7 | import { getDestAmountTests } from './scenarios/getDestAmount.spec'; 8 | import { pokeTests } from './scenarios/poke.spec'; 9 | import { subscribersTests } from './scenarios/subscribers.spec'; 10 | 11 | const provider = ganacheProvider(); 12 | const blockchainLifecycle = new BlockchainLifecycle(provider); 13 | 14 | const snapshot = () => { 15 | before(async function() { 16 | await blockchainLifecycle.startAsync(); 17 | }); 18 | 19 | after(async function() { 20 | await blockchainLifecycle.revertAsync(); 21 | }); 22 | }; 23 | 24 | describe('Polaris', () => { 25 | before(async function() { 26 | this.protocol = await deployPriceOracle(provider, TX_DEFAULTS); 27 | this.provider = provider; 28 | }); 29 | 30 | beforeEach(async function() { 31 | await blockchainLifecycle.startAsync(); 32 | }); 33 | 34 | afterEach(async function() { 35 | await blockchainLifecycle.revertAsync(); 36 | }); 37 | 38 | getDestAmountTests(snapshot); 39 | pokeTests(snapshot); 40 | subscribersTests(snapshot); 41 | }); 42 | -------------------------------------------------------------------------------- /test/integration/scenarios/getDestAmount.spec.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from '@0x/utils'; 2 | import { Web3Wrapper } from '@0x/web3-wrapper'; 3 | import { Provider } from 'ethereum-types'; 4 | import * as _ from 'lodash'; 5 | 6 | import { chai } from '../../utils/chai_setup'; 7 | import { ETH_ADDRESS, ONE_ETH_IN_WEI, TX_DEFAULTS, ZERO_AMOUNT } from '../../utils/constants'; 8 | import { Checkpoint } from '../../utils/types'; 9 | 10 | import { 11 | MockUniswapExchangeContract, 12 | MockUniswapFactoryContract, 13 | MockERC20Contract, 14 | PolarisContract 15 | } from '../../../build/wrappers'; 16 | 17 | const { expect } = chai; 18 | 19 | export function getDestAmountTests(snapshotBlockchain: () => void) { 20 | describe('#getDestAmount', async () => { 21 | let borrowERC20: MockERC20Contract; 22 | let borrowExchange: MockUniswapExchangeContract; 23 | let uniswapFactory: MockUniswapFactoryContract; 24 | let polaris: PolarisContract; 25 | 26 | let addresses: string[]; 27 | 28 | let timestampIncrease: number; 29 | 30 | let web3Wrapper: Web3Wrapper; 31 | let provider: Provider; 32 | 33 | let maxCheckpoints: number; 34 | 35 | snapshotBlockchain(); 36 | 37 | before(async function() { 38 | ({ addresses, borrowERC20, uniswapFactory, polaris } = this.protocol); 39 | 40 | provider = this.provider; 41 | web3Wrapper = new Web3Wrapper(provider); 42 | 43 | const pendingPeriod = await polaris.PENDING_PERIOD.callAsync(); 44 | timestampIncrease = +pendingPeriod.plus(1); 45 | 46 | maxCheckpoints = await polaris.MAX_CHECKPOINTS.callAsync(); 47 | 48 | const borrowExchangeAddress = await uniswapFactory.getExchange.callAsync(borrowERC20.address); 49 | borrowExchange = MockUniswapExchangeContract.new( 50 | borrowExchangeAddress, 51 | provider, 52 | TX_DEFAULTS 53 | ); 54 | }); 55 | 56 | it('should return the correct value from getDestAmount for checkpointing greater than MAX_CHECKPOINTS', async () => { 57 | const maxRandomNumber = ONE_ETH_IN_WEI.mul(99); 58 | const minRandomNumber = ONE_ETH_IN_WEI.mul(80); 59 | const prices = getRandomCheckpointArray(maxCheckpoints, maxRandomNumber, minRandomNumber); 60 | 61 | const jsMedian = getDestAmount(prices, ETH_ADDRESS, ONE_ETH_IN_WEI); 62 | 63 | const gasSum = await setExchangeValuesAndPoke( 64 | web3Wrapper, 65 | polaris, 66 | borrowExchange, 67 | borrowERC20, 68 | addresses, 69 | prices, 70 | timestampIncrease 71 | ); 72 | const gasAverage = gasSum.div(prices.length).toFixed(0); 73 | console.log('GAS AVERAGE: ', gasAverage); 74 | 75 | const median = await polaris.getDestAmount.callAsync( 76 | ETH_ADDRESS, 77 | borrowERC20.address, 78 | ONE_ETH_IN_WEI 79 | ); 80 | expect(median).to.be.bignumber.equal(jsMedian); 81 | }); 82 | 83 | it('should return the correct value from getDestAmount for checkpointing less than MAX_CHECKPOINTS', async () => { 84 | const maxRandomNumber = ONE_ETH_IN_WEI.mul(99); 85 | const minRandomNumber = ONE_ETH_IN_WEI.mul(80); 86 | const array1 = getRandomCheckpointArray(maxCheckpoints, maxRandomNumber, minRandomNumber); 87 | const array2 = getRandomCheckpointArray(2, maxRandomNumber, minRandomNumber); 88 | const [, ...subArray1] = array1; 89 | const array = [...subArray1, getMedianCheckpoint(array2)]; 90 | const numberOfCheckpoints = array1.length + array2.length; 91 | 92 | const jsMedian = getDestAmount(array, ETH_ADDRESS, ONE_ETH_IN_WEI); 93 | let gasSum = await setExchangeValuesAndPoke( 94 | web3Wrapper, 95 | polaris, 96 | borrowExchange, 97 | borrowERC20, 98 | addresses, 99 | array1, 100 | timestampIncrease 101 | ); 102 | 103 | await web3Wrapper.increaseTimeAsync(timestampIncrease); 104 | await web3Wrapper.mineBlockAsync(); 105 | 106 | gasSum = gasSum.plus( 107 | await setExchangeValuesAndPoke( 108 | web3Wrapper, 109 | polaris, 110 | borrowExchange, 111 | borrowERC20, 112 | addresses, 113 | array2 114 | ) 115 | ); 116 | const gasAverage = gasSum.div(numberOfCheckpoints).toFixed(0); 117 | console.log('GAS AVERAGE: ', gasAverage); 118 | 119 | const median = await polaris.getDestAmount.callAsync( 120 | ETH_ADDRESS, 121 | borrowERC20.address, 122 | ONE_ETH_IN_WEI 123 | ); 124 | expect(median).to.be.bignumber.equal(jsMedian); 125 | }); 126 | }); 127 | } 128 | 129 | async function setExchangeValuesAndPoke( 130 | web3Wrapper: Web3Wrapper, 131 | oracle: PolarisContract, 132 | exchange: MockUniswapExchangeContract, 133 | token: MockERC20Contract, 134 | addresses: string[], 135 | arr: Checkpoint[], 136 | timeBetweenCheckpoints: number = 1 137 | ) { 138 | return arr.reduce(async (previous: Promise, element: Checkpoint, index: number) => { 139 | const previousSum = await previous; 140 | 141 | await web3Wrapper.increaseTimeAsync(timeBetweenCheckpoints); 142 | await web3Wrapper.mineBlockAsync(); 143 | 144 | const currentEthInReserve = await web3Wrapper.getBalanceInWeiAsync(exchange.address); 145 | const currentTokensInReserve = await token.balanceOf.callAsync(exchange.address); 146 | 147 | const differenceInEth = element.ethReserve.sub(currentEthInReserve); 148 | const differenceInTokens = element.tokenReserve.sub(currentTokensInReserve); 149 | 150 | if (differenceInEth.lessThan(ZERO_AMOUNT)) { 151 | await exchange.removeEth.sendTransactionAsync(differenceInEth.abs()); 152 | } else if (differenceInEth.greaterThan(ZERO_AMOUNT)) { 153 | const address = await _.find(addresses, async function(a) { 154 | const balance = await web3Wrapper.getBalanceInWeiAsync(a); 155 | return balance.greaterThan(differenceInEth); 156 | }); 157 | 158 | if (!_.isUndefined(address)) { 159 | await web3Wrapper.sendTransactionAsync({ 160 | from: address as string, 161 | value: differenceInEth, 162 | to: exchange.address 163 | }); 164 | } else { 165 | throw new Error('None of the addresses have enough ETH'); 166 | } 167 | } 168 | 169 | if (differenceInTokens.lessThan(ZERO_AMOUNT)) { 170 | await exchange.removeTokens.sendTransactionAsync(differenceInTokens.abs()); 171 | } else if (differenceInTokens.greaterThan(ZERO_AMOUNT)) { 172 | await token.issueTo.sendTransactionAsync(exchange.address, differenceInTokens); 173 | } 174 | 175 | const txHash = await oracle.poke.sendTransactionAsync(token.address); 176 | const txReceipt = await web3Wrapper.awaitTransactionSuccessAsync(txHash); 177 | console.log( 178 | `${index + 1} - GAS USED FOR POKE: ${txReceipt.gasUsed} -- BLOCK NUMBER: ${ 179 | txReceipt.blockNumber 180 | }` 181 | ); 182 | return previousSum.add(txReceipt.gasUsed); 183 | }, Promise.resolve(ZERO_AMOUNT)); 184 | } 185 | 186 | function getMedianCheckpoint(arr: Checkpoint[]): Checkpoint { 187 | const index = Math.floor(arr.length / 2); 188 | 189 | const sortedArray = _.sortBy(arr, [ 190 | (x: Checkpoint) => x.ethReserve.div(x.tokenReserve).toNumber() 191 | ]); 192 | 193 | return { 194 | ethReserve: sortedArray[index].ethReserve, 195 | tokenReserve: sortedArray[index].tokenReserve 196 | }; 197 | } 198 | 199 | function getDestAmount(arr: Checkpoint[], src: string, srcAmount: BigNumber) { 200 | const medianCheckpoint = getMedianCheckpoint(arr); 201 | 202 | if (src == ETH_ADDRESS) { 203 | return new BigNumber( 204 | srcAmount 205 | .mul(medianCheckpoint.tokenReserve) 206 | .div(medianCheckpoint.ethReserve.add(srcAmount)) 207 | .toFixed(0, BigNumber.ROUND_FLOOR) 208 | ); 209 | } else { 210 | return new BigNumber( 211 | srcAmount 212 | .mul(medianCheckpoint.ethReserve) 213 | .div(medianCheckpoint.tokenReserve.add(srcAmount)) 214 | .toFixed(0) 215 | ); 216 | } 217 | } 218 | 219 | function getRandomCheckpointArray(size: number, max: BigNumber, min: BigNumber): Checkpoint[] { 220 | return Array.from({ length: size }, () => { 221 | return { 222 | ethReserve: min.plus( 223 | new BigNumber( 224 | BigNumber.random() 225 | .mul(max) 226 | .toFixed(0) 227 | ) 228 | ), 229 | tokenReserve: min.plus( 230 | new BigNumber( 231 | BigNumber.random() 232 | .mul(max) 233 | .toFixed(0) 234 | ) 235 | ) 236 | }; 237 | }); 238 | } 239 | -------------------------------------------------------------------------------- /test/integration/scenarios/poke.spec.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from '@0x/utils'; 2 | import { Web3Wrapper } from '@0x/web3-wrapper'; 3 | import { Provider } from 'ethereum-types'; 4 | import * as _ from 'lodash'; 5 | 6 | import { chai } from '../../utils/chai_setup'; 7 | import { TX_DEFAULTS, ZERO_AMOUNT } from '../../utils/constants'; 8 | 9 | import { 10 | MockPokerContract, 11 | MockUniswapExchangeContract, 12 | MockUniswapFactoryContract, 13 | MockERC20Contract, 14 | PolarisContract, 15 | OracleTokenContract 16 | } from '../../../build/wrappers'; 17 | 18 | const { expect } = chai; 19 | 20 | export function pokeTests(snapshotBlockchain: () => void) { 21 | describe('#poke', async () => { 22 | let borrowERC20: MockERC20Contract; 23 | let borrowOracleToken: OracleTokenContract; 24 | let borrowUniswapExchange: MockUniswapExchangeContract; 25 | let uniswapFactory: MockUniswapFactoryContract; 26 | let polaris: PolarisContract; 27 | 28 | let poker: string; 29 | let pokerBalanceBefore: BigNumber; 30 | let pokerReward: BigNumber; 31 | let maxCheckpoints: number; 32 | let pendingPeriod: BigNumber; 33 | let maxTimePeriod: BigNumber; 34 | 35 | let web3Wrapper: Web3Wrapper; 36 | let provider: Provider; 37 | 38 | before(async function() { 39 | ({ borrowERC20, poker, uniswapFactory, polaris } = this.protocol); 40 | 41 | provider = this.provider; 42 | web3Wrapper = new Web3Wrapper(provider); 43 | 44 | const borrowExchangeAddress = await uniswapFactory.getExchange.callAsync(borrowERC20.address); 45 | borrowUniswapExchange = MockUniswapExchangeContract.new(borrowExchangeAddress, this.provider, TX_DEFAULTS); 46 | 47 | pokerReward = await polaris.CHECKPOINT_REWARD.callAsync(); 48 | maxCheckpoints = await polaris.MAX_CHECKPOINTS.callAsync(); 49 | pendingPeriod = await polaris.PENDING_PERIOD.callAsync(); 50 | maxTimePeriod = await polaris.MAX_TIME_SINCE_LAST_CHECKPOINT.callAsync(); 51 | }); 52 | 53 | it('should only allow externally owned accounts to poke', async () => { 54 | const mockPoker = await MockPokerContract.deployAsync(provider, TX_DEFAULTS, polaris.address); 55 | 56 | expect(mockPoker.poke.sendTransactionAsync(borrowERC20.address)).to.be.rejectedWith( 57 | 'Polaris::poke: Poke must be called by an externally owned account' 58 | ); 59 | }); 60 | 61 | describe('first token poke', () => { 62 | snapshotBlockchain(); 63 | 64 | it('should reward for Oracle Token creation plus poke', async () => { 65 | pokerBalanceBefore = ZERO_AMOUNT; 66 | await polaris.poke.sendTransactionAsync(borrowERC20.address, { from: poker }); 67 | const expectedPokerBalance = pokerBalanceBefore.plus(pokerReward.times(10)); 68 | const borrowOracleTokenAddress = await polaris.oracleTokens.callAsync(borrowERC20.address); 69 | 70 | borrowOracleToken = OracleTokenContract.new(borrowOracleTokenAddress, provider, TX_DEFAULTS); 71 | const afterBalance = await borrowOracleToken.balanceOf.callAsync(poker); 72 | 73 | expect(afterBalance).to.be.bignumber.equal(expectedPokerBalance); 74 | }); 75 | }); 76 | 77 | describe('poke for 1% price change from previous poke', () => { 78 | snapshotBlockchain(); 79 | 80 | before(async () => { 81 | await polaris.poke.sendTransactionAsync(borrowERC20.address, { from: poker }); 82 | const borrowOracleTokenAddress = await polaris.oracleTokens.callAsync(borrowERC20.address); 83 | 84 | borrowOracleToken = OracleTokenContract.new(borrowOracleTokenAddress, provider, TX_DEFAULTS); 85 | 86 | const currentExchangeBalance = await web3Wrapper.getBalanceInWeiAsync(borrowUniswapExchange.address); 87 | const increaseByOnePercent = currentExchangeBalance.times(0.011); 88 | 89 | await web3Wrapper.sendTransactionAsync({ 90 | from: poker, 91 | value: increaseByOnePercent, 92 | to: borrowUniswapExchange.address 93 | }); 94 | 95 | pokerBalanceBefore = await borrowOracleToken.balanceOf.callAsync(poker); 96 | 97 | await web3Wrapper.increaseTimeAsync(1); 98 | await web3Wrapper.mineBlockAsync(); 99 | 100 | await polaris.poke.sendTransactionAsync(borrowERC20.address, { from: poker }); 101 | }); 102 | 103 | it('should reward poke', async () => { 104 | const afterBalance = await borrowOracleToken.balanceOf.callAsync(poker); 105 | const expectedPokerBalance = pokerBalanceBefore.plus(pokerReward); 106 | 107 | expect(afterBalance).to.be.bignumber.equal(expectedPokerBalance); 108 | }); 109 | }); 110 | 111 | describe('poke for 1% price change from overall median', () => { 112 | snapshotBlockchain(); 113 | 114 | before(async () => { 115 | await polaris.poke.sendTransactionAsync(borrowERC20.address, { from: poker }); 116 | const borrowOracleTokenAddress = await polaris.oracleTokens.callAsync(borrowERC20.address); 117 | 118 | borrowOracleToken = OracleTokenContract.new(borrowOracleTokenAddress, provider, TX_DEFAULTS); 119 | 120 | const currentExchangeBalance = await web3Wrapper.getBalanceInWeiAsync(borrowUniswapExchange.address); 121 | const increaseByFivePercent = currentExchangeBalance.times(0.05); 122 | 123 | await web3Wrapper.sendTransactionAsync({ 124 | from: poker, 125 | value: increaseByFivePercent, 126 | to: borrowUniswapExchange.address 127 | }); 128 | 129 | await web3Wrapper.increaseTimeAsync(1); 130 | await web3Wrapper.mineBlockAsync(); 131 | 132 | await polaris.poke.sendTransactionAsync(borrowERC20.address, { from: poker }); 133 | 134 | pokerBalanceBefore = await borrowOracleToken.balanceOf.callAsync(poker); 135 | 136 | await web3Wrapper.increaseTimeAsync(+pendingPeriod.plus(1)); 137 | await web3Wrapper.mineBlockAsync(); 138 | 139 | await polaris.poke.sendTransactionAsync(borrowERC20.address, { from: poker }); 140 | }); 141 | 142 | it('should reward poke', async () => { 143 | const afterBalance = await borrowOracleToken.balanceOf.callAsync(poker); 144 | const expectedPokerBalance = pokerBalanceBefore.plus(pokerReward); 145 | 146 | expect(afterBalance).to.be.bignumber.equal(expectedPokerBalance); 147 | }); 148 | }); 149 | 150 | describe('poke for 1% price change from pending median', () => { 151 | snapshotBlockchain(); 152 | 153 | before(async () => { 154 | await polaris.poke.sendTransactionAsync(borrowERC20.address, { from: poker }); 155 | const borrowOracleTokenAddress = await polaris.oracleTokens.callAsync(borrowERC20.address); 156 | 157 | borrowOracleToken = OracleTokenContract.new(borrowOracleTokenAddress, provider, TX_DEFAULTS); 158 | 159 | const currentExchangeBalance = await web3Wrapper.getBalanceInWeiAsync(borrowUniswapExchange.address); 160 | const increaseByFivePercent = currentExchangeBalance.times(0.05); 161 | 162 | await web3Wrapper.increaseTimeAsync(+pendingPeriod.plus(1)); 163 | await web3Wrapper.mineBlockAsync(); 164 | 165 | await web3Wrapper.sendTransactionAsync({ 166 | from: poker, 167 | value: increaseByFivePercent, 168 | to: borrowUniswapExchange.address 169 | }); 170 | 171 | await polaris.poke.sendTransactionAsync(borrowERC20.address, { from: poker }); 172 | 173 | await web3Wrapper.increaseTimeAsync(1); 174 | await web3Wrapper.mineBlockAsync(); 175 | 176 | await polaris.poke.sendTransactionAsync(borrowERC20.address, { from: poker }); 177 | 178 | await web3Wrapper.increaseTimeAsync(1); 179 | await web3Wrapper.mineBlockAsync(); 180 | 181 | await borrowUniswapExchange.removeEth.sendTransactionAsync(increaseByFivePercent); 182 | 183 | await web3Wrapper.increaseTimeAsync(1); 184 | await web3Wrapper.mineBlockAsync(); 185 | 186 | await polaris.poke.sendTransactionAsync(borrowERC20.address, { from: poker }); 187 | 188 | await web3Wrapper.increaseTimeAsync(1); 189 | await web3Wrapper.mineBlockAsync(); 190 | 191 | pokerBalanceBefore = await borrowOracleToken.balanceOf.callAsync(poker); 192 | 193 | await polaris.poke.sendTransactionAsync(borrowERC20.address, { from: poker }); 194 | }); 195 | 196 | it('should reward poke', async () => { 197 | const afterBalance = await borrowOracleToken.balanceOf.callAsync(poker); 198 | const expectedPokerBalance = pokerBalanceBefore.plus(pokerReward); 199 | 200 | expect(afterBalance).to.be.bignumber.equal(expectedPokerBalance); 201 | }); 202 | 203 | it('should have correct pending length', async () => { 204 | const medianizer = await polaris.getMedianizer.callAsync(borrowERC20.address); 205 | 206 | expect(medianizer.pending.length).to.equal(4); 207 | }); 208 | }); 209 | 210 | describe('poke for no pokes in maximum time frame', () => { 211 | snapshotBlockchain(); 212 | 213 | before(async () => { 214 | await polaris.poke.sendTransactionAsync(borrowERC20.address, { from: poker }); 215 | const borrowOracleTokenAddress = await polaris.oracleTokens.callAsync(borrowERC20.address); 216 | 217 | borrowOracleToken = OracleTokenContract.new(borrowOracleTokenAddress, provider, TX_DEFAULTS); 218 | 219 | await web3Wrapper.increaseTimeAsync(+maxTimePeriod.plus(1)); 220 | await web3Wrapper.mineBlockAsync(); 221 | 222 | pokerBalanceBefore = await borrowOracleToken.balanceOf.callAsync(poker); 223 | await polaris.poke.sendTransactionAsync(borrowERC20.address, { from: poker }); 224 | }); 225 | 226 | it('should reward poke', async () => { 227 | const afterBalance = await borrowOracleToken.balanceOf.callAsync(poker); 228 | const expectedPokerBalance = pokerBalanceBefore.plus(pokerReward); 229 | 230 | expect(afterBalance).to.be.bignumber.equal(expectedPokerBalance); 231 | }); 232 | }); 233 | 234 | describe('Register poke but do not payout reward if criteria not met', () => { 235 | let previousTailIndex: number; 236 | 237 | snapshotBlockchain(); 238 | 239 | before(async () => { 240 | await polaris.poke.sendTransactionAsync(borrowERC20.address, { from: poker }); 241 | 242 | const borrowOracleTokenAddress = await polaris.oracleTokens.callAsync(borrowERC20.address); 243 | 244 | borrowOracleToken = OracleTokenContract.new(borrowOracleTokenAddress, provider, TX_DEFAULTS); 245 | 246 | const currentExchangeBalance = await web3Wrapper.getBalanceInWeiAsync(borrowUniswapExchange.address); 247 | const increaseByLessThanOnePercent = new BigNumber(currentExchangeBalance.times(0.009).toFixed(0)); 248 | 249 | await web3Wrapper.sendTransactionAsync({ 250 | from: poker, 251 | value: increaseByLessThanOnePercent, 252 | to: borrowUniswapExchange.address 253 | }); 254 | 255 | await web3Wrapper.increaseTimeAsync(+pendingPeriod.plus(1)); 256 | await web3Wrapper.mineBlockAsync(); 257 | 258 | await polaris.poke.sendTransactionAsync(borrowERC20.address, { from: poker }); 259 | 260 | const medianizer = await polaris.getMedianizer.callAsync(borrowERC20.address); 261 | previousTailIndex = medianizer.tail; 262 | 263 | pokerBalanceBefore = await borrowOracleToken.balanceOf.callAsync(poker); 264 | 265 | await web3Wrapper.increaseTimeAsync(+pendingPeriod.plus(1)); 266 | await web3Wrapper.mineBlockAsync(); 267 | 268 | await polaris.poke.sendTransactionAsync(borrowERC20.address, { from: poker }); 269 | }); 270 | 271 | it('should not reward poke', async () => { 272 | const afterBalance = await borrowOracleToken.balanceOf.callAsync(poker); 273 | 274 | expect(afterBalance).to.be.bignumber.equal(pokerBalanceBefore); 275 | }); 276 | 277 | it('should increase tail index by 1', async () => { 278 | const medianizer = await polaris.getMedianizer.callAsync(borrowERC20.address); 279 | const tailAfter = +medianizer.tail; 280 | const expectedTailIndex = (previousTailIndex + 1) % maxCheckpoints; 281 | 282 | expect(tailAfter).to.equal(expectedTailIndex); 283 | }); 284 | }); 285 | }); 286 | } 287 | -------------------------------------------------------------------------------- /test/integration/scenarios/subscribers.spec.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from '@0x/utils'; 2 | import { Web3Wrapper } from '@0x/web3-wrapper'; 3 | import { Provider, TransactionReceiptWithDecodedLogs, Transaction } from 'ethereum-types'; 4 | import * as _ from 'lodash'; 5 | 6 | import { chai } from '../../utils/chai_setup'; 7 | import { 8 | ETH_ADDRESS, 9 | ONE_ETH_IN_WEI, 10 | TX_DEFAULTS, 11 | ONE_MONTH_IN_SECONDS, 12 | ONE_DAY_IN_SECONDS 13 | } from '../../utils/constants'; 14 | 15 | import { 16 | MockSubscriberContract, 17 | MockERC20Contract, 18 | OracleTokenContract, 19 | PolarisContract 20 | } from '../../../build/wrappers'; 21 | 22 | const { expect } = chai; 23 | 24 | export function subscribersTests(snapshotBlockchain: () => void) { 25 | describe('#subscribers', async () => { 26 | let borrowERC20: MockERC20Contract; 27 | let borrowOracleToken: OracleTokenContract; 28 | let collateralERC20: MockERC20Contract; 29 | let subscriber: MockSubscriberContract; 30 | let polaris: PolarisContract; 31 | 32 | let poker: string; 33 | 34 | let subscriptionFee: BigNumber; 35 | 36 | let web3Wrapper: Web3Wrapper; 37 | let provider: Provider; 38 | 39 | snapshotBlockchain(); 40 | 41 | before(async function() { 42 | ({ borrowERC20, collateralERC20, poker, subscriber, polaris } = this.protocol); 43 | 44 | provider = this.provider; 45 | web3Wrapper = new Web3Wrapper(provider); 46 | 47 | subscriptionFee = await polaris.MONTHLY_SUBSCRIPTION_FEE.callAsync(); 48 | 49 | await polaris.poke.sendTransactionAsync(borrowERC20.address, { 50 | from: poker 51 | }); 52 | await polaris.poke.sendTransactionAsync(collateralERC20.address, { 53 | from: poker 54 | }); 55 | 56 | const borrowOracleTokenAddress = await polaris.oracleTokens.callAsync(borrowERC20.address); 57 | 58 | borrowOracleToken = OracleTokenContract.new(borrowOracleTokenAddress, provider, TX_DEFAULTS); 59 | }); 60 | 61 | describe('Subscribing to feed', () => { 62 | it('non-subscriber cannot read median', async () => { 63 | expect( 64 | subscriber.getDestAmount.callAsync(borrowERC20.address, ETH_ADDRESS, ONE_ETH_IN_WEI) 65 | ).to.be.rejectedWith('Polaris::getDestAmount: Not subscribed'); 66 | }); 67 | 68 | it('non-subscriber cannot read checkpoint data', async () => { 69 | expect(subscriber.getMedianizer.callAsync(borrowERC20.address)).to.be.rejectedWith( 70 | 'Polaris::getMedianizer: Not subscribed' 71 | ); 72 | }); 73 | 74 | it('subscriber can read median', async () => { 75 | await subscriber.subscribe.sendTransactionAsync(borrowERC20.address, { 76 | value: subscriptionFee 77 | }); 78 | 79 | const expectedDestAmount = await polaris.getDestAmount.callAsync( 80 | borrowERC20.address, 81 | ETH_ADDRESS, 82 | ONE_ETH_IN_WEI 83 | ); 84 | 85 | const subscriberDestAmount = await subscriber.getDestAmount.callAsync( 86 | borrowERC20.address, 87 | ETH_ADDRESS, 88 | ONE_ETH_IN_WEI 89 | ); 90 | 91 | expect(subscriberDestAmount).to.be.bignumber.equal(expectedDestAmount); 92 | }); 93 | 94 | it('subscriber can read checkpoint data', async () => { 95 | await subscriber.subscribe.sendTransactionAsync(borrowERC20.address, { 96 | value: subscriptionFee 97 | }); 98 | 99 | expect(subscriber.getMedianizer.callAsync(borrowERC20.address)).to.be.eventually.fulfilled; 100 | }); 101 | 102 | it('subscriber cannot read median if does not subscribed to both asset streams', async () => { 103 | await subscriber.subscribe.sendTransactionAsync(borrowERC20.address, { 104 | value: subscriptionFee 105 | }); 106 | 107 | expect( 108 | subscriber.getDestAmount.callAsync( 109 | borrowERC20.address, 110 | collateralERC20.address, 111 | ONE_ETH_IN_WEI 112 | ) 113 | ).to.be.rejectedWith('Polaris::getDestAmount: Not subscribed'); 114 | }); 115 | 116 | it('subscriber can read median if subscribed to both asset streams', async () => { 117 | await subscriber.subscribe.sendTransactionAsync(borrowERC20.address, { 118 | value: subscriptionFee 119 | }); 120 | 121 | await subscriber.subscribe.sendTransactionAsync(collateralERC20.address, { 122 | value: subscriptionFee 123 | }); 124 | 125 | const expectedDestAmount = await polaris.getDestAmount.callAsync( 126 | borrowERC20.address, 127 | collateralERC20.address, 128 | ONE_ETH_IN_WEI 129 | ); 130 | 131 | const subscriberDestAmount = await subscriber.getDestAmount.callAsync( 132 | borrowERC20.address, 133 | collateralERC20.address, 134 | ONE_ETH_IN_WEI 135 | ); 136 | 137 | expect(subscriberDestAmount).to.be.bignumber.equal(expectedDestAmount); 138 | }); 139 | }); 140 | 141 | describe('#subscribe', () => { 142 | it('cannot deposit less than a month', async () => { 143 | const oneMonthOfFee = new BigNumber(subscriptionFee.div(12).toFixed(0)); 144 | 145 | expect( 146 | subscriber.subscribe.sendTransactionAsync(borrowERC20.address, { 147 | value: oneMonthOfFee.sub(1) 148 | }) 149 | ).to.be.rejectedWith('Polaris::subscribe: Account balance is below the minimum'); 150 | }); 151 | }); 152 | 153 | describe('#unsubscribe', () => { 154 | let expectedWithdrawAmount: BigNumber; 155 | let subscriberOracleBalanceBefore: BigNumber; 156 | let subscriberEthBalanceBefore: BigNumber; 157 | 158 | snapshotBlockchain(); 159 | 160 | before(async () => { 161 | const subscriptionDeposit = subscriptionFee.mul(2); 162 | await subscriber.subscribe.sendTransactionAsync(borrowERC20.address, { 163 | value: subscriptionDeposit 164 | }); 165 | 166 | expectedWithdrawAmount = subscriptionFee; 167 | const withdrawAmount = expectedWithdrawAmount.plus(10); 168 | 169 | const account = await polaris.getAccount.callAsync(borrowERC20.address, subscriber.address); 170 | subscriberOracleBalanceBefore = account.balance; 171 | subscriberEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(subscriber.address); 172 | 173 | await subscriber.unsubscribe.sendTransactionAsync(borrowERC20.address, withdrawAmount); 174 | }); 175 | 176 | it('should leave subscriber oracle balance with at least minimum upon unsubscribe', async () => { 177 | const account = await polaris.getAccount.callAsync(borrowERC20.address, subscriber.address); 178 | const balanceAfter = account.balance; 179 | const actualWithdrawAmount = subscriberOracleBalanceBefore.sub(balanceAfter); 180 | 181 | expect(actualWithdrawAmount).to.be.bignumber.equal(expectedWithdrawAmount); 182 | }); 183 | 184 | it('should increase subscriber ETH balance by withdraw amount', async () => { 185 | const balanceAfter = await web3Wrapper.getBalanceInWeiAsync(subscriber.address); 186 | const expectedBalance = subscriberEthBalanceBefore.plus(expectedWithdrawAmount); 187 | 188 | expect(balanceAfter).to.be.bignumber.equal(expectedBalance); 189 | }); 190 | }); 191 | 192 | describe('#collect', () => { 193 | describe('should collect appropriate fee amount', async () => { 194 | let polarisEthBalanceBefore: BigNumber; 195 | let oracleTokenEthBalanceBefore: BigNumber; 196 | let owedAmount: BigNumber; 197 | let txReceipt: TransactionReceiptWithDecodedLogs; 198 | 199 | snapshotBlockchain(); 200 | 201 | before(async () => { 202 | await subscriber.subscribe.sendTransactionAsync(borrowERC20.address, { 203 | value: subscriptionFee 204 | }); 205 | 206 | const timeElapsedInSeconds = ONE_DAY_IN_SECONDS; 207 | 208 | await web3Wrapper.increaseTimeAsync(+timeElapsedInSeconds); 209 | await web3Wrapper.mineBlockAsync(); 210 | 211 | owedAmount = new BigNumber( 212 | subscriptionFee 213 | .mul(timeElapsedInSeconds) 214 | .div(ONE_MONTH_IN_SECONDS) 215 | .toFixed(0, BigNumber.ROUND_FLOOR) 216 | ); 217 | 218 | polarisEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(polaris.address); 219 | 220 | oracleTokenEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync( 221 | borrowOracleToken.address 222 | ); 223 | 224 | await polaris.collect.callAsync(borrowERC20.address, subscriber.address); 225 | 226 | const txHash = await polaris.collect.sendTransactionAsync( 227 | borrowERC20.address, 228 | subscriber.address 229 | ); 230 | txReceipt = await web3Wrapper.awaitTransactionSuccessAsync(txHash); 231 | }); 232 | 233 | it('should decrease subscriber balance by fee amount', async () => { 234 | const account = await polaris.getAccount.callAsync( 235 | borrowERC20.address, 236 | subscriber.address 237 | ); 238 | const balance = account.balance; 239 | const expectedBalance = subscriptionFee.sub(owedAmount); 240 | 241 | expect(balance).to.be.bignumber.equal(expectedBalance); 242 | }); 243 | 244 | it('should set subscriber payments to correct timestamp', async () => { 245 | const account = await polaris.getAccount.callAsync( 246 | borrowERC20.address, 247 | subscriber.address 248 | ); 249 | const timestamp = account.collectionTimestamp; 250 | const expectedTimestamp = await web3Wrapper.getBlockTimestampAsync(txReceipt.blockNumber); 251 | 252 | expect(timestamp).to.be.bignumber.equal(expectedTimestamp); 253 | }); 254 | 255 | it('should decrease Polaris ETH balance by owed amount', async () => { 256 | const balance = await await web3Wrapper.getBalanceInWeiAsync(polaris.address); 257 | const expectedBalance = polarisEthBalanceBefore.sub(owedAmount); 258 | 259 | expect(balance).to.be.bignumber.equal(expectedBalance); 260 | }); 261 | 262 | it('should increase OracleToken ETH balance by owed amount', async () => { 263 | const balance = await await web3Wrapper.getBalanceInWeiAsync(borrowOracleToken.address); 264 | const expectedBalance = oracleTokenEthBalanceBefore.plus(owedAmount); 265 | 266 | expect(balance).to.be.bignumber.equal(expectedBalance); 267 | }); 268 | }); 269 | }); 270 | }); 271 | } 272 | -------------------------------------------------------------------------------- /test/utils/chai_setup.ts: -------------------------------------------------------------------------------- 1 | import * as _chai from 'chai'; 2 | import chaiAsPromised = require('chai-as-promised'); 3 | import ChaiBigNumber = require('chai-bignumber'); 4 | import * as dirtyChai from 'dirty-chai'; 5 | 6 | _chai.config.includeStack = true; 7 | _chai.use(ChaiBigNumber()); 8 | _chai.use(dirtyChai); 9 | _chai.use(chaiAsPromised); 10 | 11 | export const chai = _chai; 12 | -------------------------------------------------------------------------------- /test/utils/constants.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from '@0x/utils'; 2 | import { Web3Wrapper } from '@0x/web3-wrapper'; 3 | import { devConstants } from '@marbleprotocol/dev-utils'; 4 | import * as ethUtil from 'ethereumjs-util'; 5 | import * as _ from 'lodash'; 6 | 7 | const TESTRPC_PRIVATE_KEYS_STRINGS = [ 8 | '0xf2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e0164837257d', 9 | '0x5d862464fe9303452126c8bc94274b8c5f9874cbd219789b3eb2128075a76f72', 10 | '0xdf02719c4df8b9b8ac7f551fcb5d9ef48fa27eef7a66453879f4d8fdc6e78fb1', 11 | '0xff12e391b79415e941a94de3bf3a9aee577aed0731e297d5cfa0b8a1e02fa1d0', 12 | '0x752dd9cf65e68cfaba7d60225cbdbc1f4729dd5e5507def72815ed0d8abc6249', 13 | '0xefb595a0178eb79a8df953f87c5148402a224cdf725e88c0146727c6aceadccd', 14 | '0x83c6d2cc5ddcf9711a6d59b417dc20eb48afd58d45290099e5987e3d768f328f', 15 | '0xbb2d3f7c9583780a7d3904a2f55d792707c345f21de1bacb2d389934d82796b2', 16 | '0xb2fd4d29c1390b71b8795ae81196bfd60293adf99f9d32a0aff06288fcdac55f', 17 | '0x23cb7121166b9a2f93ae0b7c05bde02eae50d64449b2cbb42bc84e9d38d6cc89' 18 | ]; 19 | 20 | const port = 8545; 21 | 22 | export const AWAIT_TRANSACTION_MINED_MS = 0; 23 | export const BASE_16 = 16; 24 | export const DEFAULT_SALT = new BigNumber(420); 25 | export const INVALID_OPCODE = 'invalid opcode'; 26 | export const EVM_REVERT = 'revert'; 27 | export const NULL_ADDRESS = '0x0000000000000000000000000000000000000000'; 28 | export const ETH_ADDRESS = '0x0000000000000000000000000000000000000000'; 29 | export const EXAMPLE_BYTES_1 = '0x0000000000000000000000000000000000000000000000000000000000000000'; 30 | export const EXAMPLE_BYTES_2 = '0x0000000000000000000000000000000000000000000000000000000000000001'; 31 | export const UNLIMITED_ALLOWANCE_IN_BASE_UNITS = new BigNumber(2).pow(256).minus(1); 32 | export const TESTRPC_PRIVATE_KEYS = _.map(TESTRPC_PRIVATE_KEYS_STRINGS, privateKeyString => 33 | ethUtil.toBuffer(privateKeyString) 34 | ); 35 | export const INITIAL_ERC20_BALANCE = Web3Wrapper.toBaseUnitAmount(new BigNumber(10000), 18); 36 | export const INITIAL_ERC20_ALLOWANCE = Web3Wrapper.toBaseUnitAmount(new BigNumber(10000), 18); 37 | export const STATIC_ORDER_PARAMS = { 38 | makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18), 39 | takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(200), 18), 40 | makerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 18), 41 | takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 18) 42 | }; 43 | export const ZERO_AMOUNT = new BigNumber(0); 44 | export const PERCENTAGE_DENOMINATOR = new BigNumber(10).pow(18); 45 | export const ONE_ETH_IN_WEI = new BigNumber(10).pow(18); 46 | export const ONE_WEI = new BigNumber(1); 47 | export const ONE_HOUR_IN_SECONDS = new BigNumber(60 * 60); 48 | export const ONE_DAY_IN_SECONDS = new BigNumber(24 * 60 * 60); 49 | export const ONE_MONTH_IN_SECONDS = new BigNumber(30 * 24 * 60 * 60); 50 | export const ONE_YEAR_IN_SECONDS = new BigNumber(365 * 24 * 60 * 60); 51 | export const MAX_UINT = new BigNumber( 52 | '115792089237316195423570985008687907853269984665640564039457584007913129639935' 53 | ); 54 | export const RPC_URL = `http://localhost:${port}`; 55 | export const GAS_LIMIT = 8000000; 56 | export const RPC_PORT = port; 57 | export const NETWORK_ID = 50; 58 | export const INFURA_MAINNET = 'https://mainnet.infura.io/'; 59 | export const BLOCK_NUMBER = 6000000; 60 | export const MNEMONIC = 61 | 'concert load couple harbor equip island argue ramp clarify fence smart topic'; 62 | export const WETH_ADDRESS = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'; 63 | export const DAI_ADDRESS = '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359'; 64 | export const MANA_ADDRESS = '0x0f5d2fb29fb7d3cfee444a200298f468908cc942'; 65 | export const KYBER_ETH_ADDRESS = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'; 66 | export const KYBER_EXCHANGE_ADDRESS = '0x91a502C678605fbCe581eae053319747482276b9'; 67 | export const POINT_ZERO_ONE_PERCENT_MARGIN = new BigNumber(0.0001); // 0.01% 68 | export const BORROW_AMOUNT = new BigNumber('1e18'); 69 | export const DEPOSIT_AMOUNT = BORROW_AMOUNT.times(5); 70 | export const WITHDRAW_AMOUNT = BORROW_AMOUNT.times(3); 71 | export const COLLATERAL_RATIO = new BigNumber(1.5); 72 | export const COLLATERAL_AMOUNT = BORROW_AMOUNT.times(COLLATERAL_RATIO); 73 | export const BASIS_POINTS = new BigNumber(10000); 74 | export const RESERVE_RATIO = BASIS_POINTS.div(5); 75 | export const ONE_QUARTER_PERCENT_INTEREST_RATE = new BigNumber(25); 76 | export const TWO_PERCENT_INTEREST_RATE = new BigNumber('2e16'); 77 | export const FIVE_PERCENT_INTEREST_RATE = new BigNumber('5e16'); 78 | export const ONE_HUNDRED_PERCENT_INTEREST_RATE = new BigNumber('1e18'); 79 | export const ASSET_PRICE = new BigNumber('1000000000000000000'); 80 | export const JANUARY_1_2020 = new BigNumber('1577836800'); 81 | export const JANUARY_1_2025 = new BigNumber('1735689600'); 82 | export const TX_DEFAULTS = { 83 | from: devConstants.TESTRPC_FIRST_ADDRESS, 84 | gas: GAS_LIMIT 85 | }; 86 | export const EMPTY_DATA = '0x'; 87 | -------------------------------------------------------------------------------- /test/utils/deployer.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from '@0x/utils'; 2 | import { Web3Wrapper } from '@0x/web3-wrapper'; 3 | import { Provider, TxData } from 'ethereum-types'; 4 | import { PriceOracleProtocol } from './types'; 5 | 6 | import { 7 | MockSubscriberContract, 8 | MockUniswapFactoryContract, 9 | MockERC20Contract, 10 | PolarisContract 11 | } from '../../build/wrappers'; 12 | 13 | export const deployMockERC20 = async ( 14 | unsiwapFactory: MockUniswapFactoryContract, 15 | provider: Provider, 16 | txDefaults: Partial 17 | ) => { 18 | const token = await MockERC20Contract.deployAsync(provider, txDefaults); 19 | await unsiwapFactory.createExchange.sendTransactionAsync(token.address); 20 | const exchangeAddress = await unsiwapFactory.getExchange.callAsync(token.address); 21 | 22 | await token.issueTo.sendTransactionAsync( 23 | exchangeAddress, 24 | new BigNumber(100000000000000000000000000000) 25 | ); 26 | 27 | const web3Wrapper = new Web3Wrapper(provider); 28 | const addresses = await web3Wrapper.getAvailableAddressesAsync(); 29 | 30 | await web3Wrapper.sendTransactionAsync({ 31 | from: addresses[addresses.length - 1], 32 | value: Web3Wrapper.toWei(new BigNumber(99)), 33 | to: exchangeAddress 34 | }); 35 | 36 | return token; 37 | }; 38 | 39 | export async function deployPriceOracle( 40 | provider: Provider, 41 | txDefaults: Partial 42 | ): Promise { 43 | const web3Wrapper = new Web3Wrapper(provider); 44 | const addresses = await web3Wrapper.getAvailableAddressesAsync(); 45 | const [, poker] = addresses; 46 | 47 | const uniswapFactory = await MockUniswapFactoryContract.deployAsync(provider, txDefaults); 48 | 49 | const borrowERC20 = await deployMockERC20(uniswapFactory, provider, txDefaults); 50 | const collateralERC20 = await deployMockERC20(uniswapFactory, provider, txDefaults); 51 | 52 | const polaris = await PolarisContract.deployAsync(provider, txDefaults, uniswapFactory.address); 53 | 54 | const subscriber = await MockSubscriberContract.deployAsync( 55 | provider, 56 | txDefaults, 57 | polaris.address 58 | ); 59 | 60 | return { 61 | addresses, 62 | borrowERC20, 63 | collateralERC20, 64 | poker, 65 | uniswapFactory, 66 | polaris, 67 | subscriber 68 | }; 69 | } 70 | -------------------------------------------------------------------------------- /test/utils/types.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from '@0x/utils'; 2 | 3 | import { 4 | MockSubscriberContract, 5 | MockUniswapFactoryContract, 6 | MockERC20Contract, 7 | PolarisContract 8 | } from '../../build/wrappers'; 9 | 10 | export interface Account { 11 | balance: BigNumber; 12 | lastPaymentTimestamp: BigNumber; 13 | } 14 | 15 | export interface Checkpoint { 16 | ethReserve: BigNumber; 17 | tokenReserve: BigNumber; 18 | } 19 | 20 | export interface CheckpointData { 21 | tail: number; 22 | medianEthReserve: BigNumber; 23 | medianTokenReserve: BigNumber; 24 | minMaxHeap: number[]; 25 | pokeQueue: Checkpoint[]; 26 | } 27 | 28 | export interface PriceOracleProtocol { 29 | addresses: string[]; 30 | borrowERC20: MockERC20Contract; 31 | collateralERC20: MockERC20Contract; 32 | poker: string; 33 | polaris: PolarisContract; 34 | subscriber: MockSubscriberContract; 35 | uniswapFactory: MockUniswapFactoryContract; 36 | } 37 | -------------------------------------------------------------------------------- /truffle.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | networks: { 3 | development: { 4 | host: 'localhost', 5 | port: 8545, 6 | network_id: '*', // Match any network id 7 | gas: 100000000, // High 8 | gasPrice: 0 9 | } 10 | }, 11 | compilers: { 12 | solc: { 13 | version: '0.5.2', 14 | settings: { 15 | optimizer: { 16 | enabled: true, 17 | runs: 200 18 | } 19 | } 20 | } 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "lib": ["es2017", "dom"], 6 | "sourceMap": true, 7 | "experimentalDecorators": true, 8 | "downlevelIteration": true, 9 | "noImplicitReturns": true, 10 | "pretty": true, 11 | "skipLibCheck": true, 12 | "strict": true, 13 | "outDir": "lib", 14 | "baseUrl": ".", 15 | "declaration": false, 16 | "allowJs": true, 17 | "typeRoots": ["test/types", "node_modules/@0x/typescript-typings/types", "node_modules/@types"] 18 | }, 19 | "include": [ 20 | "./globals.d.ts", 21 | "./build/**/*", 22 | "./src/**/*", 23 | "./test/**/*", 24 | "./scripts/**/*" 25 | ], 26 | "exclude": [ 27 | "node_modules" 28 | ] 29 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@0x/tslint-config"], 3 | } --------------------------------------------------------------------------------