/** * Source Code first verified at https://etherscan.io on Wednesday, March 27, 2019 (UTC) */ pragma solidity 0.5.2; /** * @title ERC20 interface * @dev see https://github.com/ethereum/EIPs/issues/20 */ interface IERC20 { function transfer(address to, uint256 value) external returns (bool); function approve(address spender, uint256 value) external returns (bool); function transferFrom(address from, address to, uint256 value) external returns (bool); function totalSupply() external view returns (uint256); function balanceOf(address who) external view returns (uint256); function allowance(address owner, address spender) external view returns (uint256); event Transfer(address indexed from, address indexed to, uint256 value); event Approval(address indexed owner, address indexed spender, uint256 value); } /** * @title Math * @dev Assorted math operations */ library Math { /** * @dev Returns the largest of two numbers. */ function max(uint256 a, uint256 b) internal pure returns (uint256) { return a >= b ? a : b; } /** * @dev Returns the smallest of two numbers. */ function min(uint256 a, uint256 b) internal pure returns (uint256) { return a < b ? a : b; } /** * @dev Calculates the average of two numbers. Since these are integers, * averages of an even and odd number cannot be represented, and will be * rounded down. */ function average(uint256 a, uint256 b) internal pure returns (uint256) { // (a + b) / 2 can overflow, so we distribute return (a / 2) + (b / 2) + ((a % 2 + b % 2) / 2); } } /** * @title SafeMath * @dev Unsigned math operations with safety checks that revert on error */ library SafeMath { /** * @dev Multiplies two unsigned integers, reverts on overflow. */ function mul(uint256 a, uint256 b) internal pure returns (uint256) { // Gas optimization: this is cheaper than requiring 'a' not being zero, but the // benefit is lost if 'b' is also tested. // See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522 if (a == 0) { return 0; } uint256 c = a * b; require(c / a == b, "SafeMath#mul: Integer overflow"); return c; } /** * @dev Integer division of two unsigned integers truncating the quotient, reverts on division by zero. */ function div(uint256 a, uint256 b) internal pure returns (uint256) { // Solidity only automatically asserts when dividing by 0 require(b > 0, "SafeMath#div: Invalid divisor zero"); uint256 c = a / b; // assert(a == b * c + a % b); // There is no case in which this doesn't hold return c; } /** * @dev Subtracts two unsigned integers, reverts on overflow (i.e. if subtrahend is greater than minuend). */ function sub(uint256 a, uint256 b) internal pure returns (uint256) { require(b <= a, "SafeMath#sub: Integer underflow"); uint256 c = a - b; return c; } /** * @dev Adds two unsigned integers, reverts on overflow. */ function add(uint256 a, uint256 b) internal pure returns (uint256) { uint256 c = a + b; require(c >= a, "SafeMath#add: Integer overflow"); return c; } /** * @dev Divides two unsigned integers and returns the remainder (unsigned integer modulo), * reverts when dividing by zero. */ function mod(uint256 a, uint256 b) internal pure returns (uint256) { require(b != 0, "SafeMath#mod: Invalid divisor zero"); return a % b; } } contract IUniswapExchange { // Address of ERC20 token sold on this exchange function tokenAddress() external view returns (address token); // Address of Uniswap Factory function factoryAddress() external view returns (address factory); // Provide Liquidity function addLiquidity(uint256 min_liquidity, uint256 max_tokens, uint256 deadline) external payable returns (uint256); function removeLiquidity(uint256 amount, uint256 min_eth, uint256 min_tokens, uint256 deadline) external returns (uint256, uint256); // Get Prices function getEthToTokenInputPrice(uint256 eth_sold) external view returns (uint256 tokens_bought); function getEthToTokenOutputPrice(uint256 tokens_bought) external view returns (uint256 eth_sold); function getTokenToEthInputPrice(uint256 tokens_sold) external view returns (uint256 eth_bought); function getTokenToEthOutputPrice(uint256 eth_bought) external view returns (uint256 tokens_sold); // Trade ETH to ERC20 function ethToTokenSwapInput(uint256 min_tokens, uint256 deadline) external payable returns (uint256 tokens_bought); function ethToTokenTransferInput(uint256 min_tokens, uint256 deadline, address recipient) external payable returns (uint256 tokens_bought); function ethToTokenSwapOutput(uint256 tokens_bought, uint256 deadline) external payable returns (uint256 eth_sold); function ethToTokenTransferOutput(uint256 tokens_bought, uint256 deadline, address recipient) external payable returns (uint256 eth_sold); // Trade ERC20 to ETH function tokenToEthSwapInput(uint256 tokens_sold, uint256 min_eth, uint256 deadline) external returns (uint256 eth_bought); function tokenToEthTransferInput(uint256 tokens_sold, uint256 min_tokens, uint256 deadline, address recipient) external returns (uint256 eth_bought); function tokenToEthSwapOutput(uint256 eth_bought, uint256 max_tokens, uint256 deadline) external returns (uint256 tokens_sold); function tokenToEthTransferOutput(uint256 eth_bought, uint256 max_tokens, uint256 deadline, address recipient) external returns (uint256 tokens_sold); // Trade ERC20 to ERC20 function tokenToTokenSwapInput(uint256 tokens_sold, uint256 min_tokens_bought, uint256 min_eth_bought, uint256 deadline, address token_addr) external returns (uint256 tokens_bought); 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); function tokenToTokenSwapOutput(uint256 tokens_bought, uint256 max_tokens_sold, uint256 max_eth_sold, uint256 deadline, address token_addr) external returns (uint256 tokens_sold); 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); // Trade ERC20 to Custom Pool function tokenToExchangeSwapInput(uint256 tokens_sold, uint256 min_tokens_bought, uint256 min_eth_bought, uint256 deadline, address exchange_addr) external returns (uint256 tokens_bought); 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); function tokenToExchangeSwapOutput(uint256 tokens_bought, uint256 max_tokens_sold, uint256 max_eth_sold, uint256 deadline, address exchange_addr) external returns (uint256 tokens_sold); 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); // ERC20 comaptibility for liquidity tokens bytes32 public name; bytes32 public symbol; uint256 public decimals; function transfer(address _to, uint256 _value) external returns (bool); function transferFrom(address _from, address _to, uint256 value) external returns (bool); function approve(address _spender, uint256 _value) external returns (bool); function allowance(address _owner, address _spender) external view returns (uint256); function balanceOf(address _owner) external view returns (uint256); // Never use function setup(address token_addr) external; } contract IUniswapFactory { // Public Variables address public exchangeTemplate; uint256 public tokenCount; // Create Exchange function createExchange(address token) external returns (address payable exchange); // Get Exchange and Token Info function getExchange(address token) external view returns (address payable exchange); function getToken(address exchange) external view returns (address token); function getTokenWithId(uint256 tokenId) external view returns (address token); } /** * @title Standard ERC20 token * * @dev Implementation of the basic standard token. * https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md * Originally based on code by FirstBlood: * https://github.com/Firstbloodio/token/blob/master/smart_contract/FirstBloodToken.sol * * This implementation emits additional Approval events, allowing applications to reconstruct the allowance status for * all accounts just by listening to said events. Note that this isn't required by the specification, and other * compliant implementations may not do it. */ contract ERC20 is IERC20 { using SafeMath for uint256; mapping (address => uint256) private _balances; mapping (address => mapping (address => uint256)) private _allowed; uint256 private _totalSupply; /** * @dev Total number of tokens in existence */ function totalSupply() public view returns (uint256) { return _totalSupply; } /** * @dev Gets the balance of the specified address. * @param owner The address to query the balance of. * @return An uint256 representing the amount owned by the passed address. */ function balanceOf(address owner) public view returns (uint256) { return _balances[owner]; } /** * @dev Function to check the amount of tokens that an owner allowed to a spender. * @param owner address The address which owns the funds. * @param spender address The address which will spend the funds. * @return A uint256 specifying the amount of tokens still available for the spender. */ function allowance(address owner, address spender) public view returns (uint256) { return _allowed[owner][spender]; } /** * @dev Transfer token for a specified address * @param to The address to transfer to. * @param value The amount to be transferred. */ function transfer(address to, uint256 value) public returns (bool) { _transfer(msg.sender, to, value); return true; } /** * @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender. * Beware that changing an allowance with this method brings the risk that someone may use both the old * and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this * race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards: * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 * @param spender The address which will spend the funds. * @param value The amount of tokens to be spent. */ function approve(address spender, uint256 value) public returns (bool) { require(spender != address(0), "ERC20#approve: Cannot approve address zero"); _allowed[msg.sender][spender] = value; emit Approval(msg.sender, spender, value); return true; } /** * @dev Transfer tokens from one address to another. * Note that while this function emits an Approval event, this is not required as per the specification, * and other compliant implementations may not emit the event. * @param from address The address which you want to send tokens from * @param to address The address which you want to transfer to * @param value uint256 the amount of tokens to be transferred */ function transferFrom(address from, address to, uint256 value) public returns (bool) { _allowed[from][msg.sender] = _allowed[from][msg.sender].sub(value); _transfer(from, to, value); emit Approval(from, msg.sender, _allowed[from][msg.sender]); return true; } /** * @dev Increase the amount of tokens that an owner allowed to a spender. * approve should be called when allowed_[_spender] == 0. To increment * allowed value is better to use this function to avoid 2 calls (and wait until * the first transaction is mined) * From MonolithDAO Token.sol * Emits an Approval event. * @param spender The address which will spend the funds. * @param addedValue The amount of tokens to increase the allowance by. */ function increaseAllowance(address spender, uint256 addedValue) public returns (bool) { require(spender != address(0), "ERC20#increaseAllowance: Cannot increase allowance for address zero"); _allowed[msg.sender][spender] = _allowed[msg.sender][spender].add(addedValue); emit Approval(msg.sender, spender, _allowed[msg.sender][spender]); return true; } /** * @dev Decrease the amount of tokens that an owner allowed to a spender. * approve should be called when allowed_[_spender] == 0. To decrement * allowed value is better to use this function to avoid 2 calls (and wait until * the first transaction is mined) * From MonolithDAO Token.sol * Emits an Approval event. * @param spender The address which will spend the funds. * @param subtractedValue The amount of tokens to decrease the allowance by. */ function decreaseAllowance(address spender, uint256 subtractedValue) public returns (bool) { require(spender != address(0), "ERC20#decreaseAllowance: Cannot decrease allowance for address zero"); _allowed[msg.sender][spender] = _allowed[msg.sender][spender].sub(subtractedValue); emit Approval(msg.sender, spender, _allowed[msg.sender][spender]); return true; } /** * @dev Transfer token for a specified addresses * @param from The address to transfer from. * @param to The address to transfer to. * @param value The amount to be transferred. */ function _transfer(address from, address to, uint256 value) internal { require(to != address(0), "ERC20#_transfer: Cannot transfer to address zero"); _balances[from] = _balances[from].sub(value); _balances[to] = _balances[to].add(value); emit Transfer(from, to, value); } /** * @dev Internal function that mints an amount of the token and assigns it to * an account. This encapsulates the modification of balances such that the * proper events are emitted. * @param account The account that will receive the created tokens. * @param value The amount that will be created. */ function _mint(address account, uint256 value) internal { require(account != address(0), "ERC20#_mint: Cannot mint to address zero"); _totalSupply = _totalSupply.add(value); _balances[account] = _balances[account].add(value); emit Transfer(address(0), account, value); } /** * @dev Internal function that burns an amount of the token of a given * account. * @param account The account whose tokens will be burnt. * @param value The amount that will be burnt. */ function _burn(address account, uint256 value) internal { require(account != address(0), "ERC20#_burn: Cannot burn from address zero"); _totalSupply = _totalSupply.sub(value); _balances[account] = _balances[account].sub(value); emit Transfer(account, address(0), value); } /** * @dev Internal function that burns an amount of the token of a given * account, deducting from the sender's allowance for said account. Uses the * internal burn function. * Emits an Approval event (reflecting the reduced allowance). * @param account The account whose tokens will be burnt. * @param value The amount that will be burnt. */ function _burnFrom(address account, uint256 value) internal { _allowed[account][msg.sender] = _allowed[account][msg.sender].sub(value); _burn(account, value); emit Approval(account, msg.sender, _allowed[account][msg.sender]); } } contract OracleToken is ERC20 { string public name = "Polaris Token"; string public symbol = "PLRS"; uint8 public decimals = 18; address public oracle; address public token; constructor(address _token) public payable { oracle = msg.sender; token = _token; } function () external payable {} function mint(address to, uint amount) public returns (bool) { require(msg.sender == oracle, "OracleToken::mint: Only Oracle can call mint"); _mint(to, amount); return true; } function redeem(uint amount) public { uint ethAmount = address(this).balance.mul(amount).div(totalSupply()); _burn(msg.sender, amount); msg.sender.transfer(ethAmount); } } pragma experimental ABIEncoderV2; contract Polaris { using Math for uint; using SafeMath for uint; event NewMedian(address indexed token, uint ethReserve, uint tokenReserve); event Subscribe(address indexed token, address indexed subscriber, uint amount); event Unsubscribe(address indexed token, address indexed subscriber, uint amount); uint8 public constant MAX_CHECKPOINTS = 15; // Reward for a successful poke, in oracle tokens uint public constant CHECKPOINT_REWARD = 1e18; // Conditions for checkpoint reward uint public constant MIN_PRICE_CHANGE = .01e18; // 1% uint public constant MAX_TIME_SINCE_LAST_CHECKPOINT = 3 hours; uint public constant PENDING_PERIOD = 3.5 minutes; address public constant ETHER = address(0); // Monthly subscription fee to subscribe to a single oracle uint public constant MONTHLY_SUBSCRIPTION_FEE = 5 ether; uint public constant ONE_MONTH_IN_SECONDS = 30 days; IUniswapFactory public uniswap; struct Account { uint balance; uint collectionTimestamp; } struct Checkpoint { uint ethReserve; uint tokenReserve; } struct Medianizer { uint8 tail; uint pendingStartTimestamp; uint latestTimestamp; Checkpoint[] prices; Checkpoint[] pending; Checkpoint median; } // Token => Subscriber => Account mapping (address => mapping (address => Account)) public accounts; // Token => Oracle Token (reward for poking) mapping (address => OracleToken) public oracleTokens; // Token => Medianizer mapping (address => Medianizer) private medianizers; constructor(IUniswapFactory _uniswap) public { uniswap = _uniswap; } /** * @dev Subscribe to read the price of a given token (e.g, DAI). * @param token The address of the token to subscribe to. */ function subscribe(address token) public payable { Account storage account = accounts[token][msg.sender]; _collect(token, account); account.balance = account.balance.add(msg.value); require(account.balance >= MONTHLY_SUBSCRIPTION_FEE, "Polaris::subscribe: Account balance is below the minimum"); emit Subscribe(token, msg.sender, msg.value); } /** * @dev Unsubscribe to a given token (e.g, DAI). * @param token The address of the token to unsubscribe from. * @param amount The requested amount to withdraw, in wei. * @return The actual amount withdrawn, in wei. */ function unsubscribe(address token, uint amount) public returns (uint) { Account storage account = accounts[token][msg.sender]; _collect(token, account); uint maxWithdrawAmount = account.balance.sub(MONTHLY_SUBSCRIPTION_FEE); uint actualWithdrawAmount = amount.min(maxWithdrawAmount); account.balance = account.balance.sub(actualWithdrawAmount); msg.sender.transfer(actualWithdrawAmount); emit Unsubscribe(token, msg.sender, actualWithdrawAmount); } /** * @dev Collect subscription fees from a subscriber. * @param token The address of the subscribed token to collect fees from. * @param who The address of the subscriber. */ function collect(address token, address who) public { Account storage account = accounts[token][who]; _collect(token, account); } /** * @dev Add a new price checkpoint. * @param token The address of the token to checkpoint. */ function poke(address token) public { require(_isHuman(), "Polaris::poke: Poke must be called by an externally owned account"); OracleToken oracleToken = oracleTokens[token]; // Get the current reserves from Uniswap Checkpoint memory checkpoint = _newCheckpoint(token); if (address(oracleToken) == address(0)) { _initializeMedianizer(token, checkpoint); } else { Medianizer storage medianizer = medianizers[token]; require(medianizer.latestTimestamp != block.timestamp, "Polaris::poke: Cannot poke more than once per block"); // See if checkpoint should be rewarded if (_willRewardCheckpoint(token, checkpoint)) { oracleToken.mint(msg.sender, CHECKPOINT_REWARD); } // If pending checkpoints are old, reset pending checkpoints if (block.timestamp.sub(medianizer.pendingStartTimestamp) > PENDING_PERIOD || medianizer.pending.length == MAX_CHECKPOINTS) { medianizer.pending.length = 0; medianizer.tail = (medianizer.tail + 1) % MAX_CHECKPOINTS; medianizer.pendingStartTimestamp = block.timestamp; } medianizer.latestTimestamp = block.timestamp; // Add the checkpoint to the pending array medianizer.pending.push(checkpoint); // Add the pending median to the prices array medianizer.prices[medianizer.tail] = _medianize(medianizer.pending); // Find and store the prices median medianizer.median = _medianize(medianizer.prices); emit NewMedian(token, medianizer.median.ethReserve, medianizer.median.tokenReserve); } } /** * @dev Get price data for a given token. * @param token The address of the token to query. * @return The price data struct. */ function getMedianizer(address token) public view returns (Medianizer memory) { require(_isSubscriber(accounts[token][msg.sender]) || _isHuman(), "Polaris::getMedianizer: Not subscribed"); return medianizers[token]; } /** * @notice This uses the x * y = k bonding curve to determine the destination amount based on the medianized price. * 𝝙x = (𝝙y * x) / (y + 𝝙y) * @dev Get the amount of destination token, based on a given amount of source token. * @param src The address of the source token. * @param dest The address of the destination token. * @param srcAmount The amount of the source token. * @return The amount of destination token. */ function getDestAmount(address src, address dest, uint srcAmount) public view returns (uint) { if (!_isHuman()) { require(src == ETHER || _isSubscriber(accounts[src][msg.sender]), "Polaris::getDestAmount: Not subscribed"); require(dest == ETHER || _isSubscriber(accounts[dest][msg.sender]), "Polaris::getDestAmount: Not subscribed"); } if (src == dest) { return srcAmount; } else if (src == ETHER) { Checkpoint memory median = medianizers[dest].median; return srcAmount.mul(median.tokenReserve).div(median.ethReserve.add(srcAmount)); } else if (dest == ETHER) { Checkpoint memory median = medianizers[src].median; return srcAmount.mul(median.ethReserve).div(median.tokenReserve.add(srcAmount)); } else { Checkpoint memory srcMedian = medianizers[src].median; Checkpoint memory destMedian = medianizers[dest].median; uint ethAmount = srcAmount.mul(srcMedian.ethReserve).div(srcMedian.tokenReserve.add(srcAmount)); return ethAmount.mul(destMedian.ethReserve).div(destMedian.tokenReserve.add(ethAmount)); } } /** * @dev Determine whether a given checkpoint would be rewarded with newly minted oracle tokens. * @param token The address of the token to query checkpoint for. * @return True if given checkpoint satisfies any of the following: * Less than required checkpoints exist to calculate a valid median * Exceeds max time since last checkpoint * Exceeds minimum price change from median AND no pending checkpoints * Exceeds minimum percent change from pending checkpoints median * Exceeds minimum percent change from last checkpoint */ function willRewardCheckpoint(address token) public view returns (bool) { Checkpoint memory checkpoint = _newCheckpoint(token); return _willRewardCheckpoint(token, checkpoint); } /** * @dev Get the account for a given subscriber of a token feed. * @param token The token to query the account of the given subscriber. * @param who The subscriber to query the account of the given token feed. * @return The account of the subscriber of the given token feed. */ function getAccount(address token, address who) public view returns (Account memory) { return accounts[token][who]; } /** * @dev Get the owed amount for a given subscriber of a token feed. * @param token The token to query the owed amount of the given subscriber. * @param who The subscriber to query the owed amount for the given token feed. * @return The owed amount of the subscriber of the given token feed. */ function getOwedAmount(address token, address who) public view returns (uint) { Account storage account = accounts[token][who]; return _getOwedAmount(account); } /** * @dev Update the subscriber balance of a given token feed. * @param token The token to collect subscription revenues for. * @param account The subscriber account to collect subscription revenues from. */ function _collect(address token, Account storage account) internal { if (account.balance == 0) { account.collectionTimestamp = block.timestamp; return; } uint owedAmount = _getOwedAmount(account); OracleToken oracleToken = oracleTokens[token]; // If the subscriber does not have enough, collect the remaining balance if (owedAmount >= account.balance) { address(oracleToken).transfer(account.balance); account.balance = 0; } else { address(oracleToken).transfer(owedAmount); account.balance = account.balance.sub(owedAmount); } account.collectionTimestamp = block.timestamp; } /** * @dev Initialize the medianizer * @param token The token to initialize the medianizer for. * @param checkpoint The new checkpoint to initialize the medianizer with. */ function _initializeMedianizer(address token, Checkpoint memory checkpoint) internal { address payable exchange = uniswap.getExchange(token); require(exchange != address(0), "Polaris::_initializeMedianizer: Token must exist on Uniswap"); OracleToken oracleToken = new OracleToken(token); oracleTokens[token] = oracleToken; // Reward additional oracle tokens for the first poke to compensate for extra gas costs oracleToken.mint(msg.sender, CHECKPOINT_REWARD.mul(10)); Medianizer storage medianizer = medianizers[token]; medianizer.pending.push(checkpoint); medianizer.median = checkpoint; medianizer.latestTimestamp = block.timestamp; medianizer.pendingStartTimestamp = block.timestamp; // Hydrate prices queue for (uint i = 0; i < MAX_CHECKPOINTS; i++) { medianizer.prices.push(checkpoint); } } /** * @dev Find the median given an array of checkpoints. * @param checkpoints The array of checkpoints to find the median. * @return The median checkpoint within the given array. */ function _medianize(Checkpoint[] memory checkpoints) internal pure returns (Checkpoint memory) { // To minimize complexity, return the higher of the two middle checkpoints in even-sized arrays instead of the average. uint k = checkpoints.length.div(2); uint left = 0; uint right = checkpoints.length.sub(1); while (left < right) { uint pivotIndex = left.add(right).div(2); Checkpoint memory pivotCheckpoint = checkpoints[pivotIndex]; (checkpoints[pivotIndex], checkpoints[right]) = (checkpoints[right], checkpoints[pivotIndex]); uint storeIndex = left; for (uint i = left; i < right; i++) { if (_isLessThan(checkpoints[i], pivotCheckpoint)) { (checkpoints[storeIndex], checkpoints[i]) = (checkpoints[i], checkpoints[storeIndex]); storeIndex++; } } (checkpoints[storeIndex], checkpoints[right]) = (checkpoints[right], checkpoints[storeIndex]); if (storeIndex < k) { left = storeIndex.add(1); } else { right = storeIndex; } } return checkpoints[k]; } /** * @dev Determine if checkpoint x is less than checkpoint y. * @param x The first checkpoint for comparison. * @param y The second checkpoint for comparison. * @return True if x is less than y. */ function _isLessThan(Checkpoint memory x, Checkpoint memory y) internal pure returns (bool) { return x.ethReserve.mul(y.tokenReserve) < y.ethReserve.mul(x.tokenReserve); } /** * @dev Check if msg.sender is an externally owned account. * @return True if msg.sender is an externally owned account, false if smart contract. */ function _isHuman() internal view returns (bool) { return msg.sender == tx.origin; } /** * @dev Get the reserve values of a Uniswap exchange for a given token. * @param token The token to query the reserve values for. * @return A checkpoint holding the appropriate reserve values. */ function _newCheckpoint(address token) internal view returns (Checkpoint memory) { address payable exchange = uniswap.getExchange(token); return Checkpoint({ ethReserve: exchange.balance, tokenReserve: IERC20(token).balanceOf(exchange) }); } /** * @dev Get subscriber status of a given account for a given token. * @param account The account to query. * @return True if subscribed. */ function _isSubscriber(Account storage account) internal view returns (bool) { // Strict inequality to return false for users who never subscribed and owe zero. return account.balance > _getOwedAmount(account); } /** * @dev Get amount owed by an account. Accrued amount minus collections. * @param account The account to query. * @return Amount owed. */ function _getOwedAmount(Account storage account) internal view returns (uint) { if (account.collectionTimestamp == 0) return 0; uint timeElapsed = block.timestamp.sub(account.collectionTimestamp); return MONTHLY_SUBSCRIPTION_FEE.mul(timeElapsed).div(ONE_MONTH_IN_SECONDS); } /** * @dev Determine whether a given checkpoint would be rewarded with newly minted oracle tokens. * @param token The address of the token to query checkpoint for. * @param checkpoint The checkpoint to test for reward of oracle tokens. * @return True if given checkpoint satisfies any of the following: * Less than required checkpoints exist to calculate a valid median * Exceeds max time since last checkpoint * Exceeds minimum price change from median AND no pending checkpoints * Exceeds minimum percent change from pending checkpoints median * Exceeds minimum percent change from last checkpoint */ function _willRewardCheckpoint(address token, Checkpoint memory checkpoint) internal view returns (bool) { Medianizer memory medianizer = medianizers[token]; return ( medianizer.prices.length < MAX_CHECKPOINTS || block.timestamp.sub(medianizer.latestTimestamp) >= MAX_TIME_SINCE_LAST_CHECKPOINT || (block.timestamp.sub(medianizer.pendingStartTimestamp) >= PENDING_PERIOD && _percentChange(medianizer.median, checkpoint) >= MIN_PRICE_CHANGE) || _percentChange(medianizer.prices[medianizer.tail], checkpoint) >= MIN_PRICE_CHANGE || _percentChange(medianizer.pending[medianizer.pending.length.sub(1)], checkpoint) >= MIN_PRICE_CHANGE ); } /** * @dev Get the percent change between two checkpoints. * @param x The first checkpoint. * @param y The second checkpoint. * @return The absolute value of the percent change, with 18 decimals of precision (e.g., .01e18 = 1%). */ function _percentChange(Checkpoint memory x, Checkpoint memory y) internal pure returns (uint) { uint a = x.ethReserve.mul(y.tokenReserve); uint b = y.ethReserve.mul(x.tokenReserve); uint diff = a > b ? a.sub(b) : b.sub(a); return diff.mul(10 ** 18).div(a); } }