├── .gitattributes ├── .gitignore ├── .prettierrc ├── .solcover.js ├── .soliumrc.json ├── .travis.yml ├── README.md ├── buidler.config.js ├── contracts ├── IndexedUniswapV2Oracle.sol ├── examples │ └── ExampleKeyIndex.sol ├── interfaces │ └── IIndexedUniswapV2Oracle.sol ├── lib │ ├── Bits.sol │ ├── FixedPoint.sol │ ├── IndexedPriceMapLibrary.sol │ ├── KeyIndex.sol │ ├── PriceLibrary.sol │ ├── UniswapV2Library.sol │ └── UniswapV2OracleLibrary.sol └── mocks │ ├── BaseERC20.sol │ ├── LiquidityAdder.sol │ ├── MockERC20.sol │ ├── UniswapV2PriceOracle.sol │ └── tests │ ├── TestErrorTriggers.sol │ └── TestPriceLibrary.sol ├── deploy ├── oracle.deploy.js └── uniswap.deploy.js ├── deployments ├── mainnet │ ├── .chainId │ ├── IndexedOracle.json │ ├── IndexedUniswapV2Oracle.json │ └── solcInputs │ │ └── 0x4201bd2c56bf7e5fc81e7bd5109244e38ed9712df3e34c909a8ae3f61fea064b.json └── rinkeby │ ├── .chainId │ ├── IndexedOracle.json │ ├── IndexedUniswapV2Oracle.json │ └── solcInputs │ ├── 0x352c5d9ff043f69ea087dea0c3a50477b7bb27dd18de162ad9598a7dcac9a4e0.json │ └── 0x4201bd2c56bf7e5fc81e7bd5109244e38ed9712df3e34c909a8ae3f61fea064b.json ├── lib ├── bn.js ├── deployer.js └── logger.js ├── package-lock.json ├── package.json └── test ├── ExampleKeyIndex.spec.js ├── IndexedUniswapV2Oracle.spec.js ├── PriceLibrary.spec.js ├── TestErrorTriggers.spec.js ├── compare-oracles.js ├── test-data └── test-tokens.json ├── tokens-fixture.js └── utils.js /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | artifacts/ 4 | cache/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "singleQuote": false, 6 | "bracketSpacing": false, 7 | "explicitTypes": "always" 8 | } -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mocha: { 3 | enableTimeouts: false, 4 | timeout: 250000 5 | }, 6 | skipFiles: [ 7 | 'mocks/', 8 | 'examples/', 9 | 'interfaces/', 10 | 'lib/FixedPoint.sol' 11 | ] 12 | } -------------------------------------------------------------------------------- /.soliumrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solium:all", 3 | "plugins": ["security"], 4 | "rules": { 5 | "arg-overflow": "off", 6 | "quotes": ["error", "double"], 7 | "indentation": ["error", 2], 8 | "lbrace": "off", 9 | "linebreak-style": ["error", "unix"], 10 | "no-experimental": "off", 11 | "security/no-inline-assembly": "off", 12 | "security/no-block-members": "off", 13 | "security/no-assign-params": "off", 14 | "function-order": "off", 15 | "camelcase": "off" 16 | } 17 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | language: node_js 3 | node_js: 4 | - '10' 5 | install: 6 | - npm install 7 | script: 8 | - npm run coverage 9 | - cat coverage/lcov.info | coveralls -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @indexed-finance/uniswap-v2-oracle 2 | 3 | [![Build Status](https://api.travis-ci.com/indexed-finance/uniswap-v2-oracle.svg?branch=master)](https://travis-ci.com/github/indexed-finance/uniswap-v2-oracle) 4 | [![Coverage Status](https://coveralls.io/repos/github/indexed-finance/uniswap-v2-oracle/badge.svg?branch=master)](https://coveralls.io/github/indexed-finance/uniswap-v2-oracle?branch=master) 5 | [![npm version](https://badge.fury.io/js/%40indexed-finance%2Funiswap-v2-oracle.svg)](https://badge.fury.io/js/%40indexed-finance%2Funiswap-v2-oracle) 6 | 7 | ### [Documentation (incomplete)](https://docs.indexed.finance/smart-contracts/indexeduniswapv2oracle) 8 | 9 | ## Tests 10 | 11 | **Run all tests** 12 | 13 | `npx buidler test` 14 | 15 | **Run test coverage** 16 | 17 | `npm run coverage` 18 | -------------------------------------------------------------------------------- /buidler.config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const url = require('url'); 3 | const Table = require('cli-table3'); 4 | const Logger = require('./lib/logger'); 5 | const Deployer = require('./lib/deployer'); 6 | const { toBN, toHex, oneToken } = require('./lib/bn'); 7 | 8 | require('dotenv').config(); 9 | 10 | const { InfuraProvider } = require('@ethersproject/providers'); 11 | const { fromPrivateKey } = require('ethereumjs-wallet'); 12 | const { randomBytes } = require('crypto'); 13 | 14 | usePlugin("buidler-ethers-v5"); 15 | usePlugin("buidler-deploy"); 16 | usePlugin("solidity-coverage"); 17 | usePlugin("@nomiclabs/buidler-etherscan"); 18 | 19 | const keys = { 20 | mainnet: fromPrivateKey( 21 | process.env.MAINNET_PVT_KEY 22 | ? Buffer.from(process.env.MAINNET_PVT_KEY.slice(2), 'hex') 23 | : randomBytes(32) 24 | ).getPrivateKeyString(), 25 | rinkeby: fromPrivateKey( 26 | process.env.RINKEBY_PVT_KEY 27 | ? Buffer.from(process.env.RINKEBY_PVT_KEY.slice(2), 'hex') 28 | : randomBytes(32)).getPrivateKeyString() 29 | }; 30 | 31 | internalTask('deploy-test-token-and-market', 'Deploy a test token and Uniswap market pair for it and WETH') 32 | .setAction(async ({ logger, name, symbol }) => { 33 | const bre = require('@nomiclabs/buidler'); 34 | const { deployments } = bre; 35 | const chainID = await getChainId(); 36 | if (!logger) logger = Logger(chainID, 'deploy-test-token-and-market'); 37 | if (chainID != 31337 && chainID != 4) { 38 | throw new Error(`Must be on testnet to deploy test tokens.`); 39 | } 40 | const [signer] = await ethers.getSigners(); 41 | const { deployer } = await getNamedAccounts(); 42 | const deploy = await Deployer(bre, logger); 43 | let erc20; 44 | if (await deployments.getOrNull(symbol.toLowerCase())) { 45 | erc20 = await ethers.getContractAt( 46 | 'MockERC20', 47 | (await deployments.getOrNull(symbol.toLowerCase())).address, 48 | signer 49 | ); 50 | logger.info(`Found existing deployment for ${symbol}`); 51 | } else { 52 | erc20 = await deploy('MockERC20', symbol.toLowerCase(), { 53 | from: deployer, 54 | gas: 4000000, 55 | args: [name, symbol] 56 | }, true); 57 | logger.info(`Deployed MockERC20 for ${symbol}`); 58 | } 59 | logger.info(`Creating pair for ${symbol}:WETH`); 60 | const weth = await ethers.getContract('weth'); 61 | let factory; 62 | if (chainID == 4) { 63 | factory = await ethers.getContractAt('UniswapV2Factory', '0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f', signer); 64 | } else { 65 | factory = await ethers.getContract('UniswapV2Factory', signer); 66 | } 67 | if ( 68 | (await factory.getPair(erc20.address, weth.address)) == '0x0000000000000000000000000000000000000000' && 69 | (await factory.getPair(weth.address, erc20.address)) == '0x0000000000000000000000000000000000000000' 70 | ) { 71 | await factory.createPair(erc20.address, weth.address).then(tx => tx.wait()); 72 | logger.info(`Created pair for ${symbol}:WETH`); 73 | } else { 74 | logger.error(`Pair for ${symbol}:WETH already exists`); 75 | } 76 | return erc20; 77 | }); 78 | 79 | internalTask('add-liquidity', 'Add liquidity to a test token market') 80 | .setAction(async ({ logger, symbol, amountToken, amountWeth }) => { 81 | const bre = require('@nomiclabs/buidler'); 82 | const { deployments } = bre; 83 | const chainID = await getChainId(); 84 | if (!logger) { 85 | logger = Logger(chainID, 'add-liquidity'); 86 | } 87 | const deploy = await Deployer(bre, logger); 88 | const { deployer } = await getNamedAccounts(); 89 | if (chainID != 31337 && chainID != 4) { 90 | throw new Error(`Must be on testnet to add liquidity to test tokens.`); 91 | } 92 | const [signer] = await ethers.getSigners(); 93 | const weth = await ethers.getContract('weth'); 94 | let factory, router; 95 | if (chainID == 4) { 96 | factory = '0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f'; 97 | router = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D'; 98 | } else { 99 | factory = (await deployments.getOrNull('UniswapV2Factory')).address; 100 | router = (await deployments.getOrNull('UniswapV2Router02')).address; 101 | } 102 | const liquidityAdder = await deploy('LiquidityAdder', 'liquidityAdder', { 103 | from: deployer, 104 | gas: 1000000, 105 | args: [ 106 | weth.address, 107 | factory, 108 | router 109 | ] 110 | }, true); 111 | const erc20 = await ethers.getContractAt( 112 | 'MockERC20', 113 | (await deployments.getOrNull(symbol.toLowerCase())).address, 114 | signer 115 | ); 116 | logger.success(`Adding liquidity to ${symbol}:ETH market`); 117 | await liquidityAdder.addLiquiditySingle( 118 | erc20.address, 119 | amountToken, 120 | amountWeth, 121 | { gasLimit: 4700000 } 122 | ).then(r => r.wait()); 123 | logger.success(`Added liquidity to ${symbol}:ETH market`); 124 | }); 125 | 126 | task('add-test-liquidity', 'Add liquidity to test token markets') 127 | .addParam('file', 'Path to JSON file with the array of tokens') 128 | .addParam('updatePrices', 'Whether to update the prices of the tokens on the Uniswap oracles', false, types.boolean) 129 | .setAction(async ({ file, updatePrices }) => { 130 | const [signer] = await ethers.getSigners(); 131 | const chainID = await getChainId(); 132 | if (chainID != 31337 && chainID != 4) { 133 | throw new Error(`Must be on testnet to add liquidity to test tokens.`); 134 | } 135 | const logger = Logger(10, 'add-test-liquidity'); 136 | if (!fs.existsSync(file)) { 137 | throw new Error(`Invalid path given for file: ${file}`); 138 | } 139 | const tokens = require(file); 140 | const addresses = []; 141 | for (let token of tokens) { 142 | const { marketcap, name, symbol, price } = token; 143 | if (!marketcap || !name || !symbol || !price) { 144 | throw new Error(`Token JSON must include: marketcap, name, symbol, price`); 145 | } 146 | const erc20 = await ethers.getContract( 147 | 'MockERC20', 148 | (await deployments.getOrNull(symbol.toLowerCase())).address, 149 | signer 150 | ); 151 | addresses.push(erc20.address); 152 | const totalSupply = await erc20.totalSupply(); 153 | let amountWeth = toBN(marketcap); 154 | if (totalSupply.eq(0)) { 155 | amountWeth = amountWeth.divn(10); 156 | } 157 | let amountToken = amountWeth.divn(price); 158 | await run('add-liquidity', { 159 | logger, 160 | symbol, 161 | amountToken: toHex(amountToken.mul(oneToken)), 162 | amountWeth: toHex(amountWeth.mul(oneToken)) 163 | }); 164 | } 165 | if (updatePrices) { 166 | await run('update-prices', { logger, tokens: addresses }); 167 | } 168 | }); 169 | 170 | internalTask('update-prices', 'Update the prices for a list of tokens') 171 | .setAction(async ({ logger, tokens }) => { 172 | const chainID = await getChainId(); 173 | if (!logger) { 174 | logger = Logger(chainID, 'update-prices'); 175 | } 176 | const [signer] = await ethers.getSigners(); 177 | logger.info('Updating prices on weekly TWAP oracle...'); 178 | const shortOracle = await ethers.getContract('HourlyTWAPUniswapV2Oracle', signer); 179 | const receiptHourly = await shortOracle.updatePrices(tokens, { gasLimit: 2000000 }).then(r => r.wait()); 180 | logger.info('Updated prices on weekly TWAP oracle!'); 181 | logger.info('Updating prices on hourly TWAP oracle...'); 182 | const oracle = await ethers.getContract('WeeklyTWAPUniswapV2Oracle', signer); 183 | const receiptWeekly = await oracle.updatePrices(tokens, { gasLimit: 2000000 }).then(r => r.wait()); 184 | logger.success('Updated prices on hourly TWAP oracle!'); 185 | logger.info('Updating prices on indexed oracle...'); 186 | const indexedOracle = await ethers.getContract('IndexedOracle', signer); 187 | const receiptIndexed = await indexedOracle.updatePrices(tokens, { gasLimit: 2000000 }).then(r => r.wait()); 188 | logger.success('Updated prices on indexed oracle!'); 189 | 190 | const priceTable = new Table({head: ['Contract', 'Cost']}); 191 | priceTable.push(['HourlyTWAP', receiptHourly.cumulativeGasUsed.toString()]); 192 | priceTable.push(['WeeklyTWAP', receiptWeekly.cumulativeGasUsed.toString()]); 193 | priceTable.push(['Indexed', receiptIndexed.cumulativeGasUsed.toString()]); 194 | }); 195 | 196 | internalTask('getTimestamp', () => { 197 | return ethers.provider.getBlock('latest').then(b => b.timestamp); 198 | }); 199 | 200 | internalTask('increaseTime', 'Increases the node timestamp') 201 | .setAction(async ({ days, hours, seconds }) => { 202 | const amount = days ? days * 86400 : hours ? hours * 3600 : seconds; 203 | await bre.ethers.provider.send('evm_increaseTime', [amount]); 204 | await bre.ethers.provider.send('evm_mine', []); 205 | }); 206 | 207 | module.exports = { 208 | etherscan: { 209 | apiKey: process.env.ETHERSCAN_API_KEY, 210 | }, 211 | external: { 212 | artifacts: [ 213 | "node_modules/@uniswap/v2-core/build", 214 | "node_modules/@uniswap/v2-periphery/build" 215 | ], 216 | deployments: { 217 | mainnet: [ 218 | "node_modules/@indexed-finance/uniswap-deployments/mainnet" 219 | ], 220 | rinkeby: [ 221 | "node_modules/@indexed-finance/uniswap-deployments/rinkeby" 222 | ] 223 | } 224 | }, 225 | namedAccounts: { 226 | deployer: { 227 | default: 0 228 | }, 229 | }, 230 | networks: { 231 | mainnet: { 232 | url: new InfuraProvider("mainnet", process.env.INFURA_PROJECT_ID).connection.url, 233 | accounts: [keys.mainnet], 234 | chainId: 1 235 | }, 236 | rinkeby: { 237 | url: new InfuraProvider("rinkeby", process.env.INFURA_PROJECT_ID).connection.url, 238 | accounts: [keys.rinkeby], 239 | chainId: 4 240 | }, 241 | coverage: { 242 | url: url.format({ 243 | protocol: "http:", 244 | port: 8555, 245 | hostname: "localhost", 246 | }), 247 | } 248 | }, 249 | solc: { 250 | version: "0.6.8", 251 | optimizer: { 252 | enabled: true, 253 | runs: 200 254 | } 255 | }, 256 | }; 257 | -------------------------------------------------------------------------------- /contracts/IndexedUniswapV2Oracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.6.0; 3 | pragma experimental ABIEncoderV2; 4 | 5 | /* ========== Internal Libraries ========== */ 6 | import "./lib/PriceLibrary.sol"; 7 | import "./lib/FixedPoint.sol"; 8 | import "./lib/IndexedPriceMapLibrary.sol"; 9 | 10 | /* ========== Internal Inheritance ========== */ 11 | import "./interfaces/IIndexedUniswapV2Oracle.sol"; 12 | 13 | 14 | contract IndexedUniswapV2Oracle is IIndexedUniswapV2Oracle { 15 | using PriceLibrary for address; 16 | using PriceLibrary for PriceLibrary.PriceObservation; 17 | using PriceLibrary for PriceLibrary.TwoWayAveragePrice; 18 | using FixedPoint for FixedPoint.uq112x112; 19 | using FixedPoint for FixedPoint.uq144x112; 20 | using IndexedPriceMapLibrary for IndexedPriceMapLibrary.IndexedPriceMap; 21 | 22 | 23 | /* ========== Immutables ========== */ 24 | 25 | address internal immutable _uniswapFactory; 26 | address internal immutable _weth; 27 | 28 | /* ========== Storage ========== */ 29 | 30 | // Price observations for tokens indexed by hour. 31 | mapping(address => IndexedPriceMapLibrary.IndexedPriceMap) internal _tokenPriceMaps; 32 | 33 | /* ========== Modifiers ========== */ 34 | 35 | modifier validMinMax(uint256 minTimeElapsed, uint256 maxTimeElapsed) { 36 | require( 37 | maxTimeElapsed >= minTimeElapsed, 38 | "IndexedUniswapV2Oracle::validMinMax: Minimum age can not be higher than maximum." 39 | ); 40 | _; 41 | } 42 | 43 | /* ========== Constructor ========== */ 44 | 45 | constructor(address uniswapFactory, address weth) public { 46 | _uniswapFactory = uniswapFactory; 47 | _weth = weth; 48 | } 49 | 50 | /* ========== Mutative Functions ========== */ 51 | 52 | /** 53 | * @dev Attempts to update the price of `token` and returns a boolean 54 | * indicating whether it was updated. 55 | * 56 | * Note: The price can be updated if there is no observation for the current hour 57 | * and at least 30 minutes have passed since the last observation. 58 | */ 59 | function updatePrice(address token) public override returns (bool/* didUpdatePrice */) { 60 | if (token == _weth) return true; 61 | PriceLibrary.PriceObservation memory observation = _uniswapFactory.observeTwoWayPrice(token, _weth); 62 | return _tokenPriceMaps[token].writePriceObservation(observation); 63 | } 64 | 65 | /** 66 | * @dev Attempts to update the price of each token in `tokens` and returns a boolean 67 | * array indicating which tokens had their prices updated. 68 | * 69 | * Note: The price can be updated if there is no observation for the current hour 70 | * and at least 30 minutes have passed since the last observation. 71 | */ 72 | function updatePrices(address[] calldata tokens) 73 | external 74 | override 75 | returns (bool[] memory pricesUpdated) 76 | { 77 | uint256 len = tokens.length; 78 | pricesUpdated = new bool[](len); 79 | for (uint256 i = 0; i < len; i++) { 80 | pricesUpdated[i] = updatePrice(tokens[i]); 81 | } 82 | } 83 | 84 | /* ========== Meta Price Queries ========== */ 85 | 86 | /** 87 | * @dev Returns a boolean indicating whether a price was recorded for `token` at `priceKey`. 88 | * 89 | * @param token Token to check if the oracle has a price for 90 | * @param priceKey Index of the hour to check 91 | */ 92 | function hasPriceObservationInWindow(address token, uint256 priceKey) 93 | external view override returns (bool) 94 | { 95 | return _tokenPriceMaps[token].hasPriceInWindow(priceKey); 96 | } 97 | 98 | 99 | /** 100 | * @dev Returns the price observation for `token` recorded in `priceKey`. 101 | * Reverts if no prices have been recorded for that key. 102 | * 103 | * @param token Token to retrieve a price for 104 | * @param priceKey Index of the hour to query 105 | */ 106 | function getPriceObservationInWindow(address token, uint256 priceKey) 107 | external 108 | view 109 | override 110 | returns (PriceLibrary.PriceObservation memory observation) 111 | { 112 | observation = _tokenPriceMaps[token].getPriceInWindow(priceKey); 113 | require( 114 | observation.timestamp != 0, 115 | "IndexedUniswapV2Oracle::getPriceObservationInWindow: No price observed in given hour." 116 | ); 117 | } 118 | 119 | /** 120 | * @dev Returns all price observations for `token` recorded between `timeFrom` and `timeTo`. 121 | */ 122 | function getPriceObservationsInRange(address token, uint256 timeFrom, uint256 timeTo) 123 | external 124 | view 125 | override 126 | returns (PriceLibrary.PriceObservation[] memory prices) 127 | { 128 | prices = _tokenPriceMaps[token].getPriceObservationsInRange(timeFrom, timeTo); 129 | } 130 | 131 | /* ========== Price Update Queries ========== */ 132 | 133 | /** 134 | * @dev Returns a boolean indicating whether the price of `token` can be updated. 135 | * 136 | * Note: The price can be updated if there is no observation for the current hour 137 | * and at least 30 minutes have passed since the last observation. 138 | */ 139 | function canUpdatePrice(address token) external view override returns (bool/* canUpdatePrice */) { 140 | if (!_uniswapFactory.pairInitialized(token, _weth)) return false; 141 | return _tokenPriceMaps[token].canUpdatePrice(uint32(now)); 142 | } 143 | 144 | /** 145 | * @dev Returns a boolean array indicating whether the price of each token in 146 | * `tokens` can be updated. 147 | * 148 | * Note: The price can be updated if there is no observation for the current hour 149 | * and at least 30 minutes have passed since the last observation. 150 | */ 151 | function canUpdatePrices(address[] calldata tokens) external view override returns (bool[] memory canUpdateArr) { 152 | uint256 len = tokens.length; 153 | canUpdateArr = new bool[](len); 154 | for (uint256 i = 0; i < len; i++) { 155 | address token = tokens[i]; 156 | bool timeAllowed = _tokenPriceMaps[token].canUpdatePrice(uint32(now)); 157 | canUpdateArr[i] = timeAllowed && _uniswapFactory.pairInitialized(token, _weth); 158 | } 159 | } 160 | 161 | /* ========== Price Queries: Singular ========== */ 162 | 163 | /** 164 | * @dev Returns the TwoWayAveragePrice struct representing the average price of 165 | * weth in terms of `token` and the average price of `token` in terms of weth. 166 | * 167 | * Computes the time-weighted average price of weth in terms of `token` and the price 168 | * of `token` in terms of weth by getting the current prices from Uniswap and searching 169 | * for a historical price which is between `minTimeElapsed` and `maxTimeElapsed` seconds old. 170 | * 171 | * Note: `maxTimeElapsed` is only accurate to the nearest hour (rounded down) unless 172 | * it is less than one hour. 173 | * Note: `minTimeElapsed` is only accurate to the nearest hour (rounded up) unless 174 | * it is less than one hour. 175 | */ 176 | function computeTwoWayAveragePrice( 177 | address token, 178 | uint256 minTimeElapsed, 179 | uint256 maxTimeElapsed 180 | ) 181 | external 182 | view 183 | override 184 | validMinMax(minTimeElapsed, maxTimeElapsed) 185 | returns (PriceLibrary.TwoWayAveragePrice memory) 186 | { 187 | return _getTwoWayPrice(token, minTimeElapsed, maxTimeElapsed); 188 | } 189 | 190 | /** 191 | * @dev Returns the UQ112x112 struct representing the average price of 192 | * `token` in terms of weth. 193 | * 194 | * Computes the time-weighted average price of `token` in terms of weth by getting the 195 | * current price from Uniswap and searching for a historical price which is between 196 | * `minTimeElapsed` and `maxTimeElapsed` seconds old. 197 | * 198 | * Note: `maxTimeElapsed` is only accurate to the nearest hour (rounded down) unless 199 | * it is less than one hour. 200 | * Note: `minTimeElapsed` is only accurate to the nearest hour (rounded up) unless 201 | * it is less than one hour. 202 | */ 203 | function computeAverageTokenPrice( 204 | address token, 205 | uint256 minTimeElapsed, 206 | uint256 maxTimeElapsed 207 | ) 208 | external 209 | view 210 | override 211 | validMinMax(minTimeElapsed, maxTimeElapsed) 212 | returns (FixedPoint.uq112x112 memory priceAverage) 213 | { 214 | return _getTokenPrice(token, minTimeElapsed, maxTimeElapsed); 215 | } 216 | 217 | /** 218 | * @dev Returns the UQ112x112 struct representing the average price of 219 | * weth in terms of `token`. 220 | * 221 | * Computes the time-weighted average price of weth in terms of `token` by getting the 222 | * current price from Uniswap and searching for a historical price which is between 223 | * `minTimeElapsed` and `maxTimeElapsed` seconds old. 224 | * 225 | * Note: `maxTimeElapsed` is only accurate to the nearest hour (rounded down) unless 226 | * it is less than one hour. 227 | * Note: `minTimeElapsed` is only accurate to the nearest hour (rounded up) unless 228 | * it is less than one hour. 229 | */ 230 | function computeAverageEthPrice( 231 | address token, 232 | uint256 minTimeElapsed, 233 | uint256 maxTimeElapsed 234 | ) 235 | external 236 | view 237 | override 238 | validMinMax(minTimeElapsed, maxTimeElapsed) 239 | returns (FixedPoint.uq112x112 memory priceAverage) 240 | { 241 | return _getEthPrice(token, minTimeElapsed, maxTimeElapsed); 242 | } 243 | 244 | /* ========== Price Queries: Multiple ========== */ 245 | 246 | /** 247 | * @dev Returns the TwoWayAveragePrice structs representing the average price of 248 | * weth in terms of each token in `tokens` and the average price of each token 249 | * in terms of weth. 250 | * 251 | * Computes the time-weighted average price of weth in terms of each token and the price 252 | * of each token in terms of weth by getting the current prices from Uniswap and searching 253 | * for a historical price which is between `minTimeElapsed` and `maxTimeElapsed` seconds old. 254 | * 255 | * Note: `maxTimeElapsed` is only accurate to the nearest hour (rounded down) unless 256 | * it is less than one hour. 257 | * Note: `minTimeElapsed` is only accurate to the nearest hour (rounded up) unless 258 | * it is less than one hour. 259 | */ 260 | function computeTwoWayAveragePrices( 261 | address[] calldata tokens, 262 | uint256 minTimeElapsed, 263 | uint256 maxTimeElapsed 264 | ) 265 | external 266 | view 267 | override 268 | validMinMax(minTimeElapsed, maxTimeElapsed) 269 | returns (PriceLibrary.TwoWayAveragePrice[] memory prices) 270 | { 271 | uint256 len = tokens.length; 272 | prices = new PriceLibrary.TwoWayAveragePrice[](len); 273 | for (uint256 i = 0; i < len; i++) { 274 | prices[i] = _getTwoWayPrice(tokens[i], minTimeElapsed, maxTimeElapsed); 275 | } 276 | } 277 | 278 | /** 279 | * @dev Returns the UQ112x112 structs representing the average price of 280 | * each token in `tokens` in terms of weth. 281 | * 282 | * Computes the time-weighted average price of each token in terms of weth by getting 283 | * the current price from Uniswap and searching for a historical price which is between 284 | * `minTimeElapsed` and `maxTimeElapsed` seconds old. 285 | * 286 | * Note: `maxTimeElapsed` is only accurate to the nearest hour (rounded down) unless 287 | * it is less than one hour. 288 | * Note: `minTimeElapsed` is only accurate to the nearest hour (rounded up) unless 289 | * it is less than one hour. 290 | */ 291 | function computeAverageTokenPrices( 292 | address[] calldata tokens, 293 | uint256 minTimeElapsed, 294 | uint256 maxTimeElapsed 295 | ) 296 | external 297 | view 298 | override 299 | validMinMax(minTimeElapsed, maxTimeElapsed) 300 | returns (FixedPoint.uq112x112[] memory averagePrices) 301 | { 302 | uint256 len = tokens.length; 303 | averagePrices = new FixedPoint.uq112x112[](len); 304 | for (uint256 i = 0; i < len; i++) { 305 | averagePrices[i] = _getTokenPrice(tokens[i], minTimeElapsed, maxTimeElapsed); 306 | } 307 | } 308 | 309 | /** 310 | * @dev Returns the UQ112x112 structs representing the average price of 311 | * weth in terms of each token in `tokens`. 312 | * 313 | * Computes the time-weighted average price of weth in terms of each token by getting 314 | * the current price from Uniswap and searching for a historical price which is between 315 | * `minTimeElapsed` and `maxTimeElapsed` seconds old. 316 | * 317 | * Note: `maxTimeElapsed` is only accurate to the nearest hour (rounded down) unless 318 | * it is less than one hour. 319 | * Note: `minTimeElapsed` is only accurate to the nearest hour (rounded up) unless 320 | * it is less than one hour. 321 | */ 322 | function computeAverageEthPrices( 323 | address[] calldata tokens, 324 | uint256 minTimeElapsed, 325 | uint256 maxTimeElapsed 326 | ) 327 | external 328 | view 329 | override 330 | validMinMax(minTimeElapsed, maxTimeElapsed) 331 | returns (FixedPoint.uq112x112[] memory averagePrices) 332 | { 333 | uint256 len = tokens.length; 334 | averagePrices = new FixedPoint.uq112x112[](len); 335 | for (uint256 i = 0; i < len; i++) { 336 | averagePrices[i] = _getEthPrice(tokens[i], minTimeElapsed, maxTimeElapsed); 337 | } 338 | } 339 | 340 | /* ========== Value Queries: Singular ========== */ 341 | 342 | /** 343 | * @dev Compute the average value of `tokenAmount` ether in terms of weth. 344 | * 345 | * Computes the time-weighted average price of `token` in terms of weth by getting 346 | * the current price from Uniswap and searching for a historical price which is between 347 | * `minTimeElapsed` and `maxTimeElapsed` seconds old, then multiplies by `wethAmount`. 348 | * 349 | * Note: `maxTimeElapsed` is only accurate to the nearest hour (rounded down) unless 350 | * it is less than one hour. 351 | * Note: `minTimeElapsed` is only accurate to the nearest hour (rounded up) unless 352 | * it is less than one hour. 353 | */ 354 | function computeAverageEthForTokens( 355 | address token, 356 | uint256 tokenAmount, 357 | uint256 minTimeElapsed, 358 | uint256 maxTimeElapsed 359 | ) 360 | external 361 | view 362 | override 363 | validMinMax(minTimeElapsed, maxTimeElapsed) 364 | returns (uint144 /* averageValueInWETH */) 365 | { 366 | FixedPoint.uq112x112 memory tokenPrice = _getTokenPrice(token, minTimeElapsed, maxTimeElapsed); 367 | return tokenPrice.mul(tokenAmount).decode144(); 368 | } 369 | 370 | /** 371 | * @dev Compute the average value of `wethAmount` ether in terms of `token`. 372 | * 373 | * Computes the time-weighted average price of weth in terms of the token by getting 374 | * the current price from Uniswap and searching for a historical price which is between 375 | * `minTimeElapsed` and `maxTimeElapsed` seconds old, then multiplies by `wethAmount`. 376 | * 377 | * Note: `maxTimeElapsed` is only accurate to the nearest hour (rounded down) unless 378 | * it is less than one hour. 379 | * Note: `minTimeElapsed` is only accurate to the nearest hour (rounded up) unless 380 | * it is less than one hour. 381 | */ 382 | function computeAverageTokensForEth( 383 | address token, 384 | uint256 wethAmount, 385 | uint256 minTimeElapsed, 386 | uint256 maxTimeElapsed 387 | ) 388 | external 389 | view 390 | override 391 | validMinMax(minTimeElapsed, maxTimeElapsed) 392 | returns (uint144 /* averageValueInToken */) 393 | { 394 | FixedPoint.uq112x112 memory ethPrice = _getEthPrice(token, minTimeElapsed, maxTimeElapsed); 395 | return ethPrice.mul(wethAmount).decode144(); 396 | } 397 | 398 | /* ========== Value Queries: Multiple ========== */ 399 | 400 | /** 401 | * @dev Compute the average value of each amount of tokens in `tokenAmounts` in terms 402 | * of the corresponding token in `tokens`. 403 | * 404 | * Computes the time-weighted average price of each token in terms of weth by getting 405 | * the current price from Uniswap and searching for a historical price which is between 406 | * `minTimeElapsed` and `maxTimeElapsed` seconds old, then multiplies by the corresponding 407 | * amount in `tokenAmounts`. 408 | * 409 | * Note: `maxTimeElapsed` is only accurate to the nearest hour (rounded down) unless 410 | * it is less than one hour. 411 | * Note: `minTimeElapsed` is only accurate to the nearest hour (rounded up) unless 412 | * it is less than one hour. 413 | */ 414 | function computeAverageEthForTokens( 415 | address[] calldata tokens, 416 | uint256[] calldata tokenAmounts, 417 | uint256 minTimeElapsed, 418 | uint256 maxTimeElapsed 419 | ) 420 | external 421 | view 422 | override 423 | validMinMax(minTimeElapsed, maxTimeElapsed) 424 | returns (uint144[] memory averageValuesInWETH) 425 | { 426 | uint256 len = tokens.length; 427 | require( 428 | tokenAmounts.length == len, 429 | "IndexedUniswapV2Oracle::computeAverageEthForTokens: Tokens and amounts have different lengths." 430 | ); 431 | averageValuesInWETH = new uint144[](len); 432 | for (uint256 i = 0; i < len; i++) { 433 | averageValuesInWETH[i] = _getTokenPrice( 434 | tokens[i], 435 | minTimeElapsed, 436 | maxTimeElapsed 437 | ).mul(tokenAmounts[i]).decode144(); 438 | } 439 | } 440 | 441 | /** 442 | * @dev Compute the average value of each amount of ether in `wethAmounts` in terms 443 | * of the corresponding token in `tokens`. 444 | * 445 | * Computes the time-weighted average price of weth in terms of each token by getting 446 | * the current price from Uniswap and searching for a historical price which is between 447 | * `minTimeElapsed` and `maxTimeElapsed` seconds old, then multiplies by the corresponding 448 | * amount in `wethAmounts`. 449 | * 450 | * Note: `maxTimeElapsed` is only accurate to the nearest hour (rounded down) unless 451 | * it is less than one hour. 452 | * Note: `minTimeElapsed` is only accurate to the nearest hour (rounded up) unless 453 | * it is less than one hour. 454 | */ 455 | function computeAverageTokensForEth( 456 | address[] calldata tokens, 457 | uint256[] calldata wethAmounts, 458 | uint256 minTimeElapsed, 459 | uint256 maxTimeElapsed 460 | ) 461 | external 462 | view 463 | override 464 | validMinMax(minTimeElapsed, maxTimeElapsed) 465 | returns (uint144[] memory averageValuesInWETH) 466 | { 467 | uint256 len = tokens.length; 468 | require( 469 | wethAmounts.length == len, 470 | "IndexedUniswapV2Oracle::computeAverageTokensForEth: Tokens and amounts have different lengths." 471 | ); 472 | averageValuesInWETH = new uint144[](len); 473 | for (uint256 i = 0; i < len; i++) { 474 | averageValuesInWETH[i] = _getEthPrice( 475 | tokens[i], 476 | minTimeElapsed, 477 | maxTimeElapsed 478 | ).mul(wethAmounts[i]).decode144(); 479 | } 480 | } 481 | 482 | /* ========== Internal Functions ========== */ 483 | function _getTwoWayPrice( 484 | address token, 485 | uint256 minTimeElapsed, 486 | uint256 maxTimeElapsed 487 | ) 488 | internal 489 | view 490 | returns (PriceLibrary.TwoWayAveragePrice memory) 491 | { 492 | if (token == _weth) { 493 | return PriceLibrary.TwoWayAveragePrice( 494 | FixedPoint.encode(1)._x, 495 | FixedPoint.encode(1)._x 496 | ); 497 | } 498 | // Get the current cumulative price 499 | PriceLibrary.PriceObservation memory current = _uniswapFactory.observeTwoWayPrice(token, _weth); 500 | // Get the latest usable price 501 | (bool foundPrice, uint256 lastPriceKey) = _tokenPriceMaps[token].getLastPriceObservation( 502 | current.timestamp, 503 | minTimeElapsed, 504 | maxTimeElapsed 505 | ); 506 | require(foundPrice, "IndexedUniswapV2Oracle::_getTwoWayPrice: No price found in provided range."); 507 | PriceLibrary.PriceObservation memory previous = _tokenPriceMaps[token].priceMap[lastPriceKey]; 508 | return previous.computeTwoWayAveragePrice(current); 509 | } 510 | 511 | function _getTokenPrice( 512 | address token, 513 | uint256 minTimeElapsed, 514 | uint256 maxTimeElapsed 515 | ) 516 | internal 517 | view 518 | returns (FixedPoint.uq112x112 memory) 519 | { 520 | if (token == _weth) { 521 | return FixedPoint.fraction(1, 1); 522 | } 523 | (uint32 timestamp, uint224 priceCumulativeEnd) = _uniswapFactory.observePrice(token, _weth); 524 | (bool foundPrice, uint256 lastPriceKey) = _tokenPriceMaps[token].getLastPriceObservation( 525 | timestamp, 526 | minTimeElapsed, 527 | maxTimeElapsed 528 | ); 529 | require(foundPrice, "IndexedUniswapV2Oracle::_getTokenPrice: No price found in provided range."); 530 | PriceLibrary.PriceObservation storage previous = _tokenPriceMaps[token].priceMap[lastPriceKey]; 531 | return PriceLibrary.computeAveragePrice( 532 | previous.timestamp, 533 | previous.priceCumulativeLast, 534 | timestamp, 535 | priceCumulativeEnd 536 | ); 537 | } 538 | 539 | function _getEthPrice( 540 | address token, 541 | uint256 minTimeElapsed, 542 | uint256 maxTimeElapsed 543 | ) 544 | internal 545 | view 546 | returns (FixedPoint.uq112x112 memory) 547 | { 548 | if (token == _weth) { 549 | return FixedPoint.fraction(1, 1); 550 | } 551 | (uint32 timestamp, uint224 priceCumulativeEnd) = _uniswapFactory.observePrice(_weth, token); 552 | (bool foundPrice, uint256 lastPriceKey) = _tokenPriceMaps[token].getLastPriceObservation( 553 | timestamp, 554 | minTimeElapsed, 555 | maxTimeElapsed 556 | ); 557 | require(foundPrice, "IndexedUniswapV2Oracle::_getEthPrice: No price found in provided range."); 558 | PriceLibrary.PriceObservation storage previous = _tokenPriceMaps[token].priceMap[lastPriceKey]; 559 | return PriceLibrary.computeAveragePrice( 560 | previous.timestamp, 561 | previous.ethPriceCumulativeLast, 562 | timestamp, 563 | priceCumulativeEnd 564 | ); 565 | } 566 | } -------------------------------------------------------------------------------- /contracts/examples/ExampleKeyIndex.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.6.0; 3 | 4 | /* ========== Internal Libraries ========== */ 5 | import "../lib/KeyIndex.sol"; 6 | 7 | 8 | /** 9 | * @dev Example usage of the KeyIndex library. 10 | */ 11 | contract ExampleKeyIndex { 12 | using KeyIndex for *; 13 | 14 | mapping(uint256 => uint256) internal _keyIndex; 15 | mapping(uint256 => uint256) internal _valueMap; 16 | 17 | function writeValue(uint256 mapKey, uint256 value) external { 18 | _valueMap[mapKey] = value; 19 | _keyIndex.markSetKey(mapKey); 20 | } 21 | 22 | function getPreviousValue(uint256 key, uint256 maxDistance) 23 | external 24 | view 25 | returns (bool /* foundValue */, uint256 /* value */) 26 | { 27 | (bool foundValue, uint256 mapKey) = _keyIndex.findLastSetKey(key, maxDistance); 28 | if (foundValue) { 29 | return (true, _valueMap[mapKey]); 30 | } 31 | return (false, 0); 32 | } 33 | 34 | function getNextValue(uint256 key, uint256 maxDistance) 35 | external 36 | view 37 | returns (bool /* foundValue */, uint256 /* value */) 38 | { 39 | 40 | (bool foundValue, uint256 mapKey) = _keyIndex.findNextSetKey(key, maxDistance); 41 | if (foundValue) { 42 | return (true, _valueMap[mapKey]); 43 | } 44 | return (false, 0); 45 | } 46 | 47 | function getValuesInRange(uint256 fromKey, uint256 toKey) 48 | external view returns (uint256[] memory values) 49 | { 50 | require(toKey > fromKey, "ExampleKeyIndex::getValuesInRange: Invalid Range"); 51 | bytes memory bitPositions = _keyIndex.getEncodedSetKeysInRange(fromKey, toKey); 52 | // Divide by 2 because length is in bytes and relative indices are stored as uint16 53 | uint256 len = bitPositions.length / 2; 54 | values = new uint256[](len); 55 | uint256 ptr; 56 | assembly { ptr := add(bitPositions, 32) } 57 | for (uint256 i = 0; i < len; i++) { 58 | uint256 relativeIndex; 59 | assembly { 60 | relativeIndex := shr(0xf0, mload(ptr)) 61 | ptr := add(ptr, 2) 62 | } 63 | uint256 key = fromKey + relativeIndex; 64 | values[i] = _valueMap[key]; 65 | } 66 | } 67 | 68 | function getSetKeysInRange(uint256 fromKey, uint256 toKey) 69 | external view returns (uint256[] memory setKeys) 70 | { 71 | require(toKey > fromKey, "ExampleKeyIndex::getSetKeysInRange: Invalid Range"); 72 | // Divide by 2 because length is in bytes and relative indices are stored as uint16 73 | bytes memory bitPositions = _keyIndex.getEncodedSetKeysInRange(fromKey, toKey); 74 | uint256 len = bitPositions.length / 2; 75 | setKeys = new uint256[](len); 76 | uint256 ptr; 77 | assembly { ptr := add(bitPositions, 32) } 78 | for (uint256 i = 0; i < len; i++) { 79 | uint256 relativeIndex; 80 | assembly { 81 | relativeIndex := shr(0xf0, mload(ptr)) 82 | ptr := add(ptr, 2) 83 | } 84 | setKeys[i] = fromKey + relativeIndex; 85 | } 86 | } 87 | 88 | function hasKey(uint256 key) 89 | external 90 | view 91 | returns (bool) 92 | { 93 | return _keyIndex.hasKey(key); 94 | } 95 | } -------------------------------------------------------------------------------- /contracts/interfaces/IIndexedUniswapV2Oracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.6.0; 3 | pragma experimental ABIEncoderV2; 4 | 5 | /* ========== Libraries ========== */ 6 | import "../lib/PriceLibrary.sol"; 7 | import "../lib/FixedPoint.sol"; 8 | 9 | 10 | interface IIndexedUniswapV2Oracle { 11 | /* ========== Mutative Functions ========== */ 12 | 13 | function updatePrice(address token) external returns (bool); 14 | 15 | function updatePrices(address[] calldata tokens) external returns (bool[] memory); 16 | 17 | /* ========== Meta Price Queries ========== */ 18 | 19 | function hasPriceObservationInWindow(address token, uint256 priceKey) external view returns (bool); 20 | 21 | function getPriceObservationInWindow( 22 | address token, uint256 priceKey 23 | ) external view returns (PriceLibrary.PriceObservation memory); 24 | 25 | function getPriceObservationsInRange( 26 | address token, uint256 timeFrom, uint256 timeTo 27 | ) external view returns (PriceLibrary.PriceObservation[] memory prices); 28 | 29 | /* ========== Price Update Queries ========== */ 30 | 31 | function canUpdatePrice(address token) external view returns (bool); 32 | 33 | function canUpdatePrices(address[] calldata tokens) external view returns (bool[] memory); 34 | 35 | /* ========== Price Queries: Singular ========== */ 36 | 37 | function computeTwoWayAveragePrice( 38 | address token, uint256 minTimeElapsed, uint256 maxTimeElapsed 39 | ) external view returns (PriceLibrary.TwoWayAveragePrice memory); 40 | 41 | function computeAverageTokenPrice( 42 | address token, uint256 minTimeElapsed, uint256 maxTimeElapsed 43 | ) external view returns (FixedPoint.uq112x112 memory); 44 | 45 | function computeAverageEthPrice( 46 | address token, uint256 minTimeElapsed, uint256 maxTimeElapsed 47 | ) external view returns (FixedPoint.uq112x112 memory); 48 | 49 | /* ========== Price Queries: Multiple ========== */ 50 | 51 | function computeTwoWayAveragePrices( 52 | address[] calldata tokens, 53 | uint256 minTimeElapsed, 54 | uint256 maxTimeElapsed 55 | ) external view returns (PriceLibrary.TwoWayAveragePrice[] memory); 56 | 57 | function computeAverageTokenPrices( 58 | address[] calldata tokens, 59 | uint256 minTimeElapsed, 60 | uint256 maxTimeElapsed 61 | ) external view returns (FixedPoint.uq112x112[] memory); 62 | 63 | function computeAverageEthPrices( 64 | address[] calldata tokens, 65 | uint256 minTimeElapsed, 66 | uint256 maxTimeElapsed 67 | ) external view returns (FixedPoint.uq112x112[] memory); 68 | 69 | /* ========== Value Queries: Singular ========== */ 70 | 71 | function computeAverageEthForTokens( 72 | address token, 73 | uint256 tokenAmount, 74 | uint256 minTimeElapsed, 75 | uint256 maxTimeElapsed 76 | ) external view returns (uint144); 77 | 78 | function computeAverageTokensForEth( 79 | address token, 80 | uint256 wethAmount, 81 | uint256 minTimeElapsed, 82 | uint256 maxTimeElapsed 83 | ) external view returns (uint144); 84 | 85 | /* ========== Value Queries: Multiple ========== */ 86 | 87 | function computeAverageEthForTokens( 88 | address[] calldata tokens, 89 | uint256[] calldata tokenAmounts, 90 | uint256 minTimeElapsed, 91 | uint256 maxTimeElapsed 92 | ) external view returns (uint144[] memory); 93 | 94 | function computeAverageTokensForEth( 95 | address[] calldata tokens, 96 | uint256[] calldata wethAmounts, 97 | uint256 minTimeElapsed, 98 | uint256 maxTimeElapsed 99 | ) external view returns (uint144[] memory); 100 | } -------------------------------------------------------------------------------- /contracts/lib/Bits.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.6.0; 3 | 4 | 5 | library Bits { 6 | uint256 internal constant ONE = uint256(1); 7 | uint256 internal constant ONES = uint256(~0); 8 | 9 | /** 10 | * @dev Sets the bit at the given 'index' in 'self' to '1'. 11 | * Returns the modified value. 12 | */ 13 | function setBit(uint256 self, uint256 index) internal pure returns (uint256) { 14 | return self | (ONE << index); 15 | } 16 | 17 | /** 18 | * @dev Returns a boolean indicating whether the bit at the given `index` in `self` is set. 19 | */ 20 | function bitSet(uint256 self, uint256 index) internal pure returns (bool) { 21 | return (self >> index) & 1 == 1; 22 | } 23 | 24 | /** 25 | * @dev Clears all bits in the exclusive range [index:255] 26 | */ 27 | function clearBitsAfter(uint256 self, uint256 index) internal pure returns (uint256) { 28 | return self & (ONES >> (255 - index)); 29 | } 30 | 31 | /** 32 | * @dev Clears bits in the exclusive range [0:index] 33 | */ 34 | function clearBitsBefore(uint256 self, uint256 index) internal pure returns (uint256) { 35 | return self & (ONES << (index)); 36 | } 37 | 38 | /** 39 | * @dev Writes the index of every set bit in `val` as a uint16 in `bitPositions`. 40 | * Adds `offset` to the stored bit index. 41 | * 42 | * `bitPositions` must have a length equal to twice the maximum number of bits that 43 | * could be found plus 31. Each index is stored as a uint16 to accomodate `offset` 44 | * because this is used in functions which would otherwise need expensive methods 45 | * to handle relative indices in multi-integer searches. 46 | * The specified length ensures that solc will handle memory allocation, and the 47 | * addition of 31 allows us to store whole words at a time. 48 | * After being declared, the actual length stored in memory must be set to 0 with: 49 | * `assembly { mstore(bitPositions, 0) }` because the length is used to count found bits. 50 | * 51 | * @param bitPositions Packed uint16 array for positions of set bits 52 | * @param val Value to search set bits in 53 | * @param offset Value added to the stored position, used to simplify large searches. 54 | */ 55 | function writeSetBits(bytes memory bitPositions, uint256 val, uint16 offset) internal pure { 56 | if (val == 0) return; 57 | 58 | assembly { 59 | // Read the current length, which is the number of stored bytes 60 | let len := mload(bitPositions) 61 | // Set the starting pointer by adding the length to the bytes data pointer 62 | // This does not change and is later used to compute the new length 63 | let startPtr := add(add(bitPositions, 32), len) 64 | // Set the variable pointer which is used to track where to write memory values 65 | let ptr := startPtr 66 | // Increment the number of bits to shift until the shifted integer is 0 67 | // Add 3 to the index each loop because that is the number of bits being checked 68 | // at a time. 69 | for {let i := 0} gt(shr(i, val), 0) {i := add(i, 3)} { 70 | // Loop until the last 8 bits are not all 0 71 | for {} eq(and(shr(i, val), 255), 0) {i := add(i, 8)} {} 72 | // Take only the last 3 bits 73 | let x := and(shr(i, val), 7) 74 | // Use a switch statement as a lookup table with every possible combination of 3 bits. 75 | switch x 76 | case 0 {}// no bits set 77 | case 1 {// bit 0 set 78 | // shift left 240 bits to write uint16, increment ptr by 2 bytes 79 | mstore(ptr, shl(0xf0, add(i, offset))) 80 | ptr := add(ptr, 2) 81 | } 82 | case 2 {// bit 1 set 83 | // shift left 240 bits to write uint16, increment ptr by 2 bytes 84 | mstore(ptr, shl(0xf0, add(add(i, 1), offset))) 85 | ptr := add(ptr, 2) 86 | } 87 | case 3 {// bits 0,1 set 88 | // shift first left 240 bits and second 224 to write two uint16s 89 | // increment ptr by 4 bytes 90 | mstore( 91 | ptr, 92 | or(// use OR to avoid multiple memory writes 93 | shl(0xf0, add(i, offset)), 94 | shl(0xe0, add(add(i, 1), offset)) 95 | ) 96 | ) 97 | ptr := add(ptr, 4) 98 | } 99 | case 4 {// bit 2 set 100 | // shift left 240 bits to write uint16, increment ptr by 2 bytes 101 | mstore(ptr, shl(0xf0, add(add(i, 2), offset))) 102 | ptr := add(ptr, 2) 103 | } 104 | case 5 {// 5: bits 0,2 set 105 | // shift first left 240 bits and second 224 bits to write two uint16s 106 | mstore( 107 | ptr, 108 | or(// use OR to avoid multiple memory writes 109 | shl(0xf0, add(i, offset)), 110 | shl(0xe0, add(add(i, 2), offset)) 111 | ) 112 | ) 113 | 114 | ptr := add(ptr, 4)// increment ptr by 4 bytes 115 | } 116 | case 6 {// bits 1,2 set 117 | // shift first left 240 bits and second 224 to write two uint16s 118 | mstore( 119 | ptr, 120 | or(// use OR to avoid multiple memory writes 121 | shl(0xf0, add(add(i, 1), offset)), 122 | shl(0xe0, add(add(i, 2), offset)) 123 | ) 124 | ) 125 | ptr := add(ptr, 4)// increment ptr by 4 bytes 126 | } 127 | case 7 {//bits 0,1,2 set 128 | // shift first left 240 bits, second 224, third 208 to write three uint16s 129 | mstore( 130 | ptr, 131 | or(// use OR to avoid multiple memory writes 132 | shl(0xf0, add(i, offset)), 133 | or( 134 | shl(0xe0, add(add(i, 1), offset)), 135 | shl(0xd0, add(add(i, 2), offset)) 136 | ) 137 | ) 138 | ) 139 | ptr := add(ptr, 6)// increment ptr by 6 bytes 140 | } 141 | } 142 | // subtract current pointer from initial to get byte length 143 | let newLen := sub(ptr, startPtr) 144 | // write byte length 145 | mstore(bitPositions, add(len, newLen)) 146 | } 147 | } 148 | 149 | /** 150 | * @dev Returns the index of the highest bit set in `self`. 151 | * Note: Requires that `self != 0` 152 | */ 153 | function highestBitSet(uint256 self) internal pure returns (uint256 r) { 154 | uint256 x = self; 155 | require (x > 0, "Bits::highestBitSet: Value 0 has no bits set"); 156 | if (x >= 0x100000000000000000000000000000000) {x >>= 128; r += 128;} 157 | if (x >= 0x10000000000000000) {x >>= 64; r += 64;} 158 | if (x >= 0x100000000) {x >>= 32; r += 32;} 159 | if (x >= 0x10000) {x >>= 16; r += 16;} 160 | if (x >= 0x100) {x >>= 8; r += 8;} 161 | if (x >= 0x10) {x >>= 4; r += 4;} 162 | if (x >= 0x4) {x >>= 2; r += 2;} 163 | if (x >= 0x2) r += 1; // No need to shift x anymore 164 | } 165 | 166 | /** 167 | * @dev Returns the index of the lowest bit set in `self`. 168 | * Note: Requires that `self != 0` 169 | */ 170 | function lowestBitSet(uint256 self) internal pure returns (uint256 _z) { 171 | require (self > 0, "Bits::lowestBitSet: Value 0 has no bits set"); 172 | uint256 _magic = 0x00818283848586878898a8b8c8d8e8f929395969799a9b9d9e9faaeb6bedeeff; 173 | uint256 val = (self & -self) * _magic >> 248; 174 | uint256 _y = val >> 5; 175 | _z = ( 176 | _y < 4 177 | ? _y < 2 178 | ? _y == 0 179 | ? 0x753a6d1b65325d0c552a4d1345224105391a310b29122104190a110309020100 180 | : 0xc976c13bb96e881cb166a933a55e490d9d56952b8d4e801485467d2362422606 181 | : _y == 2 182 | ? 0xe39ed557db96902cd38ed14fad815115c786af479b7e83247363534337271707 183 | : 0xf7cae577eec2a03cf3bad76fb589591debb2dd67e0aa9834bea6925f6a4a2e0e 184 | : _y < 6 185 | ? _y == 4 186 | ? 0xc8c0b887b0a8a4489c948c7f847c6125746c645c544c444038302820181008ff 187 | : 0xf6e4ed9ff2d6b458eadcdf97bd91692de2d4da8fd2d0ac50c6ae9a8272523616 188 | : _y == 6 189 | ? 0xf5ecf1b3e9debc68e1d9cfabc5997135bfb7a7a3938b7b606b5b4b3f2f1f0ffe 190 | : 0xf8f9cbfae6cc78fbefe7cdc3a1793dfcf4f0e8bbd8cec470b6a28a7a5a3e1efd 191 | ); 192 | _z >>= (val & 0x1f) << 3; 193 | return _z & 0xff; 194 | } 195 | 196 | /** 197 | * @dev Returns a boolean indicating whether `bit` is the highest set bit 198 | * in the integer and the index of the next lowest set bit if it is not. 199 | */ 200 | function nextLowestBitSet(uint256 self, uint256 bit) 201 | internal 202 | pure 203 | returns (bool haveValueBefore, uint256 previousBit) 204 | { 205 | uint256 val = self << (256 - bit); 206 | if (val == 0) { 207 | return (false, 0); 208 | } 209 | return (true, (highestBitSet(val) - (256 - bit))); 210 | } 211 | 212 | /** 213 | * @dev Returns a boolean indicating whether `bit` is the lowest set bit 214 | * in the integer and the index of the next highest set bit if it is not. 215 | */ 216 | function nextHighestBitSet(uint256 self, uint256 bit) 217 | internal 218 | pure 219 | returns (bool haveValueAfter, uint256 nextBit) 220 | { 221 | uint256 val = self >> (bit + 1); 222 | if (val == 0) { 223 | return (false, 0); 224 | } 225 | return (true, lowestBitSet(val) + (bit + 1)); 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /contracts/lib/FixedPoint.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.6.0; 3 | 4 | 5 | /************************************************************************************************ 6 | From https://github.com/Uniswap/uniswap-lib/blob/master/contracts/libraries/FixedPoint.sol 7 | 8 | Copied from the github repository at commit hash 9642a0705fdaf36b477354a4167a8cd765250860. 9 | 10 | Modifications: 11 | - Removed `sqrt` function 12 | 13 | Subject to the GPL-3.0 license 14 | *************************************************************************************************/ 15 | 16 | 17 | // a library for handling binary fixed point numbers (https://en.wikipedia.org/wiki/Q_(number_format)) 18 | library FixedPoint { 19 | // range: [0, 2**112 - 1] 20 | // resolution: 1 / 2**112 21 | struct uq112x112 { 22 | uint224 _x; 23 | } 24 | 25 | // range: [0, 2**144 - 1] 26 | // resolution: 1 / 2**112 27 | struct uq144x112 { 28 | uint _x; 29 | } 30 | 31 | uint8 private constant RESOLUTION = 112; 32 | uint private constant Q112 = uint(1) << RESOLUTION; 33 | uint private constant Q224 = Q112 << RESOLUTION; 34 | 35 | // encode a uint112 as a UQ112x112 36 | function encode(uint112 x) internal pure returns (uq112x112 memory) { 37 | return uq112x112(uint224(x) << RESOLUTION); 38 | } 39 | 40 | // encodes a uint144 as a UQ144x112 41 | function encode144(uint144 x) internal pure returns (uq144x112 memory) { 42 | return uq144x112(uint256(x) << RESOLUTION); 43 | } 44 | 45 | // divide a UQ112x112 by a uint112, returning a UQ112x112 46 | function div(uq112x112 memory self, uint112 x) internal pure returns (uq112x112 memory) { 47 | require(x != 0, "FixedPoint: DIV_BY_ZERO"); 48 | return uq112x112(self._x / uint224(x)); 49 | } 50 | 51 | // multiply a UQ112x112 by a uint, returning a UQ144x112 52 | // reverts on overflow 53 | function mul(uq112x112 memory self, uint y) internal pure returns (uq144x112 memory) { 54 | uint z; 55 | require( 56 | y == 0 || (z = uint(self._x) * y) / y == uint(self._x), 57 | "FixedPoint: MULTIPLICATION_OVERFLOW" 58 | ); 59 | return uq144x112(z); 60 | } 61 | 62 | // returns a UQ112x112 which represents the ratio of the numerator to the denominator 63 | // equivalent to encode(numerator).div(denominator) 64 | function fraction(uint112 numerator, uint112 denominator) internal pure returns (uq112x112 memory) { 65 | require(denominator > 0, "FixedPoint: DIV_BY_ZERO"); 66 | return uq112x112((uint224(numerator) << RESOLUTION) / denominator); 67 | } 68 | 69 | // decode a UQ112x112 into a uint112 by truncating after the radix point 70 | function decode(uq112x112 memory self) internal pure returns (uint112) { 71 | return uint112(self._x >> RESOLUTION); 72 | } 73 | 74 | // decode a UQ144x112 into a uint144 by truncating after the radix point 75 | function decode144(uq144x112 memory self) internal pure returns (uint144) { 76 | return uint144(self._x >> RESOLUTION); 77 | } 78 | 79 | // take the reciprocal of a UQ112x112 80 | function reciprocal(uq112x112 memory self) internal pure returns (uq112x112 memory) { 81 | require(self._x != 0, "FixedPoint: ZERO_RECIPROCAL"); 82 | return uq112x112(uint224(Q224 / self._x)); 83 | } 84 | } -------------------------------------------------------------------------------- /contracts/lib/IndexedPriceMapLibrary.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.6.0; 3 | 4 | /* ========== Internal Libraries ========== */ 5 | import "./PriceLibrary.sol"; 6 | import "./KeyIndex.sol"; 7 | 8 | 9 | library IndexedPriceMapLibrary { 10 | using PriceLibrary for address; 11 | using KeyIndex for mapping(uint256 => uint256); 12 | 13 | /* ========== Constants ========== */ 14 | 15 | // Period over which prices are observed, each period should have 1 price observation. 16 | uint256 public constant OBSERVATION_PERIOD = 1 hours; 17 | 18 | // Minimum time elapsed between stored price observations 19 | uint256 public constant MINIMUM_OBSERVATION_DELAY = 0.5 hours; 20 | 21 | /* ========== Struct ========== */ 22 | 23 | struct IndexedPriceMap { 24 | mapping(uint256 => uint256) keyIndex; 25 | mapping(uint256 => PriceLibrary.PriceObservation) priceMap; 26 | } 27 | 28 | /* ========= Utility Functions ========= */ 29 | 30 | /** 31 | * @dev Returns the price key for `timestamp`, which is the hour index. 32 | */ 33 | function toPriceKey(uint256 timestamp) internal pure returns (uint256/* priceKey */) { 34 | return timestamp / OBSERVATION_PERIOD; 35 | } 36 | 37 | /** 38 | * @dev Returns the number of seconds that have passed since the beginning of the hour. 39 | */ 40 | function timeElapsedSinceWindowStart(uint256 timestamp) internal pure returns (uint256/* timeElapsed */) { 41 | return timestamp % OBSERVATION_PERIOD; 42 | } 43 | 44 | /* ========= Mutative Functions ========= */ 45 | 46 | /** 47 | * @dev Writes `observation` to storage if the price can be updated. If it is 48 | * updated, also marks the price key for the observation as having a value in 49 | * the key index. 50 | * 51 | * Note: The price can be updated if there is none recorded for the current 52 | * hour 30 minutes have passed since the last price update. 53 | * Returns a boolean indicating whether the price was updated. 54 | */ 55 | function writePriceObservation( 56 | IndexedPriceMap storage indexedPriceMap, 57 | PriceLibrary.PriceObservation memory observation 58 | ) internal returns (bool/* didUpdatePrice */) { 59 | bool canUpdate = sufficientDelaySinceLastPrice(indexedPriceMap, observation.timestamp); 60 | if (canUpdate) { 61 | uint256 priceKey = toPriceKey(observation.timestamp); 62 | canUpdate = indexedPriceMap.keyIndex.markSetKey(priceKey); 63 | if (canUpdate) { 64 | indexedPriceMap.priceMap[priceKey] = observation; 65 | } 66 | } 67 | return canUpdate; 68 | } 69 | 70 | /* ========= Price Update View Functions ========= */ 71 | 72 | /** 73 | * @dev Checks whether sufficient time has passed since the beginning of the observation 74 | * window or since the price recorded in the previous window (if any) for a new price 75 | * to be recorded. 76 | */ 77 | function sufficientDelaySinceLastPrice( 78 | IndexedPriceMap storage indexedPriceMap, 79 | uint32 newTimestamp 80 | ) internal view returns (bool/* hasSufficientDelay */) { 81 | uint256 priceKey = toPriceKey(newTimestamp); 82 | // If half the observation period has already passed since the beginning of the 83 | // current window, we can write the price without checking the previous window. 84 | if (timeElapsedSinceWindowStart(newTimestamp) >= MINIMUM_OBSERVATION_DELAY) { 85 | return true; 86 | } else { 87 | // Verify that at least half the observation period has passed since the last price observation. 88 | PriceLibrary.PriceObservation storage lastObservation = indexedPriceMap.priceMap[priceKey - 1]; 89 | if ( 90 | lastObservation.timestamp == 0 || 91 | newTimestamp - lastObservation.timestamp >= MINIMUM_OBSERVATION_DELAY 92 | ) { 93 | return true; 94 | } 95 | } 96 | return false; 97 | } 98 | 99 | /** 100 | * @dev Checks if a price can be updated. PriceLibrary can be updated if there is no price 101 | * observation for the current hour and at least 30 minutes have passed since the 102 | * observation in the previous hour (if there is one). 103 | */ 104 | function canUpdatePrice( 105 | IndexedPriceMap storage indexedPriceMap, 106 | uint32 newTimestamp 107 | ) internal view returns (bool/* canUpdatePrice */) { 108 | uint256 priceKey = toPriceKey(newTimestamp); 109 | // Verify there is not already a price for the same observation window 110 | if (indexedPriceMap.keyIndex.hasKey(priceKey)) return false; 111 | return sufficientDelaySinceLastPrice(indexedPriceMap, newTimestamp); 112 | } 113 | 114 | /* ========= Price View Functions ========= */ 115 | 116 | /** 117 | * @dev Checks the key index to see if a price is recorded for `priceKey` 118 | */ 119 | function hasPriceInWindow( 120 | IndexedPriceMap storage indexedPriceMap, 121 | uint256 priceKey 122 | ) internal view returns (bool) { 123 | return indexedPriceMap.keyIndex.hasKey(priceKey); 124 | } 125 | 126 | /** 127 | * @dev Returns the price observation for `priceKey` 128 | */ 129 | function getPriceInWindow( 130 | IndexedPriceMap storage indexedPriceMap, 131 | uint256 priceKey 132 | ) internal view returns (PriceLibrary.PriceObservation memory) { 133 | return indexedPriceMap.priceMap[priceKey]; 134 | } 135 | 136 | function getPriceObservationsInRange( 137 | IndexedPriceMap storage indexedPriceMap, 138 | uint256 timeFrom, 139 | uint256 timeTo 140 | ) 141 | internal 142 | view 143 | returns (PriceLibrary.PriceObservation[] memory prices) 144 | { 145 | uint256 priceKeyFrom = toPriceKey(timeFrom); 146 | uint256 priceKeyTo = toPriceKey(timeTo); 147 | require(priceKeyTo > priceKeyFrom, "IndexedPriceMapLibrary::getPriceObservationsInRange: Invalid time range"); 148 | bytes memory bitPositions = indexedPriceMap.keyIndex.getEncodedSetKeysInRange(priceKeyFrom, priceKeyTo); 149 | // Divide by 2 because length is in bytes and relative indices are stored as uint16 150 | uint256 len = bitPositions.length / 2; 151 | prices = new PriceLibrary.PriceObservation[](len); 152 | uint256 ptr; 153 | assembly { ptr := add(bitPositions, 32) } 154 | for (uint256 i = 0; i < len; i++) { 155 | uint256 relativeIndex; 156 | assembly { 157 | relativeIndex := shr(0xf0, mload(ptr)) 158 | ptr := add(ptr, 2) 159 | } 160 | uint256 key = priceKeyFrom + relativeIndex; 161 | prices[i] = indexedPriceMap.priceMap[key]; 162 | } 163 | } 164 | 165 | /** 166 | * @dev Finds the most recent price observation before `timestamp` with a minimum 167 | * difference in observation times of `minTimeElapsed` and a maximum difference in 168 | * observation times of `maxTimeElapsed`. 169 | * 170 | * Note: `maxTimeElapsed` is only accurate to the nearest hour (rounded down) unless 171 | * it is below one hour. 172 | * 173 | * @param indexedPriceMap Struct with the indexed price mapping for the token. 174 | * @param timestamp Timestamp to search backwards from. 175 | * @param minTimeElapsed Minimum time elapsed between price observations. 176 | * @param maxTimeElapsed Maximum time elapsed between price observations. 177 | * Only accurate to the nearest hour (rounded down) unless it is below 1 hour. 178 | */ 179 | function getLastPriceObservation( 180 | IndexedPriceMap storage indexedPriceMap, 181 | uint256 timestamp, 182 | uint256 minTimeElapsed, 183 | uint256 maxTimeElapsed 184 | ) 185 | internal 186 | view 187 | returns (bool /* foundPrice */, uint256 /* lastPriceKey */) 188 | { 189 | uint256 priceKey = toPriceKey(timestamp); 190 | uint256 windowTimeElapsed = timeElapsedSinceWindowStart(timestamp); 191 | bool canBeThisWindow = minTimeElapsed <= windowTimeElapsed; 192 | bool mustBeThisWindow = maxTimeElapsed <= windowTimeElapsed; 193 | // If the observation window for `timestamp` could include a price observation less than `maxTimeElapsed` 194 | // older than `timestamp` and the time elapsed since the beginning of the hour for `timestamp` is not higher 195 | // than `maxTimeElapsed`, any allowed price must exist in the observation window for `timestamp`. 196 | if (canBeThisWindow || mustBeThisWindow) { 197 | PriceLibrary.PriceObservation storage observation = indexedPriceMap.priceMap[priceKey]; 198 | uint32 obsTimestamp = observation.timestamp; 199 | if ( 200 | obsTimestamp != 0 && 201 | timestamp > obsTimestamp && 202 | timestamp - obsTimestamp <= maxTimeElapsed && 203 | timestamp - obsTimestamp >= minTimeElapsed 204 | ) { 205 | return (true, priceKey); 206 | } 207 | if (mustBeThisWindow) { 208 | return (false, 0); 209 | } 210 | } 211 | 212 | uint256 beginSearchTime = timestamp - minTimeElapsed; 213 | priceKey = toPriceKey(beginSearchTime); 214 | uint256 maxDistance = toPriceKey(maxTimeElapsed); 215 | return indexedPriceMap.keyIndex.findLastSetKey(priceKey, maxDistance); 216 | } 217 | } -------------------------------------------------------------------------------- /contracts/lib/KeyIndex.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.6.0; 3 | 4 | /* ========== Internal Libraries ========== */ 5 | import "./Bits.sol"; 6 | 7 | 8 | /** 9 | * @dev Library for indexing keys stored in a sequential mapping for easier 10 | * queries. 11 | * 12 | * Every set of 256 keys in the value map is assigned a single index which 13 | * records set values as bits, where 1 indicates the map has a value at a given 14 | * key and 0 indicates it does not. 15 | * 16 | * The 'value map' is the map which stores the values with sequential keys. 17 | * The 'key index' is the map which records the indices for every 256 keys 18 | * in the value map. 19 | * 20 | * The 'key index' is the mapping which stores the indices for each 256 values 21 | * in the map. For example, the key '256' in the value map would have a key 22 | * in the key index of `1`, where the 0th bit in the index records whether a 23 | * value is set in the value map . 24 | */ 25 | library KeyIndex { 26 | using Bits for uint256; 27 | using Bits for bytes; 28 | 29 | /* ========= Utility Functions ========= */ 30 | 31 | /** 32 | * @dev Compute the map key for a given index key and position. 33 | * Multiplies indexKey by 256 and adds indexPosition. 34 | */ 35 | function toMapKey(uint256 indexKey, uint256 indexPosition) internal pure returns (uint256) { 36 | return (indexKey * 256) + indexPosition; 37 | } 38 | 39 | /** 40 | * @dev Returns the key in the key index which stores the index for the 256-bit 41 | * index which includes `mapKey` and the position in the index for that key. 42 | */ 43 | function indexKeyAndPosition(uint256 mapKey) 44 | internal 45 | pure 46 | returns (uint256 indexKey, uint256 indexPosition) 47 | { 48 | indexKey = mapKey / 256; 49 | indexPosition = mapKey % 256; 50 | } 51 | 52 | /* ========= Mutative Functions ========= */ 53 | 54 | /** 55 | * @dev Sets a bit at the position in `indexMap` corresponding to `mapKey` if the 56 | * bit is not already set. 57 | * 58 | * @param keyIndex Mapping with indices of set keys in the value map 59 | * @param mapKey Position in the value map to mark as set 60 | */ 61 | function markSetKey( 62 | mapping(uint256 => uint256) storage keyIndex, 63 | uint256 mapKey 64 | ) internal returns (bool /* didSetKey */) { 65 | (uint256 indexKey, uint256 indexPosition) = indexKeyAndPosition(mapKey); 66 | // console.log("IPOS", indexPosition); 67 | uint256 localIndex = keyIndex[indexKey]; 68 | bool canSetKey = !localIndex.bitSet(indexPosition); 69 | if (canSetKey) { 70 | keyIndex[indexKey] = localIndex.setBit(indexPosition); 71 | } 72 | return canSetKey; 73 | } 74 | 75 | /* ========= View Functions ========= */ 76 | 77 | /** 78 | * @dev Returns a boolean indicating whether a value is stored for `mapKey` in the map index. 79 | */ 80 | function hasKey( 81 | mapping(uint256 => uint256) storage keyIndex, 82 | uint256 mapKey 83 | ) internal view returns (bool) { 84 | (uint256 indexKey, uint256 indexPosition) = indexKeyAndPosition(mapKey); 85 | uint256 localIndex = keyIndex[indexKey]; 86 | if (localIndex == 0) return false; 87 | return localIndex.bitSet(indexPosition); 88 | } 89 | 90 | /** 91 | * @dev Returns a packed uint16 array with the offsets of all set keys 92 | * between `mapKeyFrom` and `mapKeyTo`. Offsets are relative to `mapKeyFrom` 93 | */ 94 | function getEncodedSetKeysInRange( 95 | mapping(uint256 => uint256) storage keyIndex, 96 | uint256 mapKeyFrom, 97 | uint256 mapKeyTo 98 | ) internal view returns (bytes memory bitPositions) { 99 | uint256 rangeSize = mapKeyTo - mapKeyFrom; 100 | (uint256 indexKeyStart, uint256 indexPositionStart) = indexKeyAndPosition(mapKeyFrom); 101 | (uint256 indexKeyEnd, uint256 indexPositionEnd) = indexKeyAndPosition(mapKeyTo); 102 | // Expand memory too accomodate the maximum number of bits that could be found 103 | // Length is 2*range because values are stored as uint16s 104 | // 30 is added because 32 bytes are stored at a time and this would go past rangeSize*2 105 | // if most bits are set 106 | bitPositions = new bytes((2 * rangeSize) + 30); 107 | // Set the length to 0, as it is used by the `writeSetBits` fn 108 | assembly { mstore(bitPositions, 0) } 109 | uint256 indexKey = indexKeyStart; 110 | // Clear the bits before `indexPositionStart` so they are not included in the search result 111 | uint256 localIndex = keyIndex[indexKey].clearBitsBefore(indexPositionStart); 112 | uint16 offset = 0; 113 | // Check each index until the last one is reached 114 | while (indexKey < indexKeyEnd) { 115 | // Relative index is set by adding provided `offset` to the bit index 116 | bitPositions.writeSetBits(localIndex, offset); 117 | indexKey += 1; 118 | localIndex = keyIndex[indexKey]; 119 | offset += 256; 120 | } 121 | // Clear the bits after `indexPositionEnd` before searching for set bits 122 | localIndex = localIndex.clearBitsAfter(indexPositionEnd); 123 | bitPositions.writeSetBits(localIndex, offset); 124 | } 125 | 126 | /** 127 | * @dev Find the most recent position before `mapKey` which the index map records 128 | * as having a set value. Returns the key in the value map for that position. 129 | * 130 | * @param keyIndex Mapping with indices of set keys in the value map 131 | * @param mapKey Position in the value map to look behind 132 | * @param maxDistance Maximum distance between the found value and `mapKey` 133 | */ 134 | function findLastSetKey( 135 | mapping(uint256 => uint256) storage keyIndex, 136 | uint256 mapKey, 137 | uint256 maxDistance 138 | ) 139 | internal 140 | view 141 | returns (bool/* found */, uint256/* mapKey */) 142 | { 143 | (uint256 indexKey, uint256 indexPosition) = indexKeyAndPosition(mapKey); 144 | uint256 distance = 0; 145 | bool found; 146 | uint256 position; 147 | uint256 localIndex; 148 | // If the position is 0, we must go to the previous index 149 | if (indexPosition == 0) { 150 | require(indexKey != 0, "KeyIndex::findLastSetKey:Can not query value prior to 0."); 151 | indexKey -= 1; 152 | distance = 1; 153 | } else { 154 | localIndex = keyIndex[indexKey]; 155 | (found, position) = localIndex.nextLowestBitSet(indexPosition); 156 | if (found) { 157 | distance += indexPosition - position; 158 | } else { 159 | distance += indexPosition + 1; 160 | indexKey -= 1; 161 | } 162 | } 163 | 164 | while (!found && distance <= maxDistance) { 165 | localIndex = keyIndex[indexKey]; 166 | if (localIndex == 0) { 167 | if (indexKey == 0) return (false, 0); 168 | distance += 256; 169 | indexKey -= 1; 170 | } else { 171 | position = localIndex.highestBitSet(); 172 | distance += 255 - position; 173 | found = true; 174 | } 175 | } 176 | if (distance > maxDistance) { 177 | return (false, 0); 178 | } 179 | return (true, toMapKey(indexKey, position)); 180 | } 181 | 182 | /** 183 | * @dev Find the next position after `mapKey` which the index map records as 184 | * having a set value. Returns the key in the value map for that position. 185 | * 186 | * @param keyIndex Mapping with indices of set values in the value map 187 | * @param mapKey Position in the value map to look ahead 188 | * @param maxDistance Maximum distance between the found value and `mapKey` 189 | */ 190 | function findNextSetKey( 191 | mapping(uint256 => uint256) storage keyIndex, 192 | uint256 mapKey, 193 | uint256 maxDistance 194 | ) 195 | internal 196 | view 197 | returns (bool/* found */, uint256/* mapKey */) 198 | { 199 | (uint256 indexKey, uint256 indexPosition) = indexKeyAndPosition(mapKey); 200 | uint256 distance = 0; 201 | bool found; 202 | uint256 position; 203 | uint256 localIndex; 204 | if (indexPosition == 255) { 205 | indexKey += 1; 206 | position = indexPosition; 207 | distance = 1; 208 | } else { 209 | localIndex = keyIndex[indexKey]; 210 | (found, position) = localIndex.nextHighestBitSet(indexPosition); 211 | if (found) { 212 | distance += position - indexPosition; 213 | } else { 214 | distance += 256 - indexPosition; 215 | indexKey += 1; 216 | } 217 | } 218 | while (!found && distance <= maxDistance) { 219 | localIndex = keyIndex[indexKey]; 220 | if (localIndex == 0) { 221 | distance += 256; 222 | indexKey += 1; 223 | } else { 224 | position = localIndex.lowestBitSet(); 225 | distance += position; 226 | found = true; 227 | } 228 | } 229 | if (distance > maxDistance) { 230 | return (false, 0); 231 | } 232 | return (true, toMapKey(indexKey, position)); 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /contracts/lib/PriceLibrary.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.6.0; 3 | pragma experimental ABIEncoderV2; 4 | 5 | /* ========== Internal Libraries ========== */ 6 | import "./FixedPoint.sol"; 7 | import "./UniswapV2OracleLibrary.sol"; 8 | import "./UniswapV2Library.sol"; 9 | 10 | 11 | library PriceLibrary { 12 | using FixedPoint for FixedPoint.uq112x112; 13 | using FixedPoint for FixedPoint.uq144x112; 14 | 15 | /* ========= Structs ========= */ 16 | 17 | struct PriceObservation { 18 | uint32 timestamp; 19 | uint224 priceCumulativeLast; 20 | uint224 ethPriceCumulativeLast; 21 | } 22 | 23 | /** 24 | * @dev Average prices for a token in terms of weth and weth in terms of the token. 25 | * 26 | * Note: The average weth price is not equivalent to the reciprocal of the average 27 | * token price. See the UniSwap whitepaper for more info. 28 | */ 29 | struct TwoWayAveragePrice { 30 | uint224 priceAverage; 31 | uint224 ethPriceAverage; 32 | } 33 | 34 | /* ========= View Functions ========= */ 35 | 36 | function pairInitialized( 37 | address uniswapFactory, 38 | address token, 39 | address weth 40 | ) 41 | internal 42 | view 43 | returns (bool) 44 | { 45 | address pair = UniswapV2Library.pairFor(uniswapFactory, token, weth); 46 | (uint112 reserve0, uint112 reserve1,) = IUniswapV2Pair(pair).getReserves(); 47 | return reserve0 != 0 && reserve1 != 0; 48 | } 49 | 50 | function observePrice( 51 | address uniswapFactory, 52 | address tokenIn, 53 | address quoteToken 54 | ) 55 | internal 56 | view 57 | returns (uint32 /* timestamp */, uint224 /* priceCumulativeLast */) 58 | { 59 | (address token0, address token1) = UniswapV2Library.sortTokens(tokenIn, quoteToken); 60 | address pair = UniswapV2Library.calculatePair(uniswapFactory, token0, token1); 61 | if (token0 == tokenIn) { 62 | (uint256 price0Cumulative, uint32 blockTimestamp) = UniswapV2OracleLibrary.currentCumulativePrice0(pair); 63 | return (blockTimestamp, uint224(price0Cumulative)); 64 | } else { 65 | (uint256 price1Cumulative, uint32 blockTimestamp) = UniswapV2OracleLibrary.currentCumulativePrice1(pair); 66 | return (blockTimestamp, uint224(price1Cumulative)); 67 | } 68 | } 69 | 70 | /** 71 | * @dev Query the current cumulative price of a token in terms of weth 72 | * and the current cumulative price of weth in terms of the token. 73 | */ 74 | function observeTwoWayPrice( 75 | address uniswapFactory, 76 | address token, 77 | address weth 78 | ) internal view returns (PriceObservation memory) { 79 | (address token0, address token1) = UniswapV2Library.sortTokens(token, weth); 80 | address pair = UniswapV2Library.calculatePair(uniswapFactory, token0, token1); 81 | // Get the sorted token prices 82 | ( 83 | uint256 price0Cumulative, 84 | uint256 price1Cumulative, 85 | uint32 blockTimestamp 86 | ) = UniswapV2OracleLibrary.currentCumulativePrices(pair); 87 | // Check which token is weth and which is the token, 88 | // then build the price observation. 89 | if (token0 == token) { 90 | return PriceObservation({ 91 | timestamp: blockTimestamp, 92 | priceCumulativeLast: uint224(price0Cumulative), 93 | ethPriceCumulativeLast: uint224(price1Cumulative) 94 | }); 95 | } else { 96 | return PriceObservation({ 97 | timestamp: blockTimestamp, 98 | priceCumulativeLast: uint224(price1Cumulative), 99 | ethPriceCumulativeLast: uint224(price0Cumulative) 100 | }); 101 | } 102 | } 103 | 104 | /* ========= Utility Functions ========= */ 105 | 106 | /** 107 | * @dev Computes the average price of a token in terms of weth 108 | * and the average price of weth in terms of a token using two 109 | * price observations. 110 | */ 111 | function computeTwoWayAveragePrice( 112 | PriceObservation memory observation1, 113 | PriceObservation memory observation2 114 | ) internal pure returns (TwoWayAveragePrice memory) { 115 | uint32 timeElapsed = uint32(observation2.timestamp - observation1.timestamp); 116 | FixedPoint.uq112x112 memory priceAverage = UniswapV2OracleLibrary.computeAveragePrice( 117 | observation1.priceCumulativeLast, 118 | observation2.priceCumulativeLast, 119 | timeElapsed 120 | ); 121 | FixedPoint.uq112x112 memory ethPriceAverage = UniswapV2OracleLibrary.computeAveragePrice( 122 | observation1.ethPriceCumulativeLast, 123 | observation2.ethPriceCumulativeLast, 124 | timeElapsed 125 | ); 126 | return TwoWayAveragePrice({ 127 | priceAverage: priceAverage._x, 128 | ethPriceAverage: ethPriceAverage._x 129 | }); 130 | } 131 | 132 | function computeAveragePrice( 133 | uint32 timestampStart, 134 | uint224 priceCumulativeStart, 135 | uint32 timestampEnd, 136 | uint224 priceCumulativeEnd 137 | ) internal pure returns (FixedPoint.uq112x112 memory) { 138 | return UniswapV2OracleLibrary.computeAveragePrice( 139 | priceCumulativeStart, 140 | priceCumulativeEnd, 141 | uint32(timestampEnd - timestampStart) 142 | ); 143 | } 144 | 145 | /** 146 | * @dev Computes the average price of the token the price observations 147 | * are for in terms of weth. 148 | */ 149 | function computeAverageTokenPrice( 150 | PriceObservation memory observation1, 151 | PriceObservation memory observation2 152 | ) internal pure returns (FixedPoint.uq112x112 memory) { 153 | return UniswapV2OracleLibrary.computeAveragePrice( 154 | observation1.priceCumulativeLast, 155 | observation2.priceCumulativeLast, 156 | uint32(observation2.timestamp - observation1.timestamp) 157 | ); 158 | } 159 | 160 | /** 161 | * @dev Computes the average price of weth in terms of the token 162 | * the price observations are for. 163 | */ 164 | function computeAverageEthPrice( 165 | PriceObservation memory observation1, 166 | PriceObservation memory observation2 167 | ) internal pure returns (FixedPoint.uq112x112 memory) { 168 | return UniswapV2OracleLibrary.computeAveragePrice( 169 | observation1.ethPriceCumulativeLast, 170 | observation2.ethPriceCumulativeLast, 171 | uint32(observation2.timestamp - observation1.timestamp) 172 | ); 173 | } 174 | 175 | /** 176 | * @dev Compute the average value in weth of `tokenAmount` of the 177 | * token that the average price values are for. 178 | */ 179 | function computeAverageEthForTokens( 180 | TwoWayAveragePrice memory prices, 181 | uint256 tokenAmount 182 | ) internal pure returns (uint144) { 183 | return FixedPoint.uq112x112(prices.priceAverage).mul(tokenAmount).decode144(); 184 | } 185 | 186 | /** 187 | * @dev Compute the average value of `wethAmount` weth in terms of 188 | * the token that the average price values are for. 189 | */ 190 | function computeAverageTokensForEth( 191 | TwoWayAveragePrice memory prices, 192 | uint256 wethAmount 193 | ) internal pure returns (uint144) { 194 | return FixedPoint.uq112x112(prices.ethPriceAverage).mul(wethAmount).decode144(); 195 | } 196 | } -------------------------------------------------------------------------------- /contracts/lib/UniswapV2Library.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.6.0; 3 | 4 | /************************************************************************************************ 5 | Originally from https://github.com/Uniswap/uniswap-v2-periphery/blob/master/contracts/libraries/UniswapV2Library.sol 6 | 7 | This source code has been modified from the original, which was copied from the github repository 8 | at commit hash 87edfdcaf49ccc52591502993db4c8c08ea9eec0. 9 | 10 | Subject to the GPL-3.0 license 11 | *************************************************************************************************/ 12 | 13 | 14 | library UniswapV2Library { 15 | // returns sorted token addresses, used to handle return values from pairs sorted in this order 16 | function sortTokens(address tokenA, address tokenB) 17 | internal 18 | pure 19 | returns (address token0, address token1) 20 | { 21 | require(tokenA != tokenB, "UniswapV2Library: IDENTICAL_ADDRESSES"); 22 | (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); 23 | require(token0 != address(0), "UniswapV2Library: ZERO_ADDRESS"); 24 | } 25 | 26 | function calculatePair( 27 | address factory, 28 | address token0, 29 | address token1 30 | ) internal pure returns (address pair) { 31 | pair = address( 32 | uint256( 33 | keccak256( 34 | abi.encodePacked( 35 | hex"ff", 36 | factory, 37 | keccak256(abi.encodePacked(token0, token1)), 38 | hex"96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f" // init code hash 39 | ) 40 | ) 41 | ) 42 | ); 43 | } 44 | 45 | // calculates the CREATE2 address for a pair without making any external calls 46 | function pairFor( 47 | address factory, 48 | address tokenA, 49 | address tokenB 50 | ) internal pure returns (address pair) { 51 | (address token0, address token1) = sortTokens(tokenA, tokenB); 52 | pair = calculatePair(factory, token0, token1); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /contracts/lib/UniswapV2OracleLibrary.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.6.0; 3 | 4 | /* ========== Internal Interfaces ========== */ 5 | import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol"; 6 | 7 | /* ========== Internal Libraries ========== */ 8 | import "./FixedPoint.sol"; 9 | 10 | 11 | /************************************************************************************************ 12 | Originally from https://github.com/Uniswap/uniswap-v2-periphery/blob/master/contracts/libraries/UniswapV2OracleLibrary.sol 13 | 14 | This source code has been modified from the original, which was copied from the github repository 15 | at commit hash 6d03bede0a97c72323fa1c379ed3fdf7231d0b26. 16 | 17 | Subject to the GPL-3.0 license 18 | *************************************************************************************************/ 19 | 20 | 21 | // library with helper methods for oracles that are concerned with computing average prices 22 | library UniswapV2OracleLibrary { 23 | using FixedPoint for *; 24 | 25 | // helper function that returns the current block timestamp within the range of uint32, i.e. [0, 2**32 - 1] 26 | function currentBlockTimestamp() internal view returns (uint32) { 27 | return uint32(block.timestamp % 2**32); 28 | } 29 | 30 | // produces the cumulative prices using counterfactuals to save gas and avoid a call to sync. 31 | function currentCumulativePrices(address pair) 32 | internal 33 | view 34 | returns ( 35 | uint256 price0Cumulative, 36 | uint256 price1Cumulative, 37 | uint32 blockTimestamp 38 | ) 39 | { 40 | blockTimestamp = currentBlockTimestamp(); 41 | price0Cumulative = IUniswapV2Pair(pair).price0CumulativeLast(); 42 | price1Cumulative = IUniswapV2Pair(pair).price1CumulativeLast(); 43 | 44 | // if time has elapsed since the last update on the pair, mock the accumulated price values 45 | ( 46 | uint112 reserve0, 47 | uint112 reserve1, 48 | uint32 blockTimestampLast 49 | ) = IUniswapV2Pair(pair).getReserves(); 50 | require( 51 | reserve0 != 0 && reserve1 != 0, 52 | "UniswapV2OracleLibrary::currentCumulativePrices: Pair has no reserves." 53 | ); 54 | if (blockTimestampLast != blockTimestamp) { 55 | // subtraction overflow is desired 56 | uint32 timeElapsed = blockTimestamp - blockTimestampLast; 57 | // addition overflow is desired 58 | // counterfactual 59 | price0Cumulative += ( 60 | uint256(FixedPoint.fraction(reserve1, reserve0)._x) * 61 | timeElapsed 62 | ); 63 | // counterfactual 64 | price1Cumulative += ( 65 | uint256(FixedPoint.fraction(reserve0, reserve1)._x) * 66 | timeElapsed 67 | ); 68 | } 69 | } 70 | 71 | // produces the cumulative price using counterfactuals to save gas and avoid a call to sync. 72 | // only gets the first price 73 | function currentCumulativePrice0(address pair) 74 | internal 75 | view 76 | returns (uint256 price0Cumulative, uint32 blockTimestamp) 77 | { 78 | blockTimestamp = currentBlockTimestamp(); 79 | price0Cumulative = IUniswapV2Pair(pair).price0CumulativeLast(); 80 | 81 | // if time has elapsed since the last update on the pair, mock the accumulated price values 82 | ( 83 | uint112 reserve0, 84 | uint112 reserve1, 85 | uint32 blockTimestampLast 86 | ) = IUniswapV2Pair(pair).getReserves(); 87 | require( 88 | reserve0 != 0 && reserve1 != 0, 89 | "UniswapV2OracleLibrary::currentCumulativePrice0: Pair has no reserves." 90 | ); 91 | if (blockTimestampLast != blockTimestamp) { 92 | // subtraction overflow is desired 93 | uint32 timeElapsed = blockTimestamp - blockTimestampLast; 94 | // addition overflow is desired 95 | // counterfactual 96 | price0Cumulative += ( 97 | uint256(FixedPoint.fraction(reserve1, reserve0)._x) * 98 | timeElapsed 99 | ); 100 | } 101 | } 102 | 103 | // produces the cumulative price using counterfactuals to save gas and avoid a call to sync. 104 | // only gets the second price 105 | function currentCumulativePrice1(address pair) 106 | internal 107 | view 108 | returns (uint256 price1Cumulative, uint32 blockTimestamp) 109 | { 110 | blockTimestamp = currentBlockTimestamp(); 111 | price1Cumulative = IUniswapV2Pair(pair).price1CumulativeLast(); 112 | 113 | // if time has elapsed since the last update on the pair, mock the accumulated price values 114 | ( 115 | uint112 reserve0, 116 | uint112 reserve1, 117 | uint32 blockTimestampLast 118 | ) = IUniswapV2Pair(pair).getReserves(); 119 | require( 120 | reserve0 != 0 && reserve1 != 0, 121 | "UniswapV2OracleLibrary::currentCumulativePrice1: Pair has no reserves." 122 | ); 123 | if (blockTimestampLast != blockTimestamp) { 124 | // subtraction overflow is desired 125 | uint32 timeElapsed = blockTimestamp - blockTimestampLast; 126 | // addition overflow is desired 127 | // counterfactual 128 | price1Cumulative += ( 129 | uint256(FixedPoint.fraction(reserve0, reserve1)._x) * 130 | timeElapsed 131 | ); 132 | } 133 | } 134 | 135 | function computeAveragePrice( 136 | uint224 priceCumulativeStart, 137 | uint224 priceCumulativeEnd, 138 | uint32 timeElapsed 139 | ) internal pure returns (FixedPoint.uq112x112 memory priceAverage) { 140 | // overflow is desired. 141 | priceAverage = FixedPoint.uq112x112( 142 | uint224((priceCumulativeEnd - priceCumulativeStart) / timeElapsed) 143 | ); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /contracts/mocks/BaseERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.6.0; 3 | 4 | import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; 6 | 7 | 8 | // Originally from https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol 9 | // This source code has been modified from the original. 10 | // Subject to the MIT license. 11 | 12 | 13 | /** 14 | * @dev Implementation of the {IERC20} interface. 15 | * 16 | * This implementation is agnostic to the way tokens are created. This means 17 | * that a supply mechanism has to be added in a derived contract using {_mint}. 18 | * For a generic mechanism see {ERC20PresetMinterPauser}. 19 | * 20 | * TIP: For a detailed writeup see our guide 21 | * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[How 22 | * to implement supply mechanisms]. 23 | * 24 | * We have followed general OpenZeppelin guidelines: functions revert instead 25 | * of returning `false` on failure. This behavior is nonetheless conventional 26 | * and does not conflict with the expectations of ERC20 applications. 27 | * 28 | * Additionally, an {Approval} event is emitted on calls to {transferFrom}. 29 | * This allows applications to reconstruct the allowance for all accounts just 30 | * by listening to said events. Other implementations of the EIP may not emit 31 | * these events, as it isn't required by the specification. 32 | * 33 | * Finally, the non-standard {decreaseAllowance} and {increaseAllowance} 34 | * functions have been added to mitigate the well-known issues around setting 35 | * allowances. See {IERC20-approve}. 36 | */ 37 | contract BaseERC20 is IERC20 { 38 | using SafeMath for uint256; 39 | 40 | mapping (address => uint256) internal _balances; 41 | 42 | mapping (address => mapping (address => uint256)) internal _allowances; 43 | 44 | uint256 internal _totalSupply; 45 | 46 | string private _name; 47 | string private _symbol; 48 | uint8 private _decimals; 49 | 50 | /** 51 | * @dev Sets the values for {name} and {symbol}, initializes {decimals} with 52 | * a default value of 18. 53 | * 54 | * To select a different value for {decimals}, use {_setupDecimals}. 55 | * 56 | * All three of these values are immutable: they can only be set once during 57 | * construction. 58 | */ 59 | constructor (string memory name, string memory symbol) public { 60 | _name = name; 61 | _symbol = symbol; 62 | _decimals = 18; 63 | } 64 | 65 | /** 66 | * @dev Returns the name of the token. 67 | */ 68 | function name() public view returns (string memory) { 69 | return _name; 70 | } 71 | 72 | /** 73 | * @dev Returns the symbol of the token, usually a shorter version of the 74 | * name. 75 | */ 76 | function symbol() public view returns (string memory) { 77 | return _symbol; 78 | } 79 | 80 | /** 81 | * @dev Returns the number of decimals used to get its user representation. 82 | * For example, if `decimals` equals `2`, a balance of `505` tokens should 83 | * be displayed to a user as `5,05` (`505 / 10 ** 2`). 84 | * 85 | * Tokens usually opt for a value of 18, imitating the relationship between 86 | * Ether and Wei. This is the value {ERC20} uses, unless {_setupDecimals} is 87 | * called. 88 | * 89 | * NOTE: This information is only used for _display_ purposes: it in 90 | * no way affects any of the arithmetic of the contract, including 91 | * {IERC20-balanceOf} and {IERC20-transfer}. 92 | */ 93 | function decimals() public view returns (uint8) { 94 | return _decimals; 95 | } 96 | 97 | /** 98 | * @dev See {IERC20-totalSupply}. 99 | */ 100 | function totalSupply() public view override returns (uint256) { 101 | return _totalSupply; 102 | } 103 | 104 | /** 105 | * @dev See {IERC20-balanceOf}. 106 | */ 107 | function balanceOf(address account) public view override returns (uint256) { 108 | return _balances[account]; 109 | } 110 | 111 | /** 112 | * @dev See {IERC20-transfer}. 113 | * 114 | * Requirements: 115 | * 116 | * - `recipient` cannot be the zero address. 117 | * - the caller must have a balance of at least `amount`. 118 | */ 119 | function transfer(address recipient, uint256 amount) public virtual override returns (bool) { 120 | _transfer(msg.sender, recipient, amount); 121 | return true; 122 | } 123 | 124 | /** 125 | * @dev See {IERC20-allowance}. 126 | */ 127 | function allowance(address owner, address spender) public view virtual override returns (uint256) { 128 | return _allowances[owner][spender]; 129 | } 130 | 131 | /** 132 | * @dev See {IERC20-approve}. 133 | * 134 | * Requirements: 135 | * 136 | * - `spender` cannot be the zero address. 137 | */ 138 | function approve(address spender, uint256 amount) public virtual override returns (bool) { 139 | _approve(msg.sender, spender, amount); 140 | return true; 141 | } 142 | 143 | /** 144 | * @dev See {IERC20-transferFrom}. 145 | * 146 | * Emits an {Approval} event indicating the updated allowance. This is not 147 | * required by the EIP. See the note at the beginning of {ERC20}; 148 | * 149 | * Requirements: 150 | * - `sender` and `recipient` cannot be the zero address. 151 | * - `sender` must have a balance of at least `amount`. 152 | * - the caller must have allowance for ``sender``'s tokens of at least 153 | * `amount`. 154 | */ 155 | function transferFrom(address sender, address recipient, uint256 amount) public virtual override returns (bool) { 156 | _transfer(sender, recipient, amount); 157 | _approve( 158 | sender, 159 | msg.sender, 160 | _allowances[sender][msg.sender].sub(amount, "ERC20: transfer amount exceeds allowance") 161 | ); 162 | return true; 163 | } 164 | 165 | /** 166 | * @dev Atomically increases the allowance granted to `spender` by the caller. 167 | * 168 | * This is an alternative to {approve} that can be used as a mitigation for 169 | * problems described in {IERC20-approve}. 170 | * 171 | * Emits an {Approval} event indicating the updated allowance. 172 | * 173 | * Requirements: 174 | * 175 | * - `spender` cannot be the zero address. 176 | */ 177 | function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) { 178 | _approve(msg.sender, spender, _allowances[msg.sender][spender].add(addedValue)); 179 | return true; 180 | } 181 | 182 | /** 183 | * @dev Atomically decreases the allowance granted to `spender` by the caller. 184 | * 185 | * This is an alternative to {approve} that can be used as a mitigation for 186 | * problems described in {IERC20-approve}. 187 | * 188 | * Emits an {Approval} event indicating the updated allowance. 189 | * 190 | * Requirements: 191 | * 192 | * - `spender` cannot be the zero address. 193 | * - `spender` must have allowance for the caller of at least 194 | * `subtractedValue`. 195 | */ 196 | function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) { 197 | _approve( 198 | msg.sender, 199 | spender, 200 | _allowances[msg.sender][spender].sub(subtractedValue, "ERC20: decreased allowance below zero") 201 | ); 202 | return true; 203 | } 204 | 205 | /** 206 | * @dev Moves tokens `amount` from `sender` to `recipient`. 207 | * 208 | * This is internal function is equivalent to {transfer}, and can be used to 209 | * e.g. implement automatic token fees, slashing mechanisms, etc. 210 | * 211 | * Emits a {Transfer} event. 212 | * 213 | * Requirements: 214 | * 215 | * - `sender` cannot be the zero address. 216 | * - `recipient` cannot be the zero address. 217 | * - `sender` must have a balance of at least `amount`. 218 | */ 219 | function _transfer(address sender, address recipient, uint256 amount) internal virtual { 220 | require(sender != address(0), "ERC20: transfer from the zero address"); 221 | require(recipient != address(0), "ERC20: transfer to the zero address"); 222 | 223 | _beforeTokenTransfer(sender, recipient, amount); 224 | 225 | _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance"); 226 | _balances[recipient] = _balances[recipient].add(amount); 227 | emit Transfer(sender, recipient, amount); 228 | } 229 | 230 | /** 231 | * @dev Sets `amount` as the allowance of `spender` over the `owner`s tokens. 232 | * 233 | * This is internal function is equivalent to `approve`, and can be used to 234 | * e.g. set automatic allowances for certain subsystems, etc. 235 | * 236 | * Emits an {Approval} event. 237 | * 238 | * Requirements: 239 | * 240 | * - `owner` cannot be the zero address. 241 | * - `spender` cannot be the zero address. 242 | */ 243 | function _approve(address owner, address spender, uint256 amount) internal virtual { 244 | require(owner != address(0), "ERC20: approve from the zero address"); 245 | require(spender != address(0), "ERC20: approve to the zero address"); 246 | 247 | _allowances[owner][spender] = amount; 248 | emit Approval(owner, spender, amount); 249 | } 250 | 251 | /** 252 | * @dev Sets {decimals} to a value other than the default one of 18. 253 | * 254 | * WARNING: This function should only be called from the constructor. Most 255 | * applications that interact with token contracts will not expect 256 | * {decimals} to ever change, and may work incorrectly if it does. 257 | */ 258 | function _setupDecimals(uint8 decimals_) internal { 259 | _decimals = decimals_; 260 | } 261 | 262 | /** 263 | * @dev Hook that is called before any transfer of tokens. This includes 264 | * minting and burning. 265 | * 266 | * Calling conditions: 267 | * 268 | * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens 269 | * will be to transferred to `to`. 270 | * - when `from` is zero, `amount` tokens will be minted for `to`. 271 | * - when `to` is zero, `amount` of ``from``'s tokens will be burned. 272 | * - `from` and `to` are never both zero. 273 | * 274 | * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. 275 | */ 276 | function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual { } 277 | } 278 | -------------------------------------------------------------------------------- /contracts/mocks/LiquidityAdder.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.6.0; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import { 6 | IUniswapV2Pair 7 | } from "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol"; 8 | import { 9 | IUniswapV2Factory 10 | } from "@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol"; 11 | import { 12 | IUniswapV2Router02 13 | } from "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol"; 14 | import "./MockERC20.sol"; 15 | 16 | 17 | contract LiquidityAdder { 18 | MockERC20 public immutable weth; 19 | IUniswapV2Factory public immutable factory; 20 | IUniswapV2Router02 public immutable router; 21 | 22 | constructor( 23 | address weth_, 24 | address factory_, 25 | address router_ 26 | ) public { 27 | weth = MockERC20(weth_); 28 | factory = IUniswapV2Factory(factory_); 29 | router = IUniswapV2Router02(router_); 30 | } 31 | 32 | struct LiquidityToAdd { 33 | address token; 34 | uint256 amountToken; 35 | uint256 amountWeth; 36 | } 37 | 38 | function addLiquidityMulti(LiquidityToAdd[] memory inputs) public { 39 | for (uint256 i = 0; i < inputs.length; i++) { 40 | LiquidityToAdd memory _input = inputs[i]; 41 | _addLiquidity( 42 | MockERC20(_input.token), 43 | _input.amountToken, 44 | _input.amountWeth 45 | ); 46 | } 47 | } 48 | 49 | function addLiquiditySingle(MockERC20 token, uint256 amountToken, uint256 amountWeth) public { 50 | _addLiquidity( 51 | token, 52 | amountToken, 53 | amountWeth 54 | ); 55 | } 56 | 57 | function _addLiquidity( 58 | MockERC20 token, 59 | uint256 amountToken, 60 | uint256 amountWeth 61 | ) internal { 62 | token.getFreeTokens(address(this), amountToken); 63 | weth.getFreeTokens(address(this), amountWeth); 64 | token.approve(address(router), amountToken); 65 | weth.approve(address(router), amountWeth); 66 | router.addLiquidity( 67 | address(token), 68 | address(weth), 69 | amountToken, 70 | amountWeth, 71 | amountToken / 2, 72 | amountWeth / 2, 73 | address(this), 74 | now + 1 75 | ); 76 | } 77 | } -------------------------------------------------------------------------------- /contracts/mocks/MockERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.6.0; 3 | 4 | import "./BaseERC20.sol"; 5 | 6 | 7 | contract MockERC20 is BaseERC20 { 8 | constructor( 9 | string memory name, 10 | string memory symbol 11 | ) public BaseERC20(name, symbol) {} 12 | 13 | function getFreeTokens(address to, uint256 amount) public { 14 | _mint(to, amount); 15 | } 16 | 17 | /** 18 | * @dev Creates `amount` tokens and assigns them to `account`, increasing 19 | * the total supply. 20 | * Emits a {Transfer} event with `from` set to the zero address. 21 | * 22 | * Requirements: 23 | * - `to` cannot be the zero address. 24 | */ 25 | function _mint(address account, uint256 amount) internal virtual { 26 | require(account != address(0), "ERC20: mint to the zero address"); 27 | _totalSupply = _totalSupply.add(amount); 28 | _balances[account] = _balances[account].add(amount); 29 | emit Transfer(address(0), account, amount); 30 | } 31 | } -------------------------------------------------------------------------------- /contracts/mocks/UniswapV2PriceOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.6.0; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import "../lib/FixedPoint.sol"; 6 | import { PriceLibrary as Prices } from "../lib/PriceLibrary.sol"; 7 | import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 8 | 9 | 10 | /** 11 | * @dev This contract is a UniSwapV2 price oracle that tracks the 12 | * time weighted moving average price of tokens in terms of WETH. 13 | * 14 | * The price oracle is deployed with an observation period parameter 15 | * which defines the default time over which the oracle should average 16 | * prices. 17 | * 18 | * In order to query the price of a token from the oracle, the latest 19 | * price observation from UniSwap must be at least half the observation 20 | * period old and at most twice the observation period old. 21 | * 22 | * For further reading, see: 23 | * https://uniswap.org/blog/uniswap-v2/#price-oracles 24 | * https://uniswap.org/whitepaper.pdf#subsection.2.2 25 | */ 26 | contract UniswapV2PriceOracle { 27 | using Prices for address; 28 | using Prices for Prices.PriceObservation; 29 | using Prices for Prices.TwoWayAveragePrice; 30 | using FixedPoint for FixedPoint.uq112x112; 31 | using FixedPoint for FixedPoint.uq144x112; 32 | 33 | /* ========== Constants ========== */ 34 | 35 | // Period over which prices are observed, each period should have 1 price observation. 36 | uint32 public immutable OBSERVATION_PERIOD; 37 | 38 | // Minimum time elapsed between price observations 39 | uint32 public immutable MINIMUM_OBSERVATION_DELAY; 40 | 41 | // Maximum age an observation can have to still be usable in standard price queries. 42 | uint32 public MAXIMUM_OBSERVATION_AGE; 43 | 44 | /* ========== Events ========== */ 45 | 46 | event PriceUpdated( 47 | address indexed token, 48 | uint224 tokenPriceCumulativeLast, 49 | uint224 ethPriceCumulativeLast 50 | ); 51 | 52 | /* ========== Storage ========== */ 53 | 54 | // Uniswap factory address 55 | address internal immutable _uniswapFactory; 56 | 57 | // Wrapped ether token address 58 | address internal immutable _weth; 59 | 60 | // Price observations for tokens indexed by time period. 61 | mapping( 62 | address => mapping(uint256 => Prices.PriceObservation) 63 | ) internal _priceObservations; 64 | 65 | constructor( 66 | address uniswapFactory, 67 | address weth, 68 | uint32 observationPeriod 69 | ) public { 70 | _uniswapFactory = uniswapFactory; 71 | _weth = weth; 72 | OBSERVATION_PERIOD = observationPeriod; 73 | MINIMUM_OBSERVATION_DELAY = observationPeriod / 2; 74 | MAXIMUM_OBSERVATION_AGE = observationPeriod * 2; 75 | } 76 | 77 | function setMaximumObservationAge(uint32 maxObservationAge) public { 78 | MAXIMUM_OBSERVATION_AGE = maxObservationAge; 79 | } 80 | 81 | /* ========== Price Updates ========== */ 82 | 83 | /** 84 | * @dev Updates the latest price observation for a token if allowable. 85 | * 86 | * Note: The price can only be updated once per period, and price 87 | * observations must be made at least half a period apart. 88 | * 89 | * @param token Token to update the price of 90 | * @return didUpdate Whether the token price was updated. 91 | */ 92 | function updatePrice(address token) public returns (bool didUpdate) { 93 | Prices.PriceObservation memory _new = _uniswapFactory.observeTwoWayPrice(token, _weth); 94 | // We use the observation's timestamp rather than `now` because the 95 | // UniSwap pair may not have updated the price this block. 96 | uint256 observationIndex = observationIndexOf(_new.timestamp); 97 | 98 | Prices.PriceObservation storage current = _priceObservations[token][observationIndex]; 99 | if (current.timestamp != 0) { 100 | // If an observation has already been made for this period, do not update. 101 | return false; 102 | } 103 | 104 | Prices.PriceObservation memory previous = _priceObservations[token][observationIndex - 1]; 105 | uint256 timeElapsed = _new.timestamp - previous.timestamp; 106 | if (timeElapsed < MINIMUM_OBSERVATION_DELAY) { 107 | // If less than half a period has passed since the previous observation, do not update. 108 | return false; 109 | } 110 | _priceObservations[token][observationIndex] = _new; 111 | emit PriceUpdated(token, _new.priceCumulativeLast, _new.ethPriceCumulativeLast); 112 | return true; 113 | } 114 | 115 | /** 116 | * @dev Updates the prices of multiple tokens. 117 | * 118 | * @param tokens Array of tokens to update the prices of 119 | * @return updates Array of boolean values indicating which tokens 120 | * successfully updated their prices. 121 | */ 122 | function updatePrices(address[] memory tokens) 123 | public 124 | returns (bool[] memory updates) 125 | { 126 | updates = new bool[](tokens.length); 127 | for (uint256 i = 0; i < tokens.length; i++) { 128 | updates[i] = updatePrice(tokens[i]); 129 | } 130 | } 131 | 132 | /* ========== Observation Queries ========== */ 133 | 134 | function getLastPriceObservation(address token) 135 | external 136 | view 137 | returns (Prices.PriceObservation memory) 138 | { 139 | Prices.PriceObservation memory current = _uniswapFactory.observeTwoWayPrice(token, _weth); 140 | Prices.PriceObservation memory previous = _getLatestUsableObservation( 141 | token, 142 | current.timestamp 143 | ); 144 | return previous; 145 | } 146 | 147 | /** 148 | * @dev Gets the price observation at `observationIndex` for `token`. 149 | * 150 | * Note: This does not assert that there is an observation for that index, 151 | * this should be verified by the recipient. 152 | */ 153 | function getPriceObservation(address token, uint256 observationIndex) 154 | external 155 | view 156 | returns (Prices.PriceObservation memory) 157 | { 158 | return _priceObservations[token][observationIndex]; 159 | } 160 | 161 | /** 162 | * @dev Gets the observation index for `timestamp` 163 | */ 164 | function observationIndexOf(uint256 timestamp) public view returns (uint256) { 165 | return timestamp / OBSERVATION_PERIOD; 166 | } 167 | 168 | function canUpdatePrice(address token) external view returns (bool) { 169 | Prices.PriceObservation memory _new = _uniswapFactory.observeTwoWayPrice(token, _weth); 170 | // We use the observation's timestamp rather than `now` because the 171 | // UniSwap pair may not have updated the price this block. 172 | uint256 observationIndex = observationIndexOf(_new.timestamp); 173 | // If this period already has an observation, return false. 174 | if (_priceObservations[token][observationIndex].timestamp != 0) 175 | return false; 176 | // An observation can be made if the last update was at least half a period ago. 177 | uint32 timeElapsed = _new.timestamp - 178 | _priceObservations[token][observationIndex - 1].timestamp; 179 | return timeElapsed >= MINIMUM_OBSERVATION_DELAY; 180 | } 181 | 182 | /* ========== Value Queries ========== */ 183 | 184 | /** 185 | * @dev Computes the average value in weth of `amountIn` of `token`. 186 | */ 187 | function computeAverageAmountOut(address token, uint256 amountIn) 188 | public 189 | view 190 | returns (uint144 amountOut) 191 | { 192 | FixedPoint.uq112x112 memory priceAverage = computeAverageTokenPrice(token); 193 | return priceAverage.mul(amountIn).decode144(); 194 | } 195 | 196 | /** 197 | * @dev Computes the average value in `token` of `amountOut` of weth. 198 | */ 199 | function computeAverageAmountIn(address token, uint256 amountOut) 200 | public 201 | view 202 | returns (uint144 amountIn) 203 | { 204 | FixedPoint.uq112x112 memory priceAverage = computeAverageEthPrice(token); 205 | return priceAverage.mul(amountOut).decode144(); 206 | } 207 | 208 | /** 209 | * @dev Compute the average value in weth of each token in `tokens` 210 | * for the corresponding token amount in `amountsIn`. 211 | */ 212 | function computeAverageAmountsOut( 213 | address[] calldata tokens, 214 | uint256[] calldata amountsIn 215 | ) 216 | external 217 | view 218 | returns (uint144[] memory amountsOut) 219 | { 220 | uint256 len = tokens.length; 221 | require(amountsIn.length == len, "ERR_ARR_LEN"); 222 | amountsOut = new uint144[](len); 223 | for (uint256 i = 0; i < len; i++) { 224 | amountsOut[i] = computeAverageAmountOut(tokens[i], amountsIn[i]); 225 | } 226 | } 227 | 228 | 229 | /** 230 | * @dev Compute the average value of each amount of weth in `amountsOut` 231 | * in terms of the corresponding token in `tokens`. 232 | */ 233 | function computeAverageAmountsIn( 234 | address[] calldata tokens, 235 | uint256[] calldata amountsOut 236 | ) 237 | external 238 | view 239 | returns (uint144[] memory amountsIn) 240 | { 241 | uint256 len = tokens.length; 242 | require(amountsOut.length == len, "ERR_ARR_LEN"); 243 | amountsIn = new uint144[](len); 244 | for (uint256 i = 0; i < len; i++) { 245 | amountsIn[i] = computeAverageAmountIn(tokens[i], amountsOut[i]); 246 | } 247 | } 248 | 249 | /* ========== Price Queries ========== */ 250 | 251 | /** 252 | * @dev Returns the UQ112x112 struct representing the average price of 253 | * `token` in terms of weth. 254 | * 255 | * Note: Requires that the token has a price observation between 0.5 256 | * and 2 periods old. 257 | */ 258 | function computeAverageTokenPrice(address token) 259 | public 260 | view 261 | returns (FixedPoint.uq112x112 memory priceAverage) 262 | { 263 | // Get the current cumulative price 264 | Prices.PriceObservation memory current = _uniswapFactory.observeTwoWayPrice(token, _weth); 265 | // Get the latest usable price 266 | Prices.PriceObservation memory previous = _getLatestUsableObservation( 267 | token, 268 | current.timestamp 269 | ); 270 | 271 | return previous.computeAverageTokenPrice(current); 272 | } 273 | 274 | /** 275 | * @dev Returns the UQ112x112 struct representing the average price of 276 | * weth in terms of `token`. 277 | * 278 | * Note: Requires that the token has a price observation between 0.5 279 | * and 2 periods old. 280 | */ 281 | function computeAverageEthPrice(address token) 282 | public 283 | view 284 | returns (FixedPoint.uq112x112 memory priceAverage) 285 | { 286 | // Get the current cumulative price 287 | Prices.PriceObservation memory current = _uniswapFactory.observeTwoWayPrice(token, _weth); 288 | // Get the latest usable price 289 | Prices.PriceObservation memory previous = _getLatestUsableObservation( 290 | token, 291 | current.timestamp 292 | ); 293 | 294 | return previous.computeAverageEthPrice(current); 295 | } 296 | 297 | /** 298 | * @dev Returns the TwoWayAveragePrice struct representing the average price of 299 | * weth in terms of `token` and the average price of `token` in terms of weth. 300 | * 301 | * Note: Requires that the token has a price observation between 0.5 302 | * and 2 periods old. 303 | */ 304 | function computeTwoWayAveragePrice(address token) 305 | public 306 | view 307 | returns (Prices.TwoWayAveragePrice memory) 308 | { 309 | // Get the current cumulative price 310 | Prices.PriceObservation memory current = _uniswapFactory.observeTwoWayPrice(token, _weth); 311 | // Get the latest usable price 312 | Prices.PriceObservation memory previous = _getLatestUsableObservation( 313 | token, 314 | current.timestamp 315 | ); 316 | 317 | return previous.computeTwoWayAveragePrice(current); 318 | } 319 | 320 | /** 321 | * @dev Returns the UQ112x112 structs representing the average price of 322 | * each token in `tokens` in terms of weth. 323 | */ 324 | function computeAverageTokenPrices(address[] memory tokens) 325 | public 326 | view 327 | returns (FixedPoint.uq112x112[] memory averagePrices) 328 | { 329 | uint256 len = tokens.length; 330 | averagePrices = new FixedPoint.uq112x112[](len); 331 | for (uint256 i = 0; i < len; i++) { 332 | averagePrices[i] = computeAverageTokenPrice(tokens[i]); 333 | } 334 | } 335 | 336 | /** 337 | * @dev Returns the UQ112x112 structs representing the average price of 338 | * weth in terms of each token in `tokens`. 339 | */ 340 | function computeAverageEthPrices(address[] memory tokens) 341 | public 342 | view 343 | returns (FixedPoint.uq112x112[] memory averagePrices) 344 | { 345 | uint256 len = tokens.length; 346 | averagePrices = new FixedPoint.uq112x112[](len); 347 | for (uint256 i = 0; i < len; i++) { 348 | averagePrices[i] = computeAverageEthPrice(tokens[i]); 349 | } 350 | } 351 | 352 | /** 353 | * @dev Returns the TwoWayAveragePrice structs representing the average price of 354 | * weth in terms of each token in `tokens` and the average price of each token 355 | * in terms of weth. 356 | * 357 | * Note: Requires that the token has a price observation between 0.5 358 | * and 2 periods old. 359 | */ 360 | function computeTwoWayAveragePrices(address[] memory tokens) 361 | public 362 | view 363 | returns (Prices.TwoWayAveragePrice[] memory averagePrices) 364 | { 365 | uint256 len = tokens.length; 366 | averagePrices = new Prices.TwoWayAveragePrice[](len); 367 | for (uint256 i = 0; i < len; i++) { 368 | averagePrices[i] = computeTwoWayAveragePrice(tokens[i]); 369 | } 370 | } 371 | 372 | /* ========== Internal Observation Functions ========== */ 373 | 374 | /** 375 | * @dev Gets the latest price observation which is at least half a period older 376 | * than `timestamp` and at most 2 periods older. 377 | * 378 | * @param token Token to get the latest price for 379 | * @param timestamp Reference timestamp for comparison 380 | */ 381 | function _getLatestUsableObservation(address token, uint32 timestamp) 382 | internal 383 | view 384 | returns (Prices.PriceObservation memory observation) 385 | { 386 | uint256 observationIndex = observationIndexOf(timestamp); 387 | uint256 periodTimeElapsed = timestamp % OBSERVATION_PERIOD; 388 | // uint256 maxAge = MAXIMUM_OBSERVATION_AGE; 389 | // Before looking at the current observation period, check if it is possible 390 | // for an observation in the current period to be more than half a period old. 391 | if (periodTimeElapsed >= MINIMUM_OBSERVATION_DELAY) { 392 | observation = _priceObservations[token][observationIndex]; 393 | if ( 394 | observation.timestamp != 0 && 395 | timestamp - observation.timestamp >= MINIMUM_OBSERVATION_DELAY 396 | ) { 397 | return observation; 398 | } 399 | } 400 | 401 | // Check the observation for the previous period 402 | observation = _priceObservations[token][--observationIndex]; 403 | uint256 timeElapsed = timestamp - observation.timestamp; 404 | bool usable = observation.timestamp != 0 && timeElapsed >= MINIMUM_OBSERVATION_DELAY; 405 | while(!usable) { 406 | observation = _priceObservations[token][--observationIndex]; 407 | uint256 obsTime = observation.timestamp; 408 | timeElapsed = timestamp - (obsTime == 0 ? OBSERVATION_PERIOD * observationIndex : obsTime); 409 | usable = observation.timestamp != 0 && timeElapsed >= MINIMUM_OBSERVATION_DELAY; 410 | require( 411 | timeElapsed <= MAXIMUM_OBSERVATION_AGE, 412 | "ERR_USABLE_PRICE_NOT_FOUND" 413 | ); 414 | } 415 | return observation; 416 | } 417 | } 418 | -------------------------------------------------------------------------------- /contracts/mocks/tests/TestErrorTriggers.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.6.0; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import "../../lib/UniswapV2Library.sol"; 6 | import "../../lib/Bits.sol"; 7 | 8 | 9 | contract TestErrorTriggers { 10 | function triggerUniswapLibrarySameTokenError() public pure { 11 | UniswapV2Library.sortTokens(address(1), address(1)); 12 | } 13 | 14 | function triggerUniswapLibraryNullTokenError() public pure { 15 | UniswapV2Library.sortTokens(address(1), address(0)); 16 | } 17 | 18 | function triggerHighestBitError() public pure { 19 | Bits.highestBitSet(0); 20 | } 21 | 22 | function triggerLowestBitError() public pure { 23 | Bits.lowestBitSet(0); 24 | } 25 | } -------------------------------------------------------------------------------- /contracts/mocks/tests/TestPriceLibrary.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.6.0; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import "../../lib/PriceLibrary.sol"; 6 | import "../../lib/Bits.sol"; 7 | 8 | 9 | contract TestPriceLibrary { 10 | address internal immutable _uniswapFactory; 11 | address internal immutable _weth; 12 | 13 | constructor(address uniswapFactory_, address weth) public { 14 | _uniswapFactory = uniswapFactory_; 15 | _weth = weth; 16 | } 17 | 18 | function pairInitialized( 19 | address token, 20 | address weth 21 | ) 22 | public 23 | view 24 | returns (bool) 25 | { 26 | return PriceLibrary.pairInitialized(_uniswapFactory, token, weth); 27 | } 28 | 29 | function observePrice( 30 | address tokenIn, 31 | address quoteToken 32 | ) 33 | public 34 | view 35 | returns (uint32 timestamp, uint224 priceCumulativeLast) 36 | { 37 | return PriceLibrary.observePrice(_uniswapFactory, tokenIn, quoteToken); 38 | } 39 | 40 | function observeTwoWayPrice( 41 | address token, 42 | address weth 43 | ) public view returns (PriceLibrary.PriceObservation memory) { 44 | return PriceLibrary.observeTwoWayPrice(_uniswapFactory, token, weth); 45 | } 46 | 47 | function computeTwoWayAveragePrice( 48 | PriceLibrary.PriceObservation calldata observation1, 49 | PriceLibrary.PriceObservation calldata observation2 50 | ) 51 | external 52 | pure 53 | returns (PriceLibrary.TwoWayAveragePrice memory) 54 | { 55 | return PriceLibrary.computeTwoWayAveragePrice(observation1, observation2); 56 | } 57 | 58 | function computeAveragePrice( 59 | uint32 timestampStart, 60 | uint224 priceCumulativeStart, 61 | uint32 timestampEnd, 62 | uint224 priceCumulativeEnd 63 | ) 64 | public 65 | pure 66 | returns (FixedPoint.uq112x112 memory) 67 | { 68 | return PriceLibrary.computeAveragePrice( 69 | timestampStart, 70 | priceCumulativeStart, 71 | timestampEnd, 72 | priceCumulativeEnd 73 | ); 74 | } 75 | 76 | function computeAverageTokenPrice( 77 | PriceLibrary.PriceObservation calldata observation1, 78 | PriceLibrary.PriceObservation calldata observation2 79 | ) 80 | external 81 | pure 82 | returns (FixedPoint.uq112x112 memory) 83 | { 84 | return PriceLibrary.computeAverageTokenPrice(observation1, observation2); 85 | } 86 | 87 | function computeAverageEthPrice( 88 | PriceLibrary.PriceObservation calldata observation1, 89 | PriceLibrary.PriceObservation calldata observation2 90 | ) 91 | external 92 | pure 93 | returns (FixedPoint.uq112x112 memory) 94 | { 95 | return PriceLibrary.computeAverageEthPrice(observation1, observation2); 96 | } 97 | 98 | function computeAverageEthForTokens( 99 | PriceLibrary.TwoWayAveragePrice calldata prices, 100 | uint256 tokenAmount 101 | ) 102 | external 103 | pure 104 | returns (uint144) 105 | { 106 | return PriceLibrary.computeAverageEthForTokens(prices, tokenAmount); 107 | } 108 | 109 | function computeAverageTokensForEth( 110 | PriceLibrary.TwoWayAveragePrice calldata prices, 111 | uint256 wethAmount 112 | ) external pure returns (uint144) { 113 | return PriceLibrary.computeAverageTokensForEth(prices, wethAmount); 114 | } 115 | } -------------------------------------------------------------------------------- /deploy/oracle.deploy.js: -------------------------------------------------------------------------------- 1 | const Deployer = require('../lib/deployer'); 2 | const Logger = require('../lib/logger'); 3 | 4 | module.exports = async (bre) => { 5 | const { 6 | deployments, 7 | getChainId, 8 | getNamedAccounts, 9 | ethers 10 | } = bre; 11 | const chainID = await getChainId(); 12 | const logger = Logger(chainID) 13 | const { deployer } = await getNamedAccounts(); 14 | 15 | const deploy = await Deployer(bre, logger); 16 | const uniswapFactory = (await deployments.get('uniswapFactory')).address; 17 | const weth = (await deployments.get('weth')).address; 18 | 19 | await deploy("IndexedUniswapV2Oracle", 'IndexedOracle', { 20 | from: deployer, 21 | gas: 4000000, 22 | args: [uniswapFactory, weth] 23 | }); 24 | }; 25 | 26 | module.exports.tags = ['Oracle']; 27 | module.exports.dependencies = ['Uniswap'] -------------------------------------------------------------------------------- /deploy/uniswap.deploy.js: -------------------------------------------------------------------------------- 1 | const Deployer = require('../lib/deployer'); 2 | const Logger = require('../lib/logger'); 3 | 4 | module.exports = async (bre) => { 5 | const { getChainId, getNamedAccounts } = bre; 6 | const chainID = +(await getChainId()); 7 | const logger = Logger(chainID, 'deploy-uniswap-mocks'); 8 | 9 | const { deployer } = await getNamedAccounts(); 10 | const deploy = await Deployer(bre, logger); 11 | 12 | if (chainID == 1 && bre.network.name != 'coverage') return; 13 | 14 | const weth = await deploy('MockERC20', 'weth', { 15 | from: deployer, 16 | gas: 4000000, 17 | args: ["Wrapped Ether V9", "WETH9"] 18 | }); 19 | 20 | if (chainID == 4) return; 21 | 22 | const uniswapFactory = await deploy("UniswapV2Factory", 'uniswapFactory', { 23 | from: deployer, 24 | gas: 4000000, 25 | args: [deployer] 26 | }); 27 | 28 | const uniswapRouter = await deploy('UniswapV2Router02', 'uniswapRouter', { 29 | from: deployer, 30 | gas: 4000000, 31 | args: [uniswapFactory.address, weth.address] 32 | }); 33 | 34 | const liquidityAdder = await deploy('LiquidityAdder', 'liquidityAdder', { 35 | from: deployer, 36 | gas: 1000000, 37 | args: [ 38 | weth.address, 39 | uniswapFactory.address, 40 | uniswapRouter.address 41 | ] 42 | }, true); 43 | }; 44 | 45 | module.exports.tags = ['Mocks', 'Uniswap']; -------------------------------------------------------------------------------- /deployments/mainnet/.chainId: -------------------------------------------------------------------------------- 1 | 1 -------------------------------------------------------------------------------- /deployments/rinkeby/.chainId: -------------------------------------------------------------------------------- 1 | 4 -------------------------------------------------------------------------------- /lib/bn.js: -------------------------------------------------------------------------------- 1 | const { BigNumber } = require("ethers"); 2 | 3 | const toBN = (bn) => BigNumber.from(bn); 4 | const oneToken = toBN(10).pow(18); // 10 ** decimals 5 | const nTokens = (amount) => oneToken.mul(amount); 6 | const toHex = (bn) => bn.toHexString(); 7 | const nTokensHex = (amount) => toHex(nTokens(amount)); 8 | 9 | module.exports = { 10 | toBN, 11 | oneToken, 12 | nTokens, 13 | toHex, 14 | nTokensHex 15 | }; -------------------------------------------------------------------------------- /lib/deployer.js: -------------------------------------------------------------------------------- 1 | const Deployer = async (bre, logger) => { 2 | const { ethers } = bre; 3 | const [ signer ] = await ethers.getSigners(); 4 | const deploy = async (name, contractName, opts, returnContract = false) => { 5 | try { 6 | // if (await deployments.getOrNull(contractName)) { 7 | // logger.info(`Found ${contractName} [${name}]`); 8 | // return await ethers.getContract(contractName, signer); 9 | // } 10 | const deployment = await bre.deployments.deploy(name, { 11 | ...opts, 12 | contractName 13 | }); 14 | if (deployment.newlyDeployed) { 15 | logger.success(`Deployed ${contractName} [${name}]`); 16 | await bre.deployments.save(contractName, deployment); 17 | } else { 18 | logger.info(`Found ${contractName} [${name}]`); 19 | } 20 | if (returnContract) { 21 | const contract = await ethers.getContractAt(deployment.abi, deployment.address, signer); 22 | contract.newlyDeployed = deployment.newlyDeployed; 23 | return contract; 24 | } 25 | return deployment; 26 | } catch (err) { 27 | logger.error(`Error deploying ${contractName} [${name}]`); 28 | throw err; 29 | } 30 | }; 31 | return deploy; 32 | } 33 | 34 | module.exports = Deployer; -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const moment = require('moment'); 3 | 4 | const chalkFn = (color, bold = true, underline = false) => { 5 | let ch = chalk[color]; 6 | if (bold) ch = ch.bold; 7 | if (underline) ch = ch.underline; 8 | return ch; 9 | } 10 | 11 | const log = (color, domain, message, underline = false) => { 12 | console.log( 13 | chalkFn(color, true, underline)( 14 | `@indexed-finance/core${domain}:${moment(new Date()).format('HH:mm:ss')}: ` 15 | ) + message 16 | ); 17 | }; 18 | 19 | const Logger = (chainID = undefined, domain = '') => { 20 | if (domain != '') domain = `/${domain}`; 21 | return { 22 | info: (v, u = false) => { 23 | if (chainID !== undefined && chainID != 1 && chainID != 4) return; 24 | log('cyan', domain, v, u); 25 | return v; 26 | }, 27 | success: (v, u = false) => { 28 | if (chainID !== undefined && chainID != 1 && chainID != 4) return; 29 | log('green', domain, v, u); 30 | return v; 31 | }, 32 | error: (v, u = false) => { 33 | if (chainID !== undefined && chainID != 1 && chainID != 4) return; 34 | log('red', domain, v, u); 35 | return v; 36 | }, 37 | }; 38 | }; 39 | 40 | module.exports = Logger; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@indexed-finance/uniswap-v2-oracle", 3 | "version": "1.0.4", 4 | "description": "", 5 | "main": "buidler.config.js", 6 | "files": [ 7 | "contracts", 8 | "artifacts", 9 | "deployments/rinkeby", 10 | "deployments/mainnet" 11 | ], 12 | "scripts": { 13 | "build": "buidler compile", 14 | "deploy:rinkeby": "buidler --network rinkeby deploy --tags Oracle", 15 | "deploy:mainnet": "buidler --network mainnet deploy --tags Oracle", 16 | "coverage": "buidler coverage --network coverage --solcoverjs ./.solcover.js", 17 | "test": "echo \"Error: no test specified\" && exit 1" 18 | }, 19 | "keywords": [], 20 | "author": "", 21 | "license": "ISC", 22 | "devDependencies": { 23 | "@ethersproject/providers": "^5.0.14", 24 | "@nomiclabs/buidler": "^1.4.8", 25 | "@nomiclabs/buidler-ethers": "^2.0.2", 26 | "@nomiclabs/buidler-etherscan": "^2.1.0", 27 | "bn.js": "^5.1.3", 28 | "buidler-deploy": "^0.6.0-beta.34", 29 | "buidler-ethers-v5": "^0.2.3", 30 | "chai": "^4.2.0", 31 | "chai-as-promised": "^7.1.1", 32 | "chalk": "^4.1.0", 33 | "cli-table3": "^0.6.0", 34 | "coveralls": "^3.1.0", 35 | "dotenv": "^8.2.0", 36 | "ethereumjs-wallet": "^0.6.5", 37 | "ethers": "^5.0.17", 38 | "mocha": "^8.2.0", 39 | "moment": "^2.29.1", 40 | "prettier": "^2.1.2", 41 | "prettier-plugin-solidity": "^1.0.0-alpha.59", 42 | "solidity-coverage": "^0.7.11", 43 | "solium": "^1.2.5", 44 | "solium-plugin-security": "^0.1.1" 45 | }, 46 | "dependencies": { 47 | "@indexed-finance/uniswap-deployments": "^1.0.2", 48 | "@openzeppelin/contracts": "^3.2.0", 49 | "@uniswap/v2-core": "^1.0.1", 50 | "@uniswap/v2-periphery": "^1.1.0-beta.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/ExampleKeyIndex.spec.js: -------------------------------------------------------------------------------- 1 | const chai = require("chai"); 2 | chai.use(require('chai-as-promised')) 3 | const { expect } = chai; 4 | 5 | const freshFixture = deployments.createFixture(async ({ deployments, ethers }) => { 6 | await deployments.fixture(); 7 | const ExampleKeyIndex = await ethers.getContractFactory("ExampleKeyIndex"); 8 | const example = await ExampleKeyIndex.deploy(); 9 | return example; 10 | }); 11 | 12 | const filledFixture = deployments.createFixture(async ({ deployments, ethers }) => { 13 | await deployments.fixture(); 14 | const ExampleKeyIndex = await ethers.getContractFactory("ExampleKeyIndex"); 15 | const example = await ExampleKeyIndex.deploy(); 16 | const proms = []; 17 | for (let i = 0; i < 512; i++) { 18 | proms.push(example.writeValue(i, i).then(tx => tx.wait())); 19 | } 20 | await Promise.all(proms); 21 | return example; 22 | }); 23 | 24 | const sparseFixture = deployments.createFixture(async ({ deployments, ethers }) => { 25 | await deployments.fixture(); 26 | const ExampleKeyIndex = await ethers.getContractFactory("ExampleKeyIndex"); 27 | const example = await ExampleKeyIndex.deploy(); 28 | const proms = []; 29 | for (let i = 0; i < 512; i += 32) { 30 | proms.push(example.writeValue(i, i).then(tx => tx.wait())); 31 | } 32 | await Promise.all(proms); 33 | return example; 34 | }); 35 | 36 | describe("ExampleKeyIndex", function() { 37 | describe('getPreviousValue()', async () => { 38 | it('Reverts when 0 is the starting key', async () => { 39 | const example = await freshFixture(); 40 | await expect(example.getPreviousValue(0, 1)).to.be.rejectedWith(/KeyIndex::findLastSetKey:Can not query value prior to 0\./g); 41 | }); 42 | 43 | it('Returns false when search passes 0', async () => { 44 | const example = await freshFixture(); 45 | const [foundValue, value] = await example.getPreviousValue(256, 256); 46 | expect(foundValue).to.be.false; 47 | expect(value.toNumber()).to.eq(0); 48 | }); 49 | 50 | it('Finds previous value in filled indices', async () => { 51 | const example = await filledFixture(); 52 | 53 | let i = 511 54 | try { 55 | for (; i > 0; i--) { 56 | const [foundValue, value] = await example.getPreviousValue(i, 1); 57 | expect(foundValue).to.be.true; 58 | expect(value.toNumber()).to.eq(i - 1); 59 | } 60 | } catch (err) { 61 | console.log(i); 62 | throw err; 63 | } 64 | }); 65 | 66 | it('Finds previous value 1 key behind', async () => { 67 | const example = await freshFixture(); 68 | const receipt0 = await example.writeValue(0, 100).then(tx => tx.wait()); 69 | console.log(`First write value cost: ${receipt0.cumulativeGasUsed}`); 70 | const gasUsed1 = await example.estimateGas.writeValue(1, 200); 71 | console.log(`Second write value cost: ${gasUsed1}`); 72 | const gasUsed2 = await example.estimateGas.getPreviousValue(1, 1); 73 | console.log(`Find previous value cost: ${gasUsed2}`); 74 | const [foundValue, value] = await example.getPreviousValue(1, 1); 75 | expect(foundValue).to.be.true; 76 | expect(value.toNumber()).to.eq(100); 77 | }); 78 | 79 | it('Fails to find value further than max distance', async () => { 80 | const example = await freshFixture(); 81 | await example.writeValue(0, 100); 82 | await example.writeValue(1000, 200); 83 | let [foundValue, value] = await example.getPreviousValue(1000, 999); 84 | expect(foundValue).to.be.false; 85 | expect(value.toNumber()).to.eq(0); 86 | [foundValue, value] = await example.getPreviousValue(1000, 1000); 87 | expect(foundValue).to.be.true; 88 | expect(value.toNumber()).to.eq(100); 89 | }); 90 | }); 91 | 92 | describe('getNextValue()', async () => { 93 | it('Finds next value in filled indices', async () => { 94 | const example = await filledFixture(); 95 | 96 | try { 97 | for (let i = 0; i < 511; i++) { 98 | const [foundValue, value] = await example.getNextValue(i, 1); 99 | expect(foundValue).to.be.true; 100 | expect(value.toNumber()).to.eq(i + 1); 101 | } 102 | } catch (err) { 103 | // console.log(i); 104 | throw err; 105 | } 106 | }); 107 | 108 | it('Finds value 1 key ahead', async () => { 109 | const example = await freshFixture(); 110 | await example.writeValue(1, 100); 111 | const gasUsed = await example.estimateGas.getNextValue(0, 1); 112 | console.log(`Find next value cost (distance 1): ${gasUsed}`); 113 | const [foundValue, value] = await example.getNextValue(0, 1); 114 | expect(foundValue).to.be.true; 115 | expect(value.toNumber()).to.eq(100); 116 | }); 117 | 118 | it('Fails to find value further than max distance', async () => { 119 | const example = await freshFixture(); 120 | await example.writeValue(0, 100); 121 | await example.writeValue(1000, 200); 122 | let [foundValue, value] = await example.getNextValue(0, 999); 123 | expect(foundValue).to.be.false; 124 | expect(value.toNumber()).to.eq(0); 125 | [foundValue, value] = await example.getNextValue(0, 1000); 126 | const gasUsed = await example.estimateGas.getNextValue(0, 1000); 127 | console.log(`getNextValue() (distance 1000) | Cost ${gasUsed}`) 128 | expect(foundValue).to.be.true; 129 | expect(value.toNumber()).to.eq(200); 130 | }); 131 | }); 132 | 133 | describe('getSetKeysInRange()', async () => { 134 | it('Reverts if bad range is given', async () => { 135 | const example = await freshFixture(); 136 | await expect(example.getSetKeysInRange(1, 0)).to.be.rejectedWith(/ExampleKeyIndex::getSetKeysInRange: Invalid Range/g); 137 | }); 138 | 139 | it('Returns all set keys in filled range', async () => { 140 | const example = await filledFixture(); 141 | const setKeys = await example.getSetKeysInRange(0, 512); 142 | const gas = await example.estimateGas.getSetKeysInRange(0, 512); 143 | console.log(`getSetKeysInRange(): filled [range 512] | Cost ${gas}`); 144 | expect(setKeys.length).to.eq(512); 145 | const numericKeys = setKeys.map(k => k.toNumber()); 146 | const expectedKeys = new Array(512).fill(null).map((_, i) => i); 147 | expect(numericKeys).to.deep.eq(expectedKeys); 148 | }); 149 | 150 | it('Returns all set keys in sparse range', async () => { 151 | const example = await sparseFixture(); 152 | const setKeys = await example.getSetKeysInRange(0, 512); 153 | const gas = await example.estimateGas.getSetKeysInRange(0, 512); 154 | console.log(`getSetKeysInRange(): sparse [range 512] | Cost ${gas}`); 155 | expect(setKeys.length).to.eq(16); 156 | const numericKeys = setKeys.map(k => k.toNumber()); 157 | const expectedKeys = new Array(512).fill(null).map((_, i) => i).filter(i => (i % 32) == 0); 158 | expect(numericKeys).to.deep.eq(expectedKeys); 159 | }); 160 | }); 161 | 162 | describe('getValuesInRange()', async () => { 163 | it('Reverts if bad range is given', async () => { 164 | const example = await freshFixture(); 165 | await expect(example.getValuesInRange(1, 0)).to.be.rejectedWith(/ExampleKeyIndex::getValuesInRange: Invalid Range/g); 166 | }); 167 | 168 | it('Returns all set keys in range', async () => { 169 | const example = await filledFixture(); 170 | const gas = await example.estimateGas.getValuesInRange(0, 512); 171 | console.log(`getValuesInRange() range 512 | Cost ${gas}`); 172 | const setKeys = await example.getValuesInRange(0, 512); 173 | expect(setKeys.length).to.eq(512); 174 | const numericKeys = setKeys.map(k => k.toNumber()); 175 | const expectedValues = new Array(512).fill(null).map((_, i) => i); 176 | expect(numericKeys).to.deep.eq(expectedValues); 177 | }); 178 | }); 179 | }); 180 | -------------------------------------------------------------------------------- /test/IndexedUniswapV2Oracle.spec.js: -------------------------------------------------------------------------------- 1 | const chai = require("chai"); 2 | chai.use(require('chai-as-promised')) 3 | const { expect } = chai; 4 | const bre = require('@nomiclabs/buidler'); 5 | const { expandTo18Decimals, fastForwardToNextHour, encodePrice, getTransactionTimestamp, fastForward, HOUR, fastForwardToPeriodStart } = require('./utils'); 6 | const { testTokensFixture } = require("./tokens-fixture"); 7 | const { BigNumber } = require("ethers"); 8 | 9 | const token0Amount = expandTo18Decimals(5); 10 | const token1Amount = expandTo18Decimals(6); 11 | const wethAmount = expandTo18Decimals(10); 12 | 13 | const overrides = {gasLimit: 999999}; 14 | 15 | describe('IndexedUniswapV2Oracle', async () => { 16 | let oracle; 17 | let deployer; 18 | let token0, token1, weth; 19 | let pair0, pair1; 20 | 21 | before(async () => { 22 | ({deployer} = await getNamedAccounts()); 23 | await deployments.fixture('Oracle'); 24 | const [signer] = await ethers.getSigners(); 25 | oracle = await ethers.getContract('IndexedUniswapV2Oracle', signer); 26 | weth = await ethers.getContract('weth', signer); 27 | }); 28 | 29 | let expectedPrice0; 30 | let expectedPrice1; 31 | 32 | async function addLiquidity0() { 33 | await token0.getFreeTokens(pair0.address, token0Amount); 34 | await weth.getFreeTokens(pair0.address, wethAmount); 35 | const timestamp = await getTransactionTimestamp(pair0.mint(deployer, overrides)); 36 | expectedPrice0 = encodePrice(token0Amount, wethAmount, +timestamp, expectedPrice0); 37 | } 38 | 39 | async function addLiquidity1() { 40 | await token1.getFreeTokens(pair1.address, token1Amount); 41 | await weth.getFreeTokens(pair1.address, wethAmount); 42 | const timestamp = await getTransactionTimestamp(pair1.mint(deployer, overrides)); 43 | expectedPrice1 = encodePrice(token1Amount, wethAmount, +timestamp, expectedPrice1); 44 | } 45 | 46 | describe('Restrictions', async () => { 47 | before(async () => { 48 | ({ 49 | token0, 50 | token1, 51 | pair0, 52 | pair1 53 | } = await testTokensFixture()); 54 | expectedPrice0 = undefined; 55 | expectedPrice1 = undefined; 56 | }); 57 | 58 | it('getPriceInWindow() reverts if there is no price for the window', async () => { 59 | await expect( 60 | oracle.getPriceObservationInWindow(token0.address, 0) 61 | ).to.be.rejectedWith(/IndexedUniswapV2Oracle::getPriceObservationInWindow: No price observed in given hour\./g); 62 | }); 63 | 64 | it('updatePrice() reverts if a pair has no reserves', async () => { 65 | await expect(oracle.updatePrice(token0.address)).to.be.rejectedWith( 66 | /UniswapV2OracleLibrary::currentCumulativePrices: Pair has no reserves./g 67 | ); 68 | }); 69 | 70 | it('canUpdatePrice() returns false if a pair has no reserves', async () => { 71 | const canUpdatePrice = await oracle.canUpdatePrice(token0.address); 72 | expect(canUpdatePrice).to.be.false; 73 | }); 74 | 75 | it('canUpdatePrices() returns false if a pair has no reserves', async () => { 76 | const canUpdatePrices = await oracle.canUpdatePrices([token0.address, token1.address]); 77 | expect(canUpdatePrices).to.deep.equal([false, false]); 78 | }); 79 | 80 | it('Does update price once there are reserves', async () => { 81 | await fastForwardToNextHour(); 82 | await fastForward(0.7 * HOUR); 83 | await addLiquidity0(); 84 | await addLiquidity1(); 85 | await getTransactionTimestamp(oracle.updatePrices([token0.address, token1.address])); 86 | }); 87 | 88 | it('getPriceObservationsInRange() reverts if timeFrom >= timeTo', async () => { 89 | await expect( 90 | oracle.getPriceObservationsInRange(token0.address, 1, 0) 91 | ).to.be.rejectedWith(/IndexedPriceMapLibrary::getPriceObservationsInRange: Invalid time range/g); 92 | }) 93 | 94 | it('computeAverageEthForTokens() reverts if array lengths do not match', async () => { 95 | await expect( 96 | oracle['computeAverageEthForTokens(address[],uint256[],uint256,uint256)']( 97 | [token0.address], [0, 1], 0.5 * HOUR, 2 * HOUR 98 | ) 99 | ).to.be.rejectedWith( 100 | /IndexedUniswapV2Oracle::computeAverageEthForTokens: Tokens and amounts have different lengths\./g 101 | ); 102 | }); 103 | 104 | it('computeAverageTokensForEth() reverts if array lengths do not match', async () => { 105 | await expect( 106 | oracle['computeAverageTokensForEth(address[],uint256[],uint256,uint256)']( 107 | [token0.address], [0, 1], 0.5 * HOUR, 2 * HOUR 108 | ) 109 | ).to.be.rejectedWith( 110 | /IndexedUniswapV2Oracle::computeAverageTokensForEth: Tokens and amounts have different lengths\./g 111 | ); 112 | }); 113 | 114 | it('All price queries fail when `minTimeElapsed` has not passed', async () => { 115 | await expect(oracle.computeTwoWayAveragePrice(token0.address, 0.5 * HOUR, 2 * HOUR)).to.be.rejectedWith(/IndexedUniswapV2Oracle::_getTwoWayPrice: No price found in provided range\./g); 116 | await expect(oracle.computeAverageTokenPrice(token0.address, 0.5 * HOUR, 2 * HOUR)).to.be.rejectedWith(/IndexedUniswapV2Oracle::_getTokenPrice: No price found in provided range\./g); 117 | await expect(oracle.computeAverageEthPrice(token0.address, 0.5 * HOUR, 2 * HOUR)).to.be.rejectedWith(/IndexedUniswapV2Oracle::_getEthPrice: No price found in provided range\./g); 118 | await expect(oracle.computeTwoWayAveragePrices([token0.address], 0.5 * HOUR, 2 * HOUR)).to.be.rejectedWith(/IndexedUniswapV2Oracle::_getTwoWayPrice: No price found in provided range\./g); 119 | await expect(oracle.computeAverageTokenPrices([token0.address], 0.5 * HOUR, 2 * HOUR)).to.be.rejectedWith(/IndexedUniswapV2Oracle::_getTokenPrice: No price found in provided range\./g); 120 | await expect(oracle.computeAverageEthPrices([token0.address], 0.5 * HOUR, 2 * HOUR)).to.be.rejectedWith(/IndexedUniswapV2Oracle::_getEthPrice: No price found in provided range\./g); 121 | 122 | await expect( 123 | oracle['computeAverageEthForTokens(address,uint256,uint256,uint256)']( 124 | token0.address, 0, 0.5 * HOUR, 2 * HOUR) 125 | ).to.be.rejectedWith(/IndexedUniswapV2Oracle::_getTokenPrice: No price found in provided range\./g 126 | ); 127 | await expect( 128 | oracle['computeAverageTokensForEth(address,uint256,uint256,uint256)']( 129 | token0.address, 0, 0.5 * HOUR, 2 * HOUR) 130 | ).to.be.rejectedWith(/IndexedUniswapV2Oracle::_getEthPrice: No price found in provided range\./g 131 | ); 132 | await expect( 133 | oracle['computeAverageEthForTokens(address[],uint256[],uint256,uint256)']( 134 | [token0.address], [0], 0.5 * HOUR, 2 * HOUR) 135 | ).to.be.rejectedWith(/IndexedUniswapV2Oracle::_getTokenPrice: No price found in provided range\./g 136 | ); 137 | await expect( 138 | oracle['computeAverageTokensForEth(address[],uint256[],uint256,uint256)']( 139 | [token0.address], [0], 0.5 * HOUR, 2 * HOUR) 140 | ).to.be.rejectedWith(/IndexedUniswapV2Oracle::_getEthPrice: No price found in provided range\./g 141 | ); 142 | }); 143 | 144 | it('All price queries fail when there are no prices between `minTimeElapsed` and `maxTimeElapsed`', async () => { 145 | await expect(oracle.computeTwoWayAveragePrice(token0.address, 0, 1)).to.be.rejectedWith(/IndexedUniswapV2Oracle::_getTwoWayPrice: No price found in provided range\./g); 146 | await expect(oracle.computeAverageTokenPrice(token0.address, 0, 1)).to.be.rejectedWith(/IndexedUniswapV2Oracle::_getTokenPrice: No price found in provided range\./g); 147 | await expect(oracle.computeAverageEthPrice(token0.address, 0, 1)).to.be.rejectedWith(/IndexedUniswapV2Oracle::_getEthPrice: No price found in provided range\./g); 148 | await expect(oracle.computeTwoWayAveragePrices([token0.address], 0, 1)).to.be.rejectedWith(/IndexedUniswapV2Oracle::_getTwoWayPrice: No price found in provided range\./g); 149 | await expect(oracle.computeAverageTokenPrices([token0.address], 0, 1)).to.be.rejectedWith(/IndexedUniswapV2Oracle::_getTokenPrice: No price found in provided range\./g); 150 | await expect(oracle.computeAverageEthPrices([token0.address], 0, 1)).to.be.rejectedWith(/IndexedUniswapV2Oracle::_getEthPrice: No price found in provided range\./g); 151 | 152 | await expect( 153 | oracle['computeAverageEthForTokens(address,uint256,uint256,uint256)'](token0.address, 0, 0, 1) 154 | ).to.be.rejectedWith(/IndexedUniswapV2Oracle::_getTokenPrice: No price found in provided range\./g); 155 | 156 | await expect( 157 | oracle['computeAverageTokensForEth(address,uint256,uint256,uint256)'](token0.address, 0, 0, 1) 158 | ).to.be.rejectedWith(/IndexedUniswapV2Oracle::_getEthPrice: No price found in provided range\./g); 159 | 160 | await expect( 161 | oracle['computeAverageEthForTokens(address[],uint256[],uint256,uint256)']([token0.address], [0], 0, 1) 162 | ).to.be.rejectedWith(/IndexedUniswapV2Oracle::_getTokenPrice: No price found in provided range\./g); 163 | 164 | await expect( 165 | oracle['computeAverageTokensForEth(address[],uint256[],uint256,uint256)']([token0.address], [0], 0, 1) 166 | ).to.be.rejectedWith(/IndexedUniswapV2Oracle::_getEthPrice: No price found in provided range\./g); 167 | }); 168 | 169 | it('canUpdatePrice() returns false during the same observation window as last update', async () => { 170 | const canUpdatePrice = await oracle.canUpdatePrice(token0.address); 171 | expect(canUpdatePrice).to.be.false; 172 | }); 173 | 174 | it('canUpdatePrices() returns false during the same observation window as last update', async () => { 175 | const canUpdatePrices = await oracle.canUpdatePrices([token0.address, token1.address]); 176 | expect(canUpdatePrices).to.deep.equal([false, false]); 177 | }); 178 | 179 | it('updatePrice() does not update during the same observation window as last update', async () => { 180 | const wouldUpdatePrice = await oracle.callStatic.updatePrice(token0.address); 181 | expect(wouldUpdatePrice).to.be.false; 182 | }); 183 | 184 | it('updatePrices() does not update during the same observation window as last update', async () => { 185 | const wouldUpdatePrices = await oracle.callStatic.updatePrices([token0.address, token1.address]); 186 | expect(wouldUpdatePrices).to.deep.equal([false, false]); 187 | }); 188 | 189 | it('canUpdatePrice() returns false during the next observation window if <30 min have passed since last update', async () => { 190 | await fastForwardToNextHour(); 191 | const canUpdatePrice = await oracle.canUpdatePrice(token0.address); 192 | expect(canUpdatePrice).to.be.false; 193 | }); 194 | 195 | it('canUpdatePrices() returns false during the next observation window if <30 min have passed since last update', async () => { 196 | const canUpdatePrices = await oracle.canUpdatePrices([token0.address, token1.address]); 197 | expect(canUpdatePrices).to.deep.equal([false, false]); 198 | }); 199 | 200 | it('updatePrice() does not update if <30 min have passed since last update', async () => { 201 | const wouldUpdatePrice = await oracle.callStatic.updatePrice(token0.address); 202 | expect(wouldUpdatePrice).to.be.false; 203 | }); 204 | 205 | it('updatePrices() does not update if <30 min have passed since last update', async () => { 206 | const wouldUpdatePrices = await oracle.callStatic.updatePrices([token0.address, token1.address]); 207 | expect(wouldUpdatePrices).to.deep.equal([false, false]); 208 | }); 209 | 210 | it('canUpdatePrice() returns true during the next observation window if >=30 min have passed since last update', async () => { 211 | await fastForward(0.3 * HOUR) 212 | const canUpdatePrice = await oracle.canUpdatePrice(token0.address); 213 | expect(canUpdatePrice).to.be.true; 214 | }); 215 | 216 | it('canUpdatePrices() returns true during the next observation window if >=30 min have passed since last update', async () => { 217 | const canUpdatePrices = await oracle.canUpdatePrices([token0.address, token1.address]); 218 | expect(canUpdatePrices).to.deep.equal([true, true]); 219 | }); 220 | 221 | describe('validMinMax', async () => { 222 | async function failsMinMax(fnName, ...beginArgs) { 223 | await expect( 224 | oracle[fnName](...beginArgs) 225 | ).to.be.rejectedWith(/IndexedUniswapV2Oracle::validMinMax: Minimum age can not be higher than maximum\./g) 226 | } 227 | 228 | it('All functions with validMinMax modifier reject invalid min/max values', async () => { 229 | await failsMinMax('computeTwoWayAveragePrice', token0.address, 2, 1); 230 | await failsMinMax('computeAverageTokenPrice', token0.address, 2, 1); 231 | await failsMinMax('computeAverageEthPrice', token0.address, 2, 1); 232 | await failsMinMax('computeTwoWayAveragePrices', [token0.address], 2, 1); 233 | await failsMinMax('computeAverageTokenPrices', [token0.address], 2, 1); 234 | await failsMinMax('computeAverageEthPrices', [token0.address], 2, 1); 235 | await failsMinMax('computeAverageEthForTokens(address,uint256,uint256,uint256)', token0.address, 0, 2, 1); 236 | await failsMinMax('computeAverageTokensForEth(address,uint256,uint256,uint256)', token0.address, 0, 2, 1); 237 | await failsMinMax('computeAverageEthForTokens(address[],uint256[],uint256,uint256)', [token0.address], [0], 2, 1); 238 | await failsMinMax('computeAverageTokensForEth(address[],uint256[],uint256,uint256)', [token0.address], [0], 2, 1); 239 | }); 240 | }); 241 | }); 242 | 243 | describe('Price Queries', async () => { 244 | let timestampUpdated; 245 | before(async () => { 246 | ({ 247 | token0, 248 | token1, 249 | pair0, 250 | pair1 251 | } = await testTokensFixture()); 252 | expectedPrice0 = undefined; 253 | expectedPrice1 = undefined; 254 | }); 255 | 256 | it('updatePrice()', async () => { 257 | await fastForwardToNextHour(); 258 | await addLiquidity0(); 259 | await addLiquidity1(); 260 | const timestamp = await getTransactionTimestamp( 261 | oracle.updatePrices([token0.address, token1.address]) 262 | ); 263 | expectedPrice0 = encodePrice(0, 0, timestamp, expectedPrice0); 264 | expectedPrice1 = encodePrice(0, 0, timestamp, expectedPrice1); 265 | timestampUpdated = timestamp; 266 | }); 267 | 268 | it('updatePrice() returns true if token is WETH', async () => { 269 | expect(await oracle.callStatic.updatePrice(weth.address)).to.be.true; 270 | }); 271 | 272 | it('hasPriceObservationInWindow()', async () => { 273 | expect(await oracle.hasPriceObservationInWindow(token0.address, Math.floor(timestampUpdated / 3600))).to.be.true; 274 | expect(await oracle.hasPriceObservationInWindow(token0.address, 0)).to.be.false; 275 | }); 276 | 277 | it('getPriceObservationInWindow()', async () => { 278 | const priceObservation = await oracle.getPriceObservationInWindow(token0.address, Math.floor(timestampUpdated / 3600)); 279 | expect(priceObservation.timestamp).to.eq(timestampUpdated); 280 | expect(priceObservation.priceCumulativeLast.eq(expectedPrice0.tokenPriceCumulativeLast)).to.be.true; 281 | expect(priceObservation.ethPriceCumulativeLast.eq(expectedPrice0.ethPriceCumulativeLast)).to.be.true; 282 | }); 283 | 284 | it('computeTwoWayAveragePrice()', async () => { 285 | await fastForwardToNextHour(); 286 | await fastForward(0.3 * HOUR) 287 | await addLiquidity0(); 288 | await addLiquidity1(); 289 | const timestamp = await getTransactionTimestamp( 290 | oracle.updatePrices([token0.address, token1.address]) 291 | ); 292 | expectedPrice0 = encodePrice(0, 0, +timestamp, expectedPrice0); 293 | expectedPrice1 = encodePrice(0, 0, +timestamp, expectedPrice1); 294 | timestampUpdated = timestamp; 295 | const price0 = await oracle.computeTwoWayAveragePrice(token0.address, 1, 2 * HOUR); 296 | expect(price0.priceAverage.eq(expectedPrice0.tokenPriceAverage)).to.be.true; 297 | expect(price0.ethPriceAverage.eq(expectedPrice0.ethPriceAverage)).to.be.true; 298 | const price1 = await oracle.computeTwoWayAveragePrice(token1.address, 1, 2 * HOUR); 299 | expect(price1.priceAverage.eq(expectedPrice1.tokenPriceAverage)).to.be.true; 300 | expect(price1.ethPriceAverage.eq(expectedPrice1.ethPriceAverage)).to.be.true; 301 | const priceWeth = await oracle.computeTwoWayAveragePrice(weth.address, 1, 2 * HOUR); 302 | expect(priceWeth.priceAverage.eq(BigNumber.from(2).pow(112))).to.be.true 303 | expect(priceWeth.ethPriceAverage.eq(BigNumber.from(2).pow(112))).to.be.true 304 | }); 305 | 306 | it('computeTwoWayAveragePrices()', async () => { 307 | const [price0, price1, priceWeth] = await oracle.computeTwoWayAveragePrices([token0.address, token1.address, weth.address], 1, 2 * HOUR); 308 | expect(price0.priceAverage.eq(expectedPrice0.tokenPriceAverage)).to.be.true; 309 | expect(price0.ethPriceAverage.eq(expectedPrice0.ethPriceAverage)).to.be.true; 310 | expect(price1.priceAverage.eq(expectedPrice1.tokenPriceAverage)).to.be.true; 311 | expect(price1.ethPriceAverage.eq(expectedPrice1.ethPriceAverage)).to.be.true; 312 | expect(priceWeth.priceAverage.eq(BigNumber.from(2).pow(112))).to.be.true 313 | expect(priceWeth.ethPriceAverage.eq(BigNumber.from(2).pow(112))).to.be.true 314 | }); 315 | 316 | it('computeAverageTokenPrice()', async () => { 317 | const price0 = await oracle.computeAverageTokenPrice(token0.address, 1, 2 * HOUR); 318 | expect(price0._x.eq(expectedPrice0.tokenPriceAverage)).to.be.true; 319 | const price1 = await oracle.computeAverageTokenPrice(token1.address, 1, 2 * HOUR); 320 | expect(price1._x.eq(expectedPrice1.tokenPriceAverage)).to.be.true; 321 | const priceWeth = await oracle.computeAverageTokenPrice(weth.address, 1, 2 * HOUR); 322 | expect(priceWeth._x.eq(BigNumber.from(2).pow(112))).to.be.true; 323 | }); 324 | 325 | it('computeAverageTokenPrices()', async () => { 326 | const [price0, price1, priceWeth] = await oracle.computeAverageTokenPrices([token0.address, token1.address, weth.address], 1, 2 * HOUR); 327 | expect(price0._x.eq(expectedPrice0.tokenPriceAverage)).to.be.true; 328 | expect(price1._x.eq(expectedPrice1.tokenPriceAverage)).to.be.true; 329 | expect(priceWeth._x.eq(BigNumber.from(2).pow(112))).to.be.true; 330 | }); 331 | 332 | it('computeAverageEthPrice()', async () => { 333 | const price0 = await oracle.computeAverageEthPrice(token0.address, 1, 2 * HOUR); 334 | expect(price0._x.eq(expectedPrice0.ethPriceAverage)).to.be.true; 335 | const price1 = await oracle.computeAverageEthPrice(token1.address, 1, 2 * HOUR); 336 | expect(price1._x.eq(expectedPrice1.ethPriceAverage)).to.be.true; 337 | const priceWeth = await oracle.computeAverageEthPrice(weth.address, 1, 2 * HOUR); 338 | expect(priceWeth._x.eq(BigNumber.from(2).pow(112))).to.be.true; 339 | }); 340 | 341 | it('computeAverageEthPrices()', async () => { 342 | const [price0, price1, priceWeth] = await oracle.computeAverageEthPrices([token0.address, token1.address, weth.address], 1, 2 * HOUR); 343 | expect(price0._x.eq(expectedPrice0.ethPriceAverage)).to.be.true; 344 | expect(price1._x.eq(expectedPrice1.ethPriceAverage)).to.be.true; 345 | expect(priceWeth._x.eq(BigNumber.from(2).pow(112))).to.be.true; 346 | }); 347 | 348 | it('computeAverageEthForTokens(address,uint256,uint256,uint256)', async () => { 349 | const amountToken = expandTo18Decimals(10); 350 | const expectedValue0 = expectedPrice0.tokenPriceAverage.mul(amountToken).div(BigNumber.from(2).pow(112)); 351 | const expectedValue1 = expectedPrice1.tokenPriceAverage.mul(amountToken).div(BigNumber.from(2).pow(112)); 352 | const tokenValue0 = await oracle['computeAverageEthForTokens(address,uint256,uint256,uint256)']( 353 | token0.address, amountToken, 1, 2 * HOUR 354 | ); 355 | const tokenValue1 = await oracle['computeAverageEthForTokens(address,uint256,uint256,uint256)']( 356 | token1.address, amountToken, 1, 2 * HOUR 357 | ); 358 | const tokenValueWeth = await oracle['computeAverageEthForTokens(address,uint256,uint256,uint256)']( 359 | weth.address, amountToken, 1, 2 * HOUR 360 | ); 361 | expect(tokenValue0.eq(expectedValue0)).to.be.true; 362 | expect(tokenValue1.eq(expectedValue1)).to.be.true; 363 | expect(tokenValueWeth.eq(amountToken)).to.be.true ; 364 | }); 365 | 366 | it('computeAverageEthForTokens(address[],uint256[],uint256,uint256)', async () => { 367 | const amountToken = expandTo18Decimals(10); 368 | const expectedValue0 = expectedPrice0.tokenPriceAverage.mul(amountToken).div(BigNumber.from(2).pow(112)); 369 | const expectedValue1 = expectedPrice1.tokenPriceAverage.mul(amountToken).div(BigNumber.from(2).pow(112)); 370 | const [tokenValue0, tokenValue1, tokenValueWeth] = await oracle['computeAverageEthForTokens(address[],uint256[],uint256,uint256)']( 371 | [token0.address,token1.address, weth.address], 372 | [amountToken, amountToken, amountToken], 373 | 1, 374 | 2 * HOUR 375 | ); 376 | expect(tokenValue0.eq(expectedValue0)).to.be.true; 377 | expect(tokenValue1.eq(expectedValue1)).to.be.true; 378 | expect(tokenValueWeth.eq(amountToken)).to.be.true; 379 | }); 380 | 381 | it('computeAverageTokensForEth(address,uint256,uint256,uint256)', async () => { 382 | const amountWeth = expandTo18Decimals(10); 383 | const expectedValue0 = expectedPrice0.ethPriceAverage.mul(amountWeth).div(BigNumber.from(2).pow(112)); 384 | const expectedValue1 = expectedPrice1.ethPriceAverage.mul(amountWeth).div(BigNumber.from(2).pow(112)); 385 | const ethValue0 = await oracle['computeAverageTokensForEth(address,uint256,uint256,uint256)']( 386 | token0.address, amountWeth, 1, 2 * HOUR 387 | ); 388 | const ethValue1 = await oracle['computeAverageTokensForEth(address,uint256,uint256,uint256)']( 389 | token1.address, amountWeth, 1, 2 * HOUR 390 | ); 391 | const ethValueWeth = await oracle['computeAverageTokensForEth(address,uint256,uint256,uint256)']( 392 | weth.address, amountWeth, 1, 2 * HOUR 393 | ); 394 | expect(ethValue0.eq(expectedValue0)).to.be.true; 395 | expect(ethValue1.eq(expectedValue1)).to.be.true; 396 | expect(ethValueWeth.eq(amountWeth)).to.be.true; 397 | }); 398 | 399 | it('computeAverageTokensForEth(address[],uint256[],uint256,uint256)', async () => { 400 | const amountWeth = expandTo18Decimals(10); 401 | const expectedValue0 = expectedPrice0.ethPriceAverage.mul(amountWeth).div(BigNumber.from(2).pow(112)); 402 | const expectedValue1 = expectedPrice1.ethPriceAverage.mul(amountWeth).div(BigNumber.from(2).pow(112)); 403 | const [ethValue0, ethValue1, ethValueWeth] = await oracle['computeAverageTokensForEth(address[],uint256[],uint256,uint256)']( 404 | [token0.address,token1.address, weth.address], 405 | [amountWeth, amountWeth, amountWeth], 406 | 1, 407 | 2 * HOUR 408 | ); 409 | expect(ethValue0.eq(expectedValue0)).to.be.true; 410 | expect(ethValue1.eq(expectedValue1)).to.be.true; 411 | expect(ethValueWeth.eq(amountWeth)).to.be.true; 412 | }); 413 | 414 | it('All price queries succeed when there is a price between `minTimeElapsed` and `maxTimeElapsed` in the same observation window', async () => { 415 | await fastForward(0.2 * HOUR); 416 | await oracle.computeTwoWayAveragePrice(token0.address, 0, 0.4 * HOUR); 417 | await oracle.computeAverageTokenPrice(token0.address, 0, 0.4 * HOUR); 418 | await oracle.computeAverageEthPrice(token0.address, 0, 0.4 * HOUR); 419 | await oracle.computeTwoWayAveragePrices([token0.address], 0, 0.4 * HOUR); 420 | await oracle.computeAverageTokenPrices([token0.address], 0, 0.4 * HOUR); 421 | await oracle.computeAverageEthPrices([token0.address], 0, 0.4 * HOUR); 422 | await oracle['computeAverageEthForTokens(address,uint256,uint256,uint256)'](token0.address, 0, 0, 0.4 * HOUR); 423 | await oracle['computeAverageTokensForEth(address,uint256,uint256,uint256)'](token0.address, 0, 0, 0.4 * HOUR); 424 | await oracle['computeAverageEthForTokens(address[],uint256[],uint256,uint256)']([token0.address], [0], 0, 0.4 * HOUR); 425 | await oracle['computeAverageTokensForEth(address[],uint256[],uint256,uint256)']([token0.address], [0], 0, 0.4 * HOUR); 426 | }); 427 | 428 | it('All price queries succeed when there is a price between `minTimeElapsed` and `maxTimeElapsed` which is multiple observation windows old', async () => { 429 | await fastForwardToNextHour(); 430 | await addLiquidity0(); 431 | await addLiquidity1(); 432 | const timestamp = await getTransactionTimestamp( 433 | oracle.updatePrices([token0.address, token1.address]) 434 | ); 435 | expectedPrice0 = encodePrice(0, 0, +timestamp, expectedPrice0); 436 | expectedPrice1 = encodePrice(0, 0, +timestamp, expectedPrice1); 437 | timestampUpdated = timestamp; 438 | await fastForward(5 * HOUR); 439 | await oracle.computeTwoWayAveragePrice(token0.address, 2*HOUR, 10 * HOUR); 440 | await oracle.computeAverageTokenPrice(token0.address, 2*HOUR, 10 * HOUR); 441 | await oracle.computeAverageEthPrice(token0.address, 2*HOUR, 10 * HOUR); 442 | await oracle.computeTwoWayAveragePrices([token0.address], 2*HOUR, 10 * HOUR); 443 | await oracle.computeAverageTokenPrices([token0.address], 2*HOUR, 10 * HOUR); 444 | await oracle.computeAverageEthPrices([token0.address], 2*HOUR, 10 * HOUR); 445 | await oracle['computeAverageEthForTokens(address,uint256,uint256,uint256)'](token0.address, 0, 2*HOUR, 10 * HOUR); 446 | await oracle['computeAverageTokensForEth(address,uint256,uint256,uint256)'](token0.address, 0, 2*HOUR, 10 * HOUR); 447 | await oracle['computeAverageEthForTokens(address[],uint256[],uint256,uint256)']([token0.address], [0], 2*HOUR, 10 * HOUR); 448 | await oracle['computeAverageTokensForEth(address[],uint256[],uint256,uint256)']([token0.address], [0], 2*HOUR, 10 * HOUR); 449 | }); 450 | 451 | it('getPriceObservationsInRange()', async () => { 452 | await fastForwardToPeriodStart(HOUR * 256); 453 | const {timestamp} = await ethers.provider.getBlock('latest'); 454 | const timestamps = []; 455 | for (let i = 0; i < 10; i++) { 456 | await fastForwardToNextHour(); 457 | await addLiquidity0(); 458 | await addLiquidity1(); 459 | const txTimestamp = await getTransactionTimestamp( 460 | oracle.updatePrices([token0.address, token1.address]) 461 | ); 462 | timestamps.push(+txTimestamp); 463 | } 464 | const observations = await oracle.getPriceObservationsInRange(token0.address, +timestamp, (+timestamp) + HOUR * 10); 465 | const actualTimestamps = observations.map(obs => +obs.timestamp); 466 | expect(actualTimestamps).to.deep.eq(timestamps); 467 | }); 468 | }); 469 | }); -------------------------------------------------------------------------------- /test/PriceLibrary.spec.js: -------------------------------------------------------------------------------- 1 | const chai = require("chai"); 2 | chai.use(require('chai-as-promised')) 3 | const { expect } = chai; 4 | 5 | const bre = require('@nomiclabs/buidler'); 6 | const { expandTo18Decimals, fastForwardToNextHour, encodePrice, getTransactionTimestamp, fastForward, HOUR } = require('./utils'); 7 | const { testTokensFixture } = require("./tokens-fixture"); 8 | const { BigNumber } = require("ethers"); 9 | 10 | const token0Amount = expandTo18Decimals(5); 11 | const token1Amount = expandTo18Decimals(6); 12 | const wethAmount = expandTo18Decimals(10); 13 | 14 | const overrides = {gasLimit: 999999}; 15 | 16 | const noReservesRegex = () => /UniswapV2OracleLibrary::currentCumulativePrice.: Pair has no reserves\./g 17 | 18 | describe('PriceLibrary', async () => { 19 | let library; 20 | let deployer; 21 | let token0, token1, weth; 22 | let pair0, pair1; 23 | 24 | before(async () => { 25 | ({deployer} = await getNamedAccounts()); 26 | await deployments.fixture('Oracle'); 27 | const [signer] = await ethers.getSigners(); 28 | const TestPriceLibrary = await ethers.getContractFactory('TestPriceLibrary', signer); 29 | weth = await ethers.getContract('weth', signer); 30 | library = await TestPriceLibrary.deploy( 31 | (await deployments.get('UniswapV2Factory')).address, 32 | weth.address 33 | ); 34 | ({token0, token1, pair0, pair1} = await testTokensFixture()); 35 | }); 36 | 37 | let expectedPrice0; 38 | let expectedPrice1; 39 | 40 | async function addLiquidity0() { 41 | await fastForward(60); 42 | await token0.getFreeTokens(pair0.address, token0Amount); 43 | await weth.getFreeTokens(pair0.address, wethAmount); 44 | const timestamp = await getTransactionTimestamp(pair0.mint(deployer, overrides)); 45 | expectedPrice0 = encodePrice(token0Amount, wethAmount, +timestamp, expectedPrice0); 46 | } 47 | 48 | async function addLiquidity1() { 49 | await token1.getFreeTokens(pair1.address, token1Amount); 50 | await weth.getFreeTokens(pair1.address, wethAmount); 51 | const timestamp = await getTransactionTimestamp(pair1.mint(deployer, overrides)); 52 | expectedPrice1 = encodePrice(token1Amount, wethAmount, +timestamp, expectedPrice1); 53 | } 54 | 55 | describe('Before pairs have reserves', async () => { 56 | it('pairInitialized() returns false', async () => { 57 | expect(await library.pairInitialized(token0.address, weth.address)).to.be.false; 58 | expect(await library.pairInitialized(token1.address, weth.address)).to.be.false; 59 | }); 60 | 61 | it('observePrice() reverts', async () => { 62 | await expect(library.observePrice(token0.address, weth.address)).to.be.rejectedWith(noReservesRegex()); 63 | await expect(library.observePrice(token1.address, weth.address)).to.be.rejectedWith(noReservesRegex()); 64 | await expect(library.observePrice(weth.address, token0.address)).to.be.rejectedWith(noReservesRegex()); 65 | await expect(library.observePrice(weth.address, token1.address)).to.be.rejectedWith(noReservesRegex()); 66 | }); 67 | 68 | it('observeTwoWayPrice() reverts', async () => { 69 | await expect(library.observeTwoWayPrice(token0.address, weth.address)).to.be.rejectedWith(noReservesRegex()); 70 | await expect(library.observeTwoWayPrice(token1.address, weth.address)).to.be.rejectedWith(noReservesRegex()); 71 | await expect(library.observeTwoWayPrice(weth.address, token0.address)).to.be.rejectedWith(noReservesRegex()); 72 | await expect(library.observeTwoWayPrice(weth.address, token1.address)).to.be.rejectedWith(noReservesRegex()); 73 | }); 74 | }); 75 | 76 | describe('After pairs have reserves', async () => { 77 | it('pairInitialized() returns true', async () => { 78 | await addLiquidity0(); 79 | await addLiquidity1(); 80 | const timestamp = await bre.run('getTimestamp'); 81 | expectedPrice0 = encodePrice(0, 0, +timestamp, expectedPrice0); 82 | expectedPrice1 = encodePrice(0, 0, +timestamp, expectedPrice1); 83 | expect(await library.pairInitialized(token0.address, weth.address)).to.be.true; 84 | expect(await library.pairInitialized(token1.address, weth.address)).to.be.true; 85 | }); 86 | 87 | it('observePrice() succeeds and returns correct values', async () => { 88 | const tokenPriceObservation0 = await library.observePrice(token0.address, weth.address); 89 | const tokenPriceObservation1 = await library.observePrice(token1.address, weth.address); 90 | const ethPriceObservation0 = await library.observePrice(weth.address, token0.address); 91 | const ethPriceObservation1 = await library.observePrice(weth.address, token1.address); 92 | expect(tokenPriceObservation0.timestamp).to.eq(expectedPrice0.blockTimestamp); 93 | expect(tokenPriceObservation1.timestamp).to.eq(expectedPrice1.blockTimestamp); 94 | expect(ethPriceObservation0.timestamp).to.eq(expectedPrice0.blockTimestamp); 95 | expect(ethPriceObservation1.timestamp).to.eq(expectedPrice1.blockTimestamp); 96 | expect(tokenPriceObservation0.priceCumulativeLast.eq(expectedPrice0.tokenPriceCumulativeLast)).to.be.true; 97 | expect(tokenPriceObservation1.priceCumulativeLast.eq(expectedPrice1.tokenPriceCumulativeLast)).to.be.true; 98 | expect(ethPriceObservation0.priceCumulativeLast.eq(expectedPrice0.ethPriceCumulativeLast)).to.be.true; 99 | expect(ethPriceObservation1.priceCumulativeLast.eq(expectedPrice1.ethPriceCumulativeLast)).to.be.true; 100 | }); 101 | 102 | it('observeTwoWayPrice() succeeds and returns correct values', async () => { 103 | const priceObservation0 = await library.observeTwoWayPrice(token0.address, weth.address); 104 | const priceObservation1 = await library.observeTwoWayPrice(token1.address, weth.address); 105 | expect(priceObservation0.timestamp).to.eq(expectedPrice0.blockTimestamp); 106 | expect(priceObservation1.timestamp).to.eq(expectedPrice1.blockTimestamp); 107 | expect(priceObservation0.priceCumulativeLast.eq(expectedPrice0.tokenPriceCumulativeLast)).to.be.true; 108 | expect(priceObservation1.priceCumulativeLast.eq(expectedPrice1.tokenPriceCumulativeLast)).to.be.true; 109 | expect(priceObservation0.ethPriceCumulativeLast.eq(expectedPrice0.ethPriceCumulativeLast)).to.be.true; 110 | expect(priceObservation1.ethPriceCumulativeLast.eq(expectedPrice1.ethPriceCumulativeLast)).to.be.true; 111 | }); 112 | }); 113 | 114 | describe('Utility functions', async () => { 115 | const copyPrice = ({ 116 | tokenPriceCumulativeLast, 117 | ethPriceCumulativeLast, 118 | blockTimestamp, 119 | tokenPriceAverage, 120 | ethPriceAverage 121 | }) => ({ 122 | observation: { 123 | timestamp: blockTimestamp, 124 | priceCumulativeLast: BigNumber.from(tokenPriceCumulativeLast), 125 | ethPriceCumulativeLast: BigNumber.from(ethPriceCumulativeLast) 126 | }, 127 | twoWayPrice: { 128 | priceAverage: BigNumber.from(tokenPriceAverage || 0), 129 | ethPriceAverage: BigNumber.from(ethPriceAverage || 0), 130 | } 131 | }); 132 | 133 | let oldPrice0, newPrice0; 134 | let oldPrice1, newPrice1; 135 | 136 | before(async () => { 137 | oldPrice0 = copyPrice(expectedPrice0); 138 | oldPrice1 = copyPrice(expectedPrice1); 139 | await addLiquidity0(); 140 | await addLiquidity1(); 141 | newPrice0 = copyPrice(expectedPrice0); 142 | newPrice1 = copyPrice(expectedPrice1); 143 | }); 144 | 145 | it('computeTwoWayAveragePrice() returns correct values', async () => { 146 | const price0 = await library.callStatic.computeTwoWayAveragePrice(oldPrice0.observation, newPrice0.observation); 147 | expect(price0.priceAverage.eq(expectedPrice0.tokenPriceAverage)).to.be.true; 148 | expect(price0.ethPriceAverage.eq(expectedPrice0.ethPriceAverage)).to.be.true; 149 | 150 | const price1 = await library.callStatic.computeTwoWayAveragePrice(oldPrice1.observation, newPrice1.observation); 151 | expect(price1.priceAverage.eq(expectedPrice1.tokenPriceAverage)).to.be.true; 152 | expect(price1.ethPriceAverage.eq(expectedPrice1.ethPriceAverage)).to.be.true; 153 | }); 154 | 155 | it('computeAveragePrice() returns correct values', async () => { 156 | const tokenPrice0 = await library.computeAveragePrice( 157 | oldPrice0.observation.timestamp, 158 | oldPrice0.observation.priceCumulativeLast, 159 | newPrice0.observation.timestamp, 160 | newPrice0.observation.priceCumulativeLast 161 | ); 162 | expect(tokenPrice0._x.eq(expectedPrice0.tokenPriceAverage)).to.be.true; 163 | 164 | const ethPrice0 = await library.computeAveragePrice( 165 | oldPrice0.observation.timestamp, 166 | oldPrice0.observation.ethPriceCumulativeLast, 167 | newPrice0.observation.timestamp, 168 | newPrice0.observation.ethPriceCumulativeLast 169 | ); 170 | expect(ethPrice0._x.eq(expectedPrice0.ethPriceAverage)).to.be.true; 171 | 172 | const tokenPrice1 = await library.computeAveragePrice( 173 | oldPrice1.observation.timestamp, 174 | oldPrice1.observation.priceCumulativeLast, 175 | newPrice1.observation.timestamp, 176 | newPrice1.observation.priceCumulativeLast 177 | ); 178 | expect(tokenPrice1._x.eq(expectedPrice1.tokenPriceAverage)).to.be.true; 179 | 180 | const ethPrice1 = await library.computeAveragePrice( 181 | oldPrice1.observation.timestamp, 182 | oldPrice1.observation.ethPriceCumulativeLast, 183 | newPrice1.observation.timestamp, 184 | newPrice1.observation.ethPriceCumulativeLast 185 | ); 186 | expect(ethPrice1._x.eq(expectedPrice1.ethPriceAverage)).to.be.true; 187 | }); 188 | 189 | it('computeAverageTokenPrice() returns correct values', async () => { 190 | const tokenPrice0 = await library.computeAverageTokenPrice(oldPrice0.observation, newPrice0.observation); 191 | expect(tokenPrice0._x.eq(expectedPrice0.tokenPriceAverage)).to.be.true; 192 | 193 | const tokenPrice1 = await library.computeAverageTokenPrice(oldPrice1.observation, newPrice1.observation); 194 | expect(tokenPrice1._x.eq(expectedPrice1.tokenPriceAverage)).to.be.true; 195 | }); 196 | 197 | it('computeAverageEthPrice() returns correct values', async () => { 198 | const ethPrice0 = await library.computeAverageEthPrice(oldPrice0.observation, newPrice0.observation); 199 | expect(ethPrice0._x.eq(expectedPrice0.ethPriceAverage)).to.be.true; 200 | 201 | const ethPrice1 = await library.computeAverageEthPrice(oldPrice1.observation, newPrice1.observation); 202 | expect(ethPrice1._x.eq(expectedPrice1.ethPriceAverage)).to.be.true; 203 | }); 204 | 205 | it('computeAverageEthForTokens() returns correct values', async () => { 206 | const tokenAmount = expandTo18Decimals(100); 207 | const expectedValue0 = expectedPrice0.tokenPriceAverage.mul(tokenAmount).div(BigNumber.from(2).pow(112)); 208 | const expectedValue1 = expectedPrice1.tokenPriceAverage.mul(tokenAmount).div(BigNumber.from(2).pow(112)); 209 | const tokenValue0 = await library.computeAverageEthForTokens(newPrice0.twoWayPrice, tokenAmount); 210 | const tokenValue1 = await library.computeAverageEthForTokens(newPrice1.twoWayPrice, tokenAmount); 211 | expect(tokenValue0.eq(expectedValue0)).to.be.true; 212 | expect(tokenValue1.eq(expectedValue1)).to.be.true; 213 | }); 214 | 215 | it('computeAverageTokensForEth() returns correct values', async () => { 216 | const wethAmount = expandTo18Decimals(100); 217 | const expectedValue0 = expectedPrice0.ethPriceAverage.mul(wethAmount).div(BigNumber.from(2).pow(112)); 218 | const expectedValue1 = expectedPrice1.ethPriceAverage.mul(wethAmount).div(BigNumber.from(2).pow(112)); 219 | const ethValue0 = await library.computeAverageTokensForEth(newPrice0.twoWayPrice, wethAmount); 220 | const ethValue1 = await library.computeAverageTokensForEth(newPrice1.twoWayPrice, wethAmount); 221 | expect(ethValue0.eq(expectedValue0)).to.be.true; 222 | expect(ethValue1.eq(expectedValue1)).to.be.true; 223 | }); 224 | }); 225 | }); -------------------------------------------------------------------------------- /test/TestErrorTriggers.spec.js: -------------------------------------------------------------------------------- 1 | const chai = require("chai"); 2 | chai.use(require('chai-as-promised')) 3 | const { expect } = chai; 4 | 5 | describe('TestErrorTriggers', async () => { 6 | let test; 7 | before(async () => { 8 | const TestErrorTriggers = await ethers .getContractFactory('TestErrorTriggers'); 9 | test = await TestErrorTriggers.deploy(); 10 | }); 11 | 12 | it('UniswapV2Library.sortTokens() fails if the tokens are the same', async () => { 13 | await expect(test.triggerUniswapLibrarySameTokenError()).to.be.rejectedWith(/UniswapV2Library: IDENTICAL_ADDRESSES/g); 14 | }); 15 | 16 | it('UniswapV2Library.sortTokens() fails if null address is given', async () => { 17 | await expect(test.triggerUniswapLibraryNullTokenError()).to.be.rejectedWith(/UniswapV2Library: ZERO_ADDRESS/g); 18 | }); 19 | 20 | it('Bits.highestBitSet() fails if zero is given', async () => { 21 | await expect(test.triggerHighestBitError()).to.be.rejectedWith(/Bits::highestBitSet: Value 0 has no bits set/g); 22 | }); 23 | 24 | it('Bits.lowestBitSet() fails if zero is given', async () => { 25 | await expect(test.triggerLowestBitError()).to.be.rejectedWith(/Bits::lowestBitSet: Value 0 has no bits set/g); 26 | }); 27 | }); -------------------------------------------------------------------------------- /test/compare-oracles.js: -------------------------------------------------------------------------------- 1 | const chai = require("chai"); 2 | chai.use(require('chai-as-promised')) 3 | const { expect } = chai; 4 | 5 | const chalk = require('chalk'); 6 | const Table = require('cli-table3'); 7 | const bre = require('@nomiclabs/buidler'); 8 | const { oneToken, toBN, toHex } = require('../lib/bn'); 9 | const testTokens = require('../test/test-data/test-tokens.json'); 10 | const { fastForwardToPeriodStart } = require("./utils"); 11 | 12 | const HOUR = 3600; 13 | const DAY = 86400; 14 | const WEEK = 604800; 15 | return; 16 | describe('Compare Oracles', async () => { 17 | let weeklyTWAP, hourlyTWAP, indexedTWAP; 18 | let tokens = []; 19 | let addresses = []; 20 | let table; 21 | 22 | before(async () => { 23 | const [signer] = await ethers.getSigners(); 24 | await deployments.fixture('Tokens'); 25 | hourlyTWAP = await ethers.getContract('HourlyTWAPUniswapV2Oracle', signer); 26 | weeklyTWAP = await ethers.getContract('WeeklyTWAPUniswapV2Oracle', signer); 27 | indexedTWAP = await ethers.getContract('IndexedOracle', signer); 28 | for (let token of testTokens) { 29 | const { symbol } = token; 30 | const erc20 = await ethers.getContractAt( 31 | 'MockERC20', 32 | (await deployments.getOrNull(symbol.toLowerCase())).address, 33 | signer 34 | ); 35 | tokens.push({...token, erc20 }); 36 | addresses.push(erc20.address); 37 | } 38 | table = new Table({ head: ['Scenario', 'Net Savings/Loss'] }); 39 | }); 40 | 41 | after(() => { 42 | console.log(table.toString()); 43 | }); 44 | 45 | async function addLiquidityAll() { 46 | for (let token of tokens) { 47 | const { marketcap, price, symbol } = token; 48 | let amountWeth = toBN(marketcap).divn(10); 49 | let amountToken = amountWeth.divn(price); 50 | await bre.run('add-liquidity', { 51 | symbol, 52 | amountToken: toHex(amountToken.mul(oneToken)), 53 | amountWeth: toHex(amountWeth.mul(oneToken)) 54 | }); 55 | } 56 | } 57 | 58 | function pushGasTable(title, oldPrice, newPrice) { 59 | // const table = new Table({ head: [title] }); 60 | let _diff = (+oldPrice) - (+newPrice); 61 | let diff; 62 | if (_diff > 0) { 63 | diff = chalk.green(_diff); 64 | } else { 65 | diff = chalk.red(_diff); 66 | } 67 | table.push([title, diff]); 68 | } 69 | 70 | describe('Hourly TWAP - computeTwoWayAveragePrices', async () => { 71 | it('Price is in the same observation window', async () => { 72 | await fastForwardToPeriodStart(HOUR); 73 | await addLiquidityAll(); 74 | await bre.run('update-prices', { tokens: addresses }); 75 | await bre.run('increaseTime', { hours: 0.6 }); 76 | await addLiquidityAll(); 77 | const oldPrice = await hourlyTWAP.estimateGas.computeTwoWayAveragePrices(addresses); 78 | const newPrice = await indexedTWAP.estimateGas.computeTwoWayAveragePrices(addresses, HOUR * 0.5, HOUR * 2); 79 | pushGasTable( 80 | 'Price in same observation window | Hourly TWAP', 81 | oldPrice, 82 | newPrice 83 | ); 84 | }); 85 | 86 | it('Price is in the previous observation window', async () => { 87 | await fastForwardToPeriodStart(HOUR); 88 | await addLiquidityAll(); 89 | const oldTimestamp = await bre.run('getTimestamp'); 90 | console.log(`Seconds since hour start: ${oldTimestamp % HOUR}`); 91 | await bre.run('update-prices', { tokens: addresses }); 92 | await fastForwardToPeriodStart(HOUR); 93 | await addLiquidityAll(); 94 | const newTimestamp = await bre.run('getTimestamp'); 95 | console.log(`Seconds since hour start: ${newTimestamp % HOUR}`); 96 | const oldPrice = await hourlyTWAP.estimateGas.computeTwoWayAveragePrices(addresses); 97 | const newPrice = await indexedTWAP.estimateGas.computeTwoWayAveragePrices(addresses, HOUR * 0.5, HOUR * 2); 98 | pushGasTable( 99 | 'Price in previous observation window | Hourly TWAP', 100 | oldPrice, 101 | newPrice 102 | ); 103 | }); 104 | 105 | it('Price is 2 observation periods old', async () => { 106 | await fastForwardToPeriodStart(HOUR); 107 | await bre.run('increaseTime', { hours: 0.8 }); 108 | await addLiquidityAll(); 109 | await bre.run('update-prices', { tokens: addresses }); 110 | // await fastForwardToPeriodStart(HOUR); 111 | await bre.run('increaseTime', { hours: 1.5 }); 112 | await addLiquidityAll(); 113 | const oldPrice = await hourlyTWAP.estimateGas.computeTwoWayAveragePrices(addresses); 114 | const newPrice = await indexedTWAP.estimateGas.computeTwoWayAveragePrices(addresses, HOUR * 0.5, HOUR * 2); 115 | pushGasTable( 116 | 'Price is 2 observation periods old | Hourly TWAP', 117 | oldPrice, 118 | newPrice 119 | ); 120 | }); 121 | 122 | it('Price is 6 observation periods old', async () => { 123 | await fastForwardToPeriodStart(HOUR); 124 | await addLiquidityAll(); 125 | await bre.run('update-prices', { tokens: addresses }); 126 | await bre.run('increaseTime', { hours: 5 }); 127 | await addLiquidityAll(); 128 | await hourlyTWAP.setMaximumObservationAge(HOUR * 6).then(tx => tx.wait()); 129 | const oldPrice = await hourlyTWAP.estimateGas.computeTwoWayAveragePrices(addresses); 130 | const newPrice = await indexedTWAP.estimateGas.computeTwoWayAveragePrices(addresses, HOUR * 0.5, HOUR * 6); 131 | pushGasTable( 132 | 'Price is 6 observation periods old | Hourly TWAP', 133 | oldPrice, 134 | newPrice 135 | ); 136 | }); 137 | }); 138 | 139 | describe('Weekly TWAP - computeTwoWayAveragePrices', async () => { 140 | it('Price is in the same observation window', async () => { 141 | await fastForwardToPeriodStart(WEEK); 142 | await addLiquidityAll(); 143 | await bre.run('update-prices', { tokens: addresses }); 144 | await bre.run('increaseTime', { days: 3.5 }); 145 | await addLiquidityAll(); 146 | const oldPrice = await weeklyTWAP.estimateGas.computeTwoWayAveragePrices(addresses); 147 | const newPrice = await indexedTWAP.estimateGas.computeTwoWayAveragePrices(addresses, WEEK * 0.5, WEEK * 2); 148 | pushGasTable( 149 | 'Price in same observation window | Weekly TWAP', 150 | oldPrice, 151 | newPrice 152 | ); 153 | }); 154 | 155 | it('Price is in the previous observation window', async () => { 156 | await fastForwardToPeriodStart(WEEK); 157 | await addLiquidityAll(); 158 | await bre.run('update-prices', { tokens: addresses }); 159 | const diff = await fastForwardToPeriodStart(WEEK); 160 | console.log(diff) 161 | await addLiquidityAll(); 162 | const oldPrice = await weeklyTWAP.estimateGas.computeTwoWayAveragePrices(addresses); 163 | const newPrice = await indexedTWAP.estimateGas.computeTwoWayAveragePrices(addresses, WEEK * 0.5, WEEK * 2); 164 | const oldValue = await weeklyTWAP.getLastPriceObservation(addresses[0]); 165 | const newValue = await indexedTWAP.getLastPriceObservation(addresses[0], WEEK * 0.5, WEEK * 2); 166 | console.log(oldValue.timestamp); 167 | console.log(newValue.timestamp); 168 | pushGasTable( 169 | 'Price in previous observation window | Weekly TWAP', 170 | oldPrice, 171 | newPrice 172 | ); 173 | }); 174 | 175 | it('Price is 2 observation periods old', async () => { 176 | await fastForwardToPeriodStart(WEEK); 177 | await bre.run('increaseTime', { days: 6 }); 178 | await addLiquidityAll(); 179 | await bre.run('update-prices', { tokens: addresses }); 180 | await fastForwardToPeriodStart(WEEK); 181 | await bre.run('increaseTime', { days: 7 }); 182 | await addLiquidityAll(); 183 | const oldPrice = await weeklyTWAP.estimateGas.computeTwoWayAveragePrices(addresses); 184 | const newPrice = await indexedTWAP.estimateGas.computeTwoWayAveragePrices(addresses, WEEK * 0.5, WEEK * 2); 185 | 186 | pushGasTable( 187 | 'Price is 2 observation periods old | Weekly TWAP', 188 | oldPrice, 189 | newPrice 190 | ); 191 | }); 192 | }); 193 | }); -------------------------------------------------------------------------------- /test/test-data/test-tokens.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Balancer", 4 | "symbol": "BAL", 5 | "price": 0.0427069, 6 | "marketcap": 348923 7 | }, 8 | { 9 | "name": "Basic Attention Token", 10 | "symbol": "BAT", 11 | "price": 0.000627697, 12 | "marketcap": 1000000 13 | }, 14 | { 15 | "name": "Chainlink", 16 | "symbol": "LINK", 17 | "price": 0.0273435, 18 | "marketcap": 10227710 19 | }, 20 | { 21 | "name": "Compound", 22 | "symbol": "COMP", 23 | "price": 0.315988, 24 | "marketcap": 1038912 25 | }, 26 | { 27 | "name": "Curve DAO Token", 28 | "symbol": "CRV", 29 | "price": 0.00155092, 30 | "marketcap": 122915 31 | }, 32 | { 33 | "name": "Kyber Network", 34 | "symbol": "KNC", 35 | "price": 0.0026834, 36 | "marketcap": 528258 37 | }, 38 | { 39 | "name": "Maker", 40 | "symbol": "MKR", 41 | "price": 1.50254, 42 | "marketcap": 1361065 43 | }, 44 | { 45 | "name": "Uniswap", 46 | "symbol": "UNI", 47 | "price": 0.00942854, 48 | "marketcap": 1703946 49 | }, 50 | { 51 | "name": "Wrapped Bitcoin", 52 | "symbol": "WBTC", 53 | "price": 31.2332, 54 | "marketcap": 2944571 55 | }, 56 | { 57 | "name": "yearn.finance", 58 | "symbol": "YFI", 59 | "price": 45.2714, 60 | "marketcap": 1367010 61 | } 62 | ] -------------------------------------------------------------------------------- /test/tokens-fixture.js: -------------------------------------------------------------------------------- 1 | const { BigNumber } = require("ethers"); 2 | 3 | let i = 0; 4 | 5 | const testTokensFixture = deployments.createFixture(async ({ 6 | deployments, 7 | getNamedAccounts, 8 | ethers 9 | }) => { 10 | const [signer] = await ethers.getSigners(); 11 | const weth = await ethers.getContract('weth', signer); 12 | const MockERC20 = await ethers.getContractFactory('MockERC20'); 13 | let token0 = await MockERC20.deploy(`Token${i++}`, `Token${i}`); 14 | let token1 = await MockERC20.deploy(`Token${i++}`, `Token${i}`); 15 | let wethBN = BigNumber.from(weth.address); 16 | let token0BN = BigNumber.from(token0.address); 17 | let token1BN = BigNumber.from(token1.address); 18 | // Coverage of case in observeTwoWayPrice where token is greater than weth 19 | while (token0BN.gt(wethBN)) { 20 | token0 = await MockERC20.deploy(`Token${i++}`, `Token${i}`); 21 | token0BN = BigNumber.from(token0.address); 22 | } 23 | // Coverage of case in observeTwoWayPrice where token is greater than weth 24 | while (wethBN.gt(token1BN)) { 25 | token1 = await MockERC20.deploy(`Token${i++}`, `Token${i}`); 26 | token1BN = BigNumber.from(token1.address); 27 | } 28 | let pair0, pair1; 29 | const uniswapFactory = await ethers.getContract('UniswapV2Factory', signer); 30 | 31 | await uniswapFactory.createPair(token0.address, weth.address).then(tx => tx.wait()).then(async ({ events }) => { 32 | const { args: { pair: pairAddress } } = events.filter(e => e.event == 'PairCreated')[0]; 33 | pair0 = await ethers.getContractAt('IUniswapV2Pair', pairAddress, signer); 34 | }); 35 | 36 | await uniswapFactory.createPair(token1.address, weth.address).then(tx => tx.wait()).then(async ({ events }) => { 37 | const { args: { pair: pairAddress } } = events.filter(e => e.event == 'PairCreated')[0]; 38 | pair1 = await ethers.getContractAt('IUniswapV2Pair', pairAddress, signer); 39 | }); 40 | 41 | return { 42 | token0, 43 | token1, 44 | pair0, 45 | pair1 46 | }; 47 | }); 48 | 49 | module.exports = {testTokensFixture}; -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | const { BigNumber } = require('ethers'); 2 | const { formatEther } = require('ethers/lib/utils'); 3 | 4 | const bre = require("@nomiclabs/buidler"); 5 | 6 | const HOUR = 3600; 7 | const DAY = 86400; 8 | const WEEK = 604800; 9 | 10 | async function mineBlock(timestamp) { 11 | return bre.ethers.provider.send('evm_mine', timestamp ? [timestamp] : []) 12 | } 13 | 14 | async function fastForward(seconds) { 15 | await bre.ethers.provider.send('evm_increaseTime', [seconds]); 16 | await mineBlock(); 17 | } 18 | 19 | async function fastForwardToPeriodStart(observationPeriod) { 20 | const timestamp = await bre.run('getTimestamp'); 21 | const seconds = observationPeriod - ((+timestamp) % observationPeriod); 22 | await fastForward(seconds); 23 | } 24 | 25 | async function fastForwardToNextHour() { 26 | await fastForwardToPeriodStart(HOUR); 27 | } 28 | 29 | function expandTo18Decimals(n) { 30 | return BigNumber.from(n).mul(BigNumber.from(10).pow(18)) 31 | } 32 | 33 | function from18Decimals(n) { 34 | return formatEther(n); 35 | } 36 | 37 | function encodePrice(_tokenReserves, _wethReserves, _blockTimestamp, lastPrice = {}) { 38 | const blockTimestamp = _blockTimestamp % (2**32); 39 | const timeElapsed = blockTimestamp - (lastPrice.blockTimestamp || 0); 40 | let tokenPriceAverage = lastPrice.tokenPriceAverage; 41 | let ethPriceAverage = lastPrice.ethPriceAverage; 42 | let tokenPriceCumulativeLast = BigNumber.from(0) 43 | let ethPriceCumulativeLast = BigNumber.from(0); 44 | if (timeElapsed > 0 && lastPrice.tokenReserves && lastPrice.wethReserves) { 45 | const { tokenReserves, wethReserves } = lastPrice; 46 | tokenPriceAverage = wethReserves.mul(BigNumber.from(2).pow(112)).div(tokenReserves); 47 | ethPriceAverage = tokenReserves.mul(BigNumber.from(2).pow(112)).div(wethReserves); 48 | tokenPriceCumulativeLast = lastPrice.tokenPriceCumulativeLast.add( 49 | tokenPriceAverage.mul(timeElapsed) 50 | ); 51 | ethPriceCumulativeLast = lastPrice.ethPriceCumulativeLast.add( 52 | ethPriceAverage.mul(timeElapsed) 53 | ); 54 | } 55 | const tokenReserves = BigNumber.from(lastPrice.tokenReserves || 0).add(_tokenReserves); 56 | const wethReserves = BigNumber.from(lastPrice.wethReserves || 0).add(_wethReserves); 57 | return { 58 | tokenReserves, 59 | wethReserves, 60 | tokenPriceAverage, 61 | ethPriceAverage, 62 | blockTimestamp, 63 | tokenPriceCumulativeLast, 64 | ethPriceCumulativeLast 65 | }; 66 | } 67 | 68 | async function getTransactionTimestamp(_tx) { 69 | const tx = await Promise.resolve(_tx) 70 | const receipt = await tx.wait(); 71 | const { timestamp } = await ethers.provider.getBlock(receipt.blockNumber); 72 | return timestamp; 73 | } 74 | 75 | module.exports = { 76 | HOUR, 77 | DAY, 78 | WEEK, 79 | expandTo18Decimals, 80 | from18Decimals, 81 | fastForward, 82 | fastForwardToNextHour, 83 | fastForwardToPeriodStart, 84 | encodePrice, 85 | getTransactionTimestamp 86 | } --------------------------------------------------------------------------------