├── .github └── workflows │ ├── publish-npm-package.yml │ └── testing.yml ├── .gitignore ├── .gitmodules ├── .husky └── .gitignore ├── .prettierrc.yaml ├── .solcover.js ├── .solhint.json ├── .solhintignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── buidler.config.ts ├── contracts ├── BandPriceFeed.sol ├── ChainlinkPriceFeed.sol ├── ChainlinkPriceFeedV1R1.sol ├── ChainlinkPriceFeedV2.sol ├── ChainlinkPriceFeedV3.sol ├── PriceFeedDispatcher.sol ├── PriceFeedUpdater.sol ├── UniswapV3PriceFeed.sol ├── base │ └── BlockContext.sol ├── interface │ ├── IChainlinkPriceFeed.sol │ ├── IChainlinkPriceFeedR1.sol │ ├── IChainlinkPriceFeedV3.sol │ ├── IPriceFeed.sol │ ├── IPriceFeedDispatcher.sol │ ├── IPriceFeedUpdate.sol │ ├── IPriceFeedV2.sol │ ├── IUniswapV3PriceFeed.sol │ └── bandProtocol │ │ └── IStdReference.sol ├── test │ ├── TestAggregatorV3.sol │ ├── TestPriceFeedV2.sol │ └── TestStdReference.sol └── twap │ ├── CachedTwap.sol │ └── CumulativeTwap.sol ├── foundry.toml ├── hardhat.config.ts ├── package-lock.json ├── package.json ├── remappings.txt ├── scripts ├── flatten.ts ├── helper.ts ├── path.ts └── slither.ts ├── test ├── BandPriceFeed.spec.ts ├── CachedTwap.spec.ts ├── ChainlinkPriceFeed.spec.ts ├── ChainlinkPriceFeedV1R1.spec.ts ├── ChainlinkPriceFeedV2.spec.ts ├── PriceFeed.gas.test.ts ├── PriceFeedUpdater.spec.ts ├── UniswapV3PriceFeed.spec.ts ├── foundry │ ├── CachedTwap.t.sol │ ├── ChainlinkPriceFeedV3.t.sol │ ├── CumulativeTwap.t.sol │ ├── PriceFeedDispatcher.t.sol │ └── Setup.sol └── shared │ └── chainlink.ts └── tsconfig.json /.github/workflows/publish-npm-package.yml: -------------------------------------------------------------------------------- 1 | name: Publish NPM package 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | 7 | # we will deploy contracts to staging on local first, then git push a tag to trigger this workflow 8 | # so the "Deploy contract" task actually won't change anything 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | with: 15 | persist-credentials: false 16 | 17 | # fix SSH error: git@github.com: Permission denied (publickey). 18 | # copy from https://github.com/actions/setup-node/issues/214#issuecomment-810829250 19 | - name: Reconfigure git to use HTTP authentication 20 | run: > 21 | git config --global url."https://github.com/".insteadOf 22 | ssh://git@github.com/ 23 | 24 | - name: Get npm cache directory 25 | id: npm-cache 26 | run: | 27 | echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT 28 | - uses: actions/cache@v2 29 | with: 30 | path: | 31 | ${{ steps.npm-cache.outputs.dir }} 32 | **/node_modules 33 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 34 | restore-keys: | 35 | ${{ runner.os }}-node- 36 | 37 | - name: Use Node.js 16.x 38 | uses: actions/setup-node@v1 39 | with: 40 | node-version: 16.x 41 | registry-url: 'https://registry.npmjs.org' 42 | 43 | - name: Install contract dependencies 44 | run: npm ci 45 | env: 46 | CI: true 47 | 48 | - name: Build contract package 49 | run: npm run build 50 | env: 51 | CI: true 52 | 53 | - name: Publish npm package 54 | run: | 55 | npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN} 56 | npm publish --access public 57 | env: 58 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 59 | 60 | - name: Discord notification 61 | env: 62 | DISCORD_WEBHOOK: ${{ secrets.DISCORD_RELEASE_WEBHOOK }} 63 | uses: Ilshidur/action-discord@master 64 | with: 65 | args: "npm package @perp/perp-oracle-contract ${{github.ref}} released" 66 | -------------------------------------------------------------------------------- /.github/workflows/testing.yml: -------------------------------------------------------------------------------- 1 | name: Testing 2 | 3 | on: [push] 4 | 5 | jobs: 6 | contract-test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | with: 11 | persist-credentials: false 12 | 13 | # fix SSH error: git@github.com: Permission denied (publickey). 14 | # copy from https://github.com/actions/setup-node/issues/214#issuecomment-810829250 15 | - name: Reconfigure git to use HTTP authentication 16 | run: > 17 | git config --global url."https://github.com/".insteadOf 18 | ssh://git@github.com/ 19 | 20 | - name: Get npm cache directory 21 | id: npm-cache 22 | run: | 23 | echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT 24 | - uses: actions/cache@v2 25 | with: 26 | path: | 27 | ${{ steps.npm-cache.outputs.dir }} 28 | **/node_modules 29 | key: ${{ github.job }}-${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 30 | restore-keys: | 31 | ${{ github.job }}-${{ runner.os }}-node- 32 | 33 | - name: Use Node.js 16.x 34 | uses: actions/setup-node@v1 35 | with: 36 | node-version: 16.x 37 | 38 | - name: Install contract package dependencies 39 | run: npm ci 40 | 41 | - name: Build contract package 42 | run: npm run build 43 | 44 | - name: Run contract tests 45 | run: npm run test 46 | env: 47 | # to solve problem of memory leak https://stackoverflow.com/a/59572966 48 | NODE_OPTIONS: "--max-old-space-size=4096" 49 | 50 | 51 | foundry-test: 52 | name: Foundry Test 53 | runs-on: ubuntu-latest 54 | steps: 55 | - uses: actions/checkout@v2 56 | with: 57 | persist-credentials: false 58 | 59 | - name: Get npm cache directory 60 | id: npm-cache 61 | run: | 62 | echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT 63 | - uses: actions/cache@v2 64 | with: 65 | path: | 66 | ${{ steps.npm-cache.outputs.dir }} 67 | **/node_modules 68 | key: ${{ github.job }}-${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 69 | restore-keys: | 70 | ${{ github.job }}-${{ runner.os }}-node- 71 | - name: Use Node.js 16.x 72 | uses: actions/setup-node@v3 73 | with: 74 | node-version: 16.x 75 | 76 | - name: Install contract package dependencies 77 | run: npm ci 78 | 79 | - name: Install Foundry 80 | uses: foundry-rs/foundry-toolchain@v1 81 | with: 82 | version: nightly 83 | 84 | - name: Run tests 85 | run: npm run foundry-test 86 | 87 | lint: 88 | runs-on: ubuntu-latest 89 | steps: 90 | - uses: actions/checkout@v2 91 | with: 92 | persist-credentials: false 93 | 94 | # fix SSH error: git@github.com: Permission denied (publickey). 95 | # copy from https://github.com/actions/setup-node/issues/214#issuecomment-810829250 96 | - name: Reconfigure git to use HTTP authentication 97 | run: > 98 | git config --global url."https://github.com/".insteadOf 99 | ssh://git@github.com/ 100 | 101 | - name: Get npm cache directory 102 | id: npm-cache 103 | run: | 104 | echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT 105 | - uses: actions/cache@v2 106 | with: 107 | path: | 108 | ${{ steps.npm-cache.outputs.dir }} 109 | **/node_modules 110 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 111 | restore-keys: | 112 | ${{ runner.os }}-node- 113 | 114 | - name: Use Node.js 16.x 115 | uses: actions/setup-node@v1 116 | with: 117 | node-version: 16.x 118 | 119 | - name: Install dependencies 120 | run: npm ci 121 | 122 | - name: Run lint 123 | run: npm run lint 124 | 125 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | artifacts 2 | cache 3 | node_modules 4 | coverage.json 5 | coverage 6 | typechain 7 | flattened 8 | slither 9 | /.DS_Store 10 | .idea 11 | contracts/hardhat-dependency-compiler/* 12 | forge-cache/ 13 | out/ 14 | coverage-out/ 15 | lcov.info -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | semi: false 2 | printWidth: 120 3 | singleQuote: false 4 | trailingComma: all 5 | arrowParens: avoid 6 | tabWidth: 4 7 | bracketSpacing: true 8 | overrides: 9 | - files: "*.json" 10 | options: 11 | tabWidth: 2 -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | skipFiles: ["interface/ICachedPriceFeed.sol", "test/TestAggregatorV3.sol", "test/TestStdReference.sol"], 3 | } 4 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier"], 3 | "extends": "solhint:recommended", 4 | "rules": { 5 | "prettier/prettier": "error", 6 | "max-line-length": "error", 7 | "compiler-version": "off", 8 | "ordering": "warn", 9 | "reason-string": ["warn", { "maxLength": 48 }], 10 | "func-name-mixedcase": "off", 11 | "private-vars-leading-underscore": "warn", 12 | "func-visibility": ["warn", { "ignoreConstructors": true }] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.solhintignore: -------------------------------------------------------------------------------- 1 | contracts/test/**/*.sol -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": false, 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "editor.tabSize": 4, 5 | "[json]": { 6 | "editor.formatOnSave": true, 7 | "editor.defaultFormatter": "esbenp.prettier-vscode" 8 | }, 9 | "[javascript]": { 10 | "editor.formatOnSave": true, 11 | "editor.codeActionsOnSave": { 12 | "source.organizeImports": true 13 | } 14 | }, 15 | "[typescript]": { 16 | "editor.formatOnSave": true, 17 | "editor.codeActionsOnSave": { 18 | "source.organizeImports": true 19 | } 20 | }, 21 | "[typescriptreact]": { 22 | "editor.formatOnSave": true, 23 | "editor.codeActionsOnSave": { 24 | "source.organizeImports": true 25 | } 26 | }, 27 | "[solidity]": { 28 | "editor.formatOnSave": true, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [unreleased] 9 | 10 | ## [0.6.8] - 2024-12-25 11 | - check `startedAt > 0` in `ChainlinkPriceFeedV1R1.getPrice` 12 | 13 | ## [0.6.7] - 2023-05-05 14 | - Add `IPriceFeedDispatcher.decimals` back for backward compatible. 15 | 16 | ## [0.6.6] - 2023-05-05 17 | - Add `PriceFeedDispatcher.getPrice` for backward compatible. 18 | 19 | ## [0.6.5] - 2023-03-31 20 | - Refine natspec 21 | 22 | ## [0.6.4] - 2023-03-30 23 | - Rename `ChainlinkPriceFeedV3.getCachedTwap` to `ChainlinkPriceFeedV3.getPrice`. 24 | - Rename `ChainlinkPriceFeedV3.getCachedPrice` to `ChainlinkPriceFeedV3.getLatestOrCachedPrice`. 25 | 26 | ## [0.6.3] - 2023-03-14 27 | - Add `ChainlinkPriceFeedV3.getCachePrice` to fetch the latest valid price and updated timestamp. 28 | - Add `ChainlinkPriceFeedV3.getTimeout` to get timeout config of ChainlinkPriceFeedV3. 29 | 30 | ## [0.6.2] - 2023-03-01 31 | - `observations` extends to `1800` at `CumulativeTwap.sol` to support extreme circumstance. 32 | - To better enhance above performance, we introduce binary search mimicked from https://github.com/Uniswap/v3-core/blob/05c10bf/contracts/libraries/Oracle.sol#L153. 33 | - Remove `CT_NEH` from `CumulativeTwap.sol`. Won't be calculated if so. Simply return latest price at `CachedTwap.sol`. 34 | - Fix imprecise TWAP calculation when historical data is not enough at `CumulativeTwap.sol`. Won't be calculated if so. 35 | 36 | ## [0.6.1] - 2023-03-01 37 | - Fix cachedTwap won't be updated when latest updated timestamp not changed 38 | 39 | ## [0.6.0] - 2023-03-01 40 | ### Added 41 | - Add `ChainlinkPriceFeedV3.sol` with better error handling when Chainlink is broken. 42 | - Add `PriceFeedDispatcher.sol`, a proxy layer to fetch Chainlink or Uniswap's price. 43 | - Add `UniswapV3PriceFeed.sol` to fetch a market TWAP with a hard coded time period. 44 | - Update `CachedTwap.sol` and `CumulativeTwap.sol` to better support above fallbackable oracle 45 | 46 | ## [0.5.1] - 2023-02-07 47 | 48 | - Add `ChainlinkPriceFeedV1R1` 49 | 50 | ## [0.5.0] - 2022-08-23 51 | 52 | - Add `PriceFeedUpdater` 53 | 54 | ## [0.4.2] - 2022-06-08 55 | 56 | - Add `ChainlinkPriceFeed.getRoundData()` 57 | 58 | ## [0.4.1] - 2022-05-24 59 | 60 | - Fix `npm pack` 61 | 62 | ## [0.4.0] - 2022-05-24 63 | 64 | - Add `ChainlinkPriceFeedV2`, which calculates the TWAP by cached TWAP 65 | - Add the origin `ChainlinkPriceFeed` back, which calculates the TWAP by round data instead of cached TWAP 66 | 67 | ## [0.3.4] - 2022-04-01 68 | 69 | - Add `cacheTwap(uint256)` to `IPriceFeed.sol` and `EmergencyPriceFeed.sol` 70 | - Remove `ICachedTwap.sol` 71 | 72 | ## [0.3.3] - 2022-03-18 73 | 74 | - Refactor `ChainlinkPriceFeed`, `BandPriceFeed`, and `EmergencyPriceFeed` with efficient TWAP calculation. 75 | - Change the license to `GPL-3.0-or-later`. 76 | 77 | ## [0.3.2] - 2022-03-04 78 | 79 | - Using cumulative twap in Chainlink price feed 80 | 81 | ## [0.3.0] - 2022-02-07 82 | 83 | - Add `EmergencyPriceFeed` 84 | 85 | ## [0.2.2] - 2021-12-28 86 | 87 | - `BandPriceFeed` will revert if price not updated yet 88 | 89 | ## [0.2.1] - 2021-12-24 90 | 91 | - Fix `BandPriceFeed` when the price feed haven't updated 92 | 93 | ## [0.2.0] - 2021-12-21 94 | 95 | - Add `BandPriceFeed` 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![image](https://user-images.githubusercontent.com/105896/160323275-78ec4b03-fd99-44da-83c5-241708baf9d7.png) 2 | 3 | 4 | # perp-oracle-contract 5 | 6 | [![@perp/perp-oracle-contract on npm](https://img.shields.io/npm/v/@perp/perp-oracle-contract?style=flat-square)](https://www.npmjs.com/package/@perp/perp-oracle-contract) 7 | 8 | This repository contains the oracle smart contracts for [Perpetual Protocol Curie (v2)](https://perp.com/). For core contracts, see [perp-curie-contract](https://github.com/perpetual-protocol/perp-curie-contract). 9 | 10 | Contract source code is also published as npm package: 11 | 12 | - [@perp/perp-oracle-contract](https://www.npmjs.com/package/@perp/perp-oracle-contract) 13 | 14 | ## Local Development 15 | 16 | You need Node.js 16+ to build. Use [nvm](https://github.com/nvm-sh/nvm) to install it. 17 | 18 | Clone this repository, install Node.js dependencies, and build the source code: 19 | 20 | ```bash 21 | git clone git@github.com:perpetual-protocol/perp-oracle-contract.git 22 | npm i 23 | npm run build 24 | ``` 25 | 26 | Run all the test cases: 27 | 28 | ```bash 29 | npm run test 30 | ``` 31 | 32 | ## Changelog 33 | 34 | See [CHANGELOG](https://github.com/perpetual-protocol/perp-oracle-contract/blob/main/CHANGELOG.md). 35 | 36 | 37 | --- 38 | 39 | > If any features/functionalities described in the Perpetual Protocol documentation, code comments, marketing, community discussion or announcements, pre-production or testing code, or other non-production-code sources, vary or differ from the code used in production, in case of any dispute, the code used in production shall prevail. 40 | 41 | -------------------------------------------------------------------------------- /buidler.config.ts: -------------------------------------------------------------------------------- 1 | // Empty file to make truffle-flattener run 2 | -------------------------------------------------------------------------------- /contracts/BandPriceFeed.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.7.6; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import { Address } from "@openzeppelin/contracts/utils/Address.sol"; 6 | import { BlockContext } from "./base/BlockContext.sol"; 7 | import { IPriceFeedV2 } from "./interface/IPriceFeedV2.sol"; 8 | import { IStdReference } from "./interface/bandProtocol/IStdReference.sol"; 9 | import { CachedTwap } from "./twap/CachedTwap.sol"; 10 | 11 | contract BandPriceFeed is IPriceFeedV2, BlockContext, CachedTwap { 12 | using Address for address; 13 | 14 | // 15 | // STATE 16 | // 17 | string public constant QUOTE_ASSET = "USD"; 18 | 19 | string public baseAsset; 20 | IStdReference public stdRef; 21 | 22 | // 23 | // EXTERNAL NON-VIEW 24 | // 25 | 26 | constructor( 27 | IStdReference stdRefArg, 28 | string memory baseAssetArg, 29 | uint80 cacheTwapInterval 30 | ) CachedTwap(cacheTwapInterval) { 31 | // BPF_ANC: Reference address is not contract 32 | require(address(stdRefArg).isContract(), "BPF_ANC"); 33 | 34 | stdRef = stdRefArg; 35 | baseAsset = baseAssetArg; 36 | } 37 | 38 | /// @dev anyone can help update it. 39 | function update() external { 40 | IStdReference.ReferenceData memory bandData = _getReferenceData(); 41 | bool isUpdated = _update(bandData.rate, bandData.lastUpdatedBase); 42 | // BPF_NU: not updated 43 | require(isUpdated, "BPF_NU"); 44 | } 45 | 46 | function cacheTwap(uint256 interval) external override returns (uint256) { 47 | IStdReference.ReferenceData memory latestBandData = _getReferenceData(); 48 | if (interval == 0) { 49 | return latestBandData.rate; 50 | } 51 | return _cacheTwap(interval, latestBandData.rate, latestBandData.lastUpdatedBase); 52 | } 53 | 54 | // 55 | // EXTERNAL VIEW 56 | // 57 | 58 | function getPrice(uint256 interval) public view override returns (uint256) { 59 | IStdReference.ReferenceData memory latestBandData = _getReferenceData(); 60 | if (interval == 0) { 61 | return latestBandData.rate; 62 | } 63 | return _getCachedTwap(interval, latestBandData.rate, latestBandData.lastUpdatedBase); 64 | } 65 | 66 | // 67 | // EXTERNAL PURE 68 | // 69 | 70 | function decimals() external pure override returns (uint8) { 71 | // We assume Band Protocol always has 18 decimals 72 | // https://docs.bandchain.org/band-standard-dataset/using-band-dataset/using-band-dataset-evm.html 73 | return 18; 74 | } 75 | 76 | // 77 | // INTERNAL VIEW 78 | // 79 | 80 | function _getReferenceData() internal view returns (IStdReference.ReferenceData memory) { 81 | IStdReference.ReferenceData memory bandData = stdRef.getReferenceData(baseAsset, QUOTE_ASSET); 82 | // BPF_TQZ: timestamp for quote is zero 83 | require(bandData.lastUpdatedQuote > 0, "BPF_TQZ"); 84 | // BPF_TBZ: timestamp for base is zero 85 | require(bandData.lastUpdatedBase > 0, "BPF_TBZ"); 86 | // BPF_IP: invalid price 87 | require(bandData.rate > 0, "BPF_IP"); 88 | 89 | return bandData; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /contracts/ChainlinkPriceFeed.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.7.6; 3 | 4 | import { Address } from "@openzeppelin/contracts/utils/Address.sol"; 5 | import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; 6 | import { AggregatorV3Interface } from "@chainlink/contracts/src/v0.6/interfaces/AggregatorV3Interface.sol"; 7 | import { IChainlinkPriceFeed } from "./interface/IChainlinkPriceFeed.sol"; 8 | import { IPriceFeed } from "./interface/IPriceFeed.sol"; 9 | import { BlockContext } from "./base/BlockContext.sol"; 10 | 11 | contract ChainlinkPriceFeed is IChainlinkPriceFeed, IPriceFeed, BlockContext { 12 | using SafeMath for uint256; 13 | using Address for address; 14 | 15 | AggregatorV3Interface private immutable _aggregator; 16 | 17 | constructor(AggregatorV3Interface aggregator) { 18 | // CPF_ANC: Aggregator address is not contract 19 | require(address(aggregator).isContract(), "CPF_ANC"); 20 | 21 | _aggregator = aggregator; 22 | } 23 | 24 | function decimals() external view override returns (uint8) { 25 | return _aggregator.decimals(); 26 | } 27 | 28 | function getAggregator() external view override returns (address) { 29 | return address(_aggregator); 30 | } 31 | 32 | function getRoundData(uint80 roundId) external view override returns (uint256, uint256) { 33 | // NOTE: aggregator will revert if roundId is invalid (but there might not be a revert message sometimes) 34 | // will return (roundId, 0, 0, 0, roundId) if round is not complete (not existed yet) 35 | // https://docs.chain.link/docs/historical-price-data/ 36 | (, int256 price, , uint256 updatedAt, ) = _aggregator.getRoundData(roundId); 37 | 38 | // CPF_IP: Invalid Price 39 | require(price > 0, "CPF_IP"); 40 | 41 | // CPF_RINC: Round Is Not Complete 42 | require(updatedAt > 0, "CPF_RINC"); 43 | 44 | return (uint256(price), updatedAt); 45 | } 46 | 47 | function getPrice(uint256 interval) external view override returns (uint256) { 48 | // there are 3 timestamps: base(our target), previous & current 49 | // base: now - _interval 50 | // current: the current round timestamp from aggregator 51 | // previous: the previous round timestamp from aggregator 52 | // now >= previous > current > = < base 53 | // 54 | // while loop i = 0 55 | // --+------+-----+-----+-----+-----+-----+ 56 | // base current now(previous) 57 | // 58 | // while loop i = 1 59 | // --+------+-----+-----+-----+-----+-----+ 60 | // base current previous now 61 | 62 | (uint80 round, uint256 latestPrice, uint256 latestTimestamp) = _getLatestRoundData(); 63 | uint256 timestamp = _blockTimestamp(); 64 | uint256 baseTimestamp = timestamp.sub(interval); 65 | 66 | // if the latest timestamp <= base timestamp, which means there's no new price, return the latest price 67 | if (interval == 0 || round == 0 || latestTimestamp <= baseTimestamp) { 68 | return latestPrice; 69 | } 70 | 71 | // rounds are like snapshots, latestRound means the latest price snapshot; follow Chainlink's namings here 72 | uint256 previousTimestamp = latestTimestamp; 73 | uint256 cumulativeTime = timestamp.sub(previousTimestamp); 74 | uint256 weightedPrice = latestPrice.mul(cumulativeTime); 75 | uint256 timeFraction; 76 | while (true) { 77 | if (round == 0) { 78 | // to prevent from div 0 error, return the latest price if `cumulativeTime == 0` 79 | return cumulativeTime == 0 ? latestPrice : weightedPrice.div(cumulativeTime); 80 | } 81 | 82 | round = round - 1; 83 | (, uint256 currentPrice, uint256 currentTimestamp) = _getRoundData(round); 84 | 85 | // check if the current round timestamp is earlier than the base timestamp 86 | if (currentTimestamp <= baseTimestamp) { 87 | // the weighted time period is (base timestamp - previous timestamp) 88 | // ex: now is 1000, interval is 100, then base timestamp is 900 89 | // if timestamp of the current round is 970, and timestamp of NEXT round is 880, 90 | // then the weighted time period will be (970 - 900) = 70 instead of (970 - 880) 91 | weightedPrice = weightedPrice.add(currentPrice.mul(previousTimestamp.sub(baseTimestamp))); 92 | break; 93 | } 94 | 95 | timeFraction = previousTimestamp.sub(currentTimestamp); 96 | weightedPrice = weightedPrice.add(currentPrice.mul(timeFraction)); 97 | cumulativeTime = cumulativeTime.add(timeFraction); 98 | previousTimestamp = currentTimestamp; 99 | } 100 | 101 | return weightedPrice == 0 ? latestPrice : weightedPrice.div(interval); 102 | } 103 | 104 | function _getLatestRoundData() 105 | private 106 | view 107 | returns ( 108 | uint80, 109 | uint256 finalPrice, 110 | uint256 111 | ) 112 | { 113 | (uint80 round, int256 latestPrice, , uint256 latestTimestamp, ) = _aggregator.latestRoundData(); 114 | finalPrice = uint256(latestPrice); 115 | if (latestPrice < 0) { 116 | _requireEnoughHistory(round); 117 | (round, finalPrice, latestTimestamp) = _getRoundData(round - 1); 118 | } 119 | return (round, finalPrice, latestTimestamp); 120 | } 121 | 122 | function _getRoundData(uint80 _round) 123 | private 124 | view 125 | returns ( 126 | uint80, 127 | uint256, 128 | uint256 129 | ) 130 | { 131 | (uint80 round, int256 latestPrice, , uint256 latestTimestamp, ) = _aggregator.getRoundData(_round); 132 | while (latestPrice < 0) { 133 | _requireEnoughHistory(round); 134 | round = round - 1; 135 | (, latestPrice, , latestTimestamp, ) = _aggregator.getRoundData(round); 136 | } 137 | return (round, uint256(latestPrice), latestTimestamp); 138 | } 139 | 140 | function _requireEnoughHistory(uint80 _round) private pure { 141 | // CPF_NEH: no enough history 142 | require(_round > 0, "CPF_NEH"); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /contracts/ChainlinkPriceFeedV1R1.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.7.6; 3 | 4 | import { Address } from "@openzeppelin/contracts/utils/Address.sol"; 5 | import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; 6 | import { AggregatorV3Interface } from "@chainlink/contracts/src/v0.6/interfaces/AggregatorV3Interface.sol"; 7 | import { IChainlinkPriceFeedR1 } from "./interface/IChainlinkPriceFeedR1.sol"; 8 | import { IPriceFeed } from "./interface/IPriceFeed.sol"; 9 | import { BlockContext } from "./base/BlockContext.sol"; 10 | 11 | contract ChainlinkPriceFeedV1R1 is IChainlinkPriceFeedR1, IPriceFeed, BlockContext { 12 | using SafeMath for uint256; 13 | using Address for address; 14 | 15 | uint256 private constant _GRACE_PERIOD_TIME = 3600; 16 | 17 | AggregatorV3Interface private immutable _aggregator; 18 | AggregatorV3Interface private immutable _sequencerUptimeFeed; 19 | 20 | constructor(AggregatorV3Interface aggregator, AggregatorV3Interface sequencerUptimeFeed) { 21 | // CPF_ANC: Aggregator address is not contract 22 | require(address(aggregator).isContract(), "CPF_ANC"); 23 | // CPF_SUFNC: Sequencer uptime feed address is not contract 24 | require(address(sequencerUptimeFeed).isContract(), "CPF_SUFNC"); 25 | 26 | _aggregator = aggregator; 27 | _sequencerUptimeFeed = sequencerUptimeFeed; 28 | } 29 | 30 | function decimals() external view override returns (uint8) { 31 | return _aggregator.decimals(); 32 | } 33 | 34 | function getAggregator() external view override returns (address) { 35 | return address(_aggregator); 36 | } 37 | 38 | function getSequencerUptimeFeed() external view override returns (address) { 39 | return address(_sequencerUptimeFeed); 40 | } 41 | 42 | function getRoundData(uint80 roundId) external view override returns (uint256, uint256) { 43 | // NOTE: aggregator will revert if roundId is invalid (but there might not be a revert message sometimes) 44 | // will return (roundId, 0, 0, 0, roundId) if round is not complete (not existed yet) 45 | // https://docs.chain.link/docs/historical-price-data/ 46 | (, int256 price, , uint256 updatedAt, ) = _aggregator.getRoundData(roundId); 47 | 48 | // CPF_IP: Invalid Price 49 | require(price > 0, "CPF_IP"); 50 | 51 | // CPF_RINC: Round Is Not Complete 52 | require(updatedAt > 0, "CPF_RINC"); 53 | 54 | return (uint256(price), updatedAt); 55 | } 56 | 57 | function getPrice(uint256 interval) external view override returns (uint256) { 58 | (, int256 answer, uint256 startedAt, , ) = _sequencerUptimeFeed.latestRoundData(); 59 | 60 | // Answer == 0: Sequencer is up 61 | // Answer == 1: Sequencer is down 62 | require(answer == 0, "CPF_SD"); 63 | 64 | // startedAt timestamp will be 0 when the round is invalid. 65 | require(startedAt > 0, "CPF_IR"); 66 | 67 | // Make sure the grace period has passed after the sequencer is back up. 68 | uint256 timeSinceUp = block.timestamp - startedAt; 69 | // CPF_GPNO: Grace Period Not Over 70 | require(timeSinceUp > _GRACE_PERIOD_TIME, "CPF_GPNO"); 71 | 72 | // there are 3 timestamps: base(our target), previous & current 73 | // base: now - _interval 74 | // current: the current round timestamp from aggregator 75 | // previous: the previous round timestamp from aggregator 76 | // now >= previous > current > = < base 77 | // 78 | // while loop i = 0 79 | // --+------+-----+-----+-----+-----+-----+ 80 | // base current now(previous) 81 | // 82 | // while loop i = 1 83 | // --+------+-----+-----+-----+-----+-----+ 84 | // base current previous now 85 | 86 | (uint80 round, uint256 latestPrice, uint256 latestTimestamp) = _getLatestRoundData(); 87 | uint256 timestamp = _blockTimestamp(); 88 | uint256 baseTimestamp = timestamp.sub(interval); 89 | 90 | // if the latest timestamp <= base timestamp, which means there's no new price, return the latest price 91 | if (interval == 0 || round == 0 || latestTimestamp <= baseTimestamp) { 92 | return latestPrice; 93 | } 94 | 95 | // rounds are like snapshots, latestRound means the latest price snapshot; follow Chainlink's namings here 96 | uint256 previousTimestamp = latestTimestamp; 97 | uint256 cumulativeTime = timestamp.sub(previousTimestamp); 98 | uint256 weightedPrice = latestPrice.mul(cumulativeTime); 99 | uint256 timeFraction; 100 | while (true) { 101 | if (round == 0) { 102 | // to prevent from div 0 error, return the latest price if `cumulativeTime == 0` 103 | return cumulativeTime == 0 ? latestPrice : weightedPrice.div(cumulativeTime); 104 | } 105 | 106 | round = round - 1; 107 | (, uint256 currentPrice, uint256 currentTimestamp) = _getRoundData(round); 108 | 109 | // check if the current round timestamp is earlier than the base timestamp 110 | if (currentTimestamp <= baseTimestamp) { 111 | // the weighted time period is (base timestamp - previous timestamp) 112 | // ex: now is 1000, interval is 100, then base timestamp is 900 113 | // if timestamp of the current round is 970, and timestamp of NEXT round is 880, 114 | // then the weighted time period will be (970 - 900) = 70 instead of (970 - 880) 115 | weightedPrice = weightedPrice.add(currentPrice.mul(previousTimestamp.sub(baseTimestamp))); 116 | break; 117 | } 118 | 119 | timeFraction = previousTimestamp.sub(currentTimestamp); 120 | weightedPrice = weightedPrice.add(currentPrice.mul(timeFraction)); 121 | cumulativeTime = cumulativeTime.add(timeFraction); 122 | previousTimestamp = currentTimestamp; 123 | } 124 | 125 | return weightedPrice == 0 ? latestPrice : weightedPrice.div(interval); 126 | } 127 | 128 | function _getLatestRoundData() 129 | private 130 | view 131 | returns ( 132 | uint80, 133 | uint256 finalPrice, 134 | uint256 135 | ) 136 | { 137 | (uint80 round, int256 latestPrice, , uint256 latestTimestamp, ) = _aggregator.latestRoundData(); 138 | finalPrice = uint256(latestPrice); 139 | if (latestPrice < 0) { 140 | _requireEnoughHistory(round); 141 | (round, finalPrice, latestTimestamp) = _getRoundData(round - 1); 142 | } 143 | return (round, finalPrice, latestTimestamp); 144 | } 145 | 146 | function _getRoundData(uint80 _round) 147 | private 148 | view 149 | returns ( 150 | uint80, 151 | uint256, 152 | uint256 153 | ) 154 | { 155 | (uint80 round, int256 latestPrice, , uint256 latestTimestamp, ) = _aggregator.getRoundData(_round); 156 | while (latestPrice < 0) { 157 | _requireEnoughHistory(round); 158 | round = round - 1; 159 | (, latestPrice, , latestTimestamp, ) = _aggregator.getRoundData(round); 160 | } 161 | return (round, uint256(latestPrice), latestTimestamp); 162 | } 163 | 164 | function _requireEnoughHistory(uint80 _round) private pure { 165 | // CPF_NEH: no enough history 166 | require(_round > 0, "CPF_NEH"); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /contracts/ChainlinkPriceFeedV2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.7.6; 3 | 4 | import { Address } from "@openzeppelin/contracts/utils/Address.sol"; 5 | import { AggregatorV3Interface } from "@chainlink/contracts/src/v0.6/interfaces/AggregatorV3Interface.sol"; 6 | import { IChainlinkPriceFeed } from "./interface/IChainlinkPriceFeed.sol"; 7 | import { IPriceFeedV2 } from "./interface/IPriceFeedV2.sol"; 8 | import { BlockContext } from "./base/BlockContext.sol"; 9 | import { CachedTwap } from "./twap/CachedTwap.sol"; 10 | 11 | contract ChainlinkPriceFeedV2 is IChainlinkPriceFeed, IPriceFeedV2, BlockContext, CachedTwap { 12 | using Address for address; 13 | 14 | AggregatorV3Interface private immutable _aggregator; 15 | 16 | constructor(AggregatorV3Interface aggregator, uint80 cacheTwapInterval) CachedTwap(cacheTwapInterval) { 17 | // CPF_ANC: Aggregator address is not contract 18 | require(address(aggregator).isContract(), "CPF_ANC"); 19 | 20 | _aggregator = aggregator; 21 | } 22 | 23 | /// @dev anyone can help update it. 24 | function update() external { 25 | (, uint256 latestPrice, uint256 latestTimestamp) = _getLatestRoundData(); 26 | bool isUpdated = _update(latestPrice, latestTimestamp); 27 | // CPF_NU: not updated 28 | require(isUpdated, "CPF_NU"); 29 | } 30 | 31 | function cacheTwap(uint256 interval) external override returns (uint256) { 32 | (uint80 round, uint256 latestPrice, uint256 latestTimestamp) = _getLatestRoundData(); 33 | 34 | if (interval == 0 || round == 0) { 35 | return latestPrice; 36 | } 37 | return _cacheTwap(interval, latestPrice, latestTimestamp); 38 | } 39 | 40 | function decimals() external view override returns (uint8) { 41 | return _aggregator.decimals(); 42 | } 43 | 44 | function getAggregator() external view override returns (address) { 45 | return address(_aggregator); 46 | } 47 | 48 | function getRoundData(uint80 roundId) external view override returns (uint256, uint256) { 49 | // NOTE: aggregator will revert if roundId is invalid (but there might not be a revert message sometimes) 50 | // will return (roundId, 0, 0, 0, roundId) if round is not complete (not existed yet) 51 | // https://docs.chain.link/docs/historical-price-data/ 52 | (, int256 price, , uint256 updatedAt, ) = _aggregator.getRoundData(roundId); 53 | 54 | // CPF_IP: Invalid Price 55 | require(price > 0, "CPF_IP"); 56 | 57 | // CPF_RINC: Round Is Not Complete 58 | require(updatedAt > 0, "CPF_RINC"); 59 | 60 | return (uint256(price), updatedAt); 61 | } 62 | 63 | function getPrice(uint256 interval) external view override returns (uint256) { 64 | (uint80 round, uint256 latestPrice, uint256 latestTimestamp) = _getLatestRoundData(); 65 | 66 | if (interval == 0 || round == 0) { 67 | return latestPrice; 68 | } 69 | 70 | return _getCachedTwap(interval, latestPrice, latestTimestamp); 71 | } 72 | 73 | function _getLatestRoundData() 74 | private 75 | view 76 | returns ( 77 | uint80, 78 | uint256 finalPrice, 79 | uint256 80 | ) 81 | { 82 | (uint80 round, int256 latestPrice, , uint256 latestTimestamp, ) = _aggregator.latestRoundData(); 83 | finalPrice = uint256(latestPrice); 84 | if (latestPrice < 0) { 85 | _requireEnoughHistory(round); 86 | (round, finalPrice, latestTimestamp) = _getRoundData(round - 1); 87 | } 88 | return (round, finalPrice, latestTimestamp); 89 | } 90 | 91 | function _getRoundData(uint80 _round) 92 | private 93 | view 94 | returns ( 95 | uint80, 96 | uint256, 97 | uint256 98 | ) 99 | { 100 | (uint80 round, int256 latestPrice, , uint256 latestTimestamp, ) = _aggregator.getRoundData(_round); 101 | while (latestPrice < 0) { 102 | _requireEnoughHistory(round); 103 | round = round - 1; 104 | (, latestPrice, , latestTimestamp, ) = _aggregator.getRoundData(round); 105 | } 106 | return (round, uint256(latestPrice), latestTimestamp); 107 | } 108 | 109 | function _requireEnoughHistory(uint80 _round) private pure { 110 | // CPF_NEH: no enough history 111 | require(_round > 0, "CPF_NEH"); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /contracts/ChainlinkPriceFeedV3.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.7.6; 3 | 4 | import { Address } from "@openzeppelin/contracts/utils/Address.sol"; 5 | import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; 6 | import { AggregatorV3Interface } from "@chainlink/contracts/src/v0.6/interfaces/AggregatorV3Interface.sol"; 7 | import { IChainlinkPriceFeed } from "./interface/IChainlinkPriceFeed.sol"; 8 | import { IPriceFeed } from "./interface/IPriceFeed.sol"; 9 | import { IChainlinkPriceFeedV3 } from "./interface/IChainlinkPriceFeedV3.sol"; 10 | import { IPriceFeedUpdate } from "./interface/IPriceFeedUpdate.sol"; 11 | import { BlockContext } from "./base/BlockContext.sol"; 12 | import { CachedTwap } from "./twap/CachedTwap.sol"; 13 | 14 | contract ChainlinkPriceFeedV3 is IPriceFeed, IChainlinkPriceFeedV3, IPriceFeedUpdate, BlockContext, CachedTwap { 15 | using SafeMath for uint256; 16 | using Address for address; 17 | 18 | // 19 | // STATE 20 | // 21 | 22 | uint8 internal immutable _decimals; 23 | uint256 internal immutable _timeout; 24 | uint256 internal _lastValidPrice; 25 | uint256 internal _lastValidTimestamp; 26 | AggregatorV3Interface internal immutable _aggregator; 27 | 28 | // 29 | // EXTERNAL NON-VIEW 30 | // 31 | 32 | constructor( 33 | AggregatorV3Interface aggregator, 34 | uint256 timeout, 35 | uint80 twapInterval 36 | ) CachedTwap(twapInterval) { 37 | // CPF_ANC: Aggregator is not contract 38 | require(address(aggregator).isContract(), "CPF_ANC"); 39 | _aggregator = aggregator; 40 | 41 | _timeout = timeout; 42 | _decimals = aggregator.decimals(); 43 | } 44 | 45 | /// @inheritdoc IPriceFeedUpdate 46 | /// @notice anyone can help with updating 47 | /// @dev this function is used by PriceFeedUpdater for updating _lastValidPrice, 48 | /// _lastValidTimestamp and observation arry. 49 | /// The keeper can invoke callstatic on this function to check if those states nened to be updated. 50 | function update() external override { 51 | bool isUpdated = _cachePrice(); 52 | // CPF_NU: not updated 53 | require(isUpdated, "CPF_NU"); 54 | 55 | _update(_lastValidPrice, _lastValidTimestamp); 56 | } 57 | 58 | /// @inheritdoc IChainlinkPriceFeedV3 59 | function cacheTwap(uint256 interval) external override { 60 | _cachePrice(); 61 | 62 | _cacheTwap(interval, _lastValidPrice, _lastValidTimestamp); 63 | } 64 | 65 | // 66 | // EXTERNAL VIEW 67 | // 68 | 69 | /// @inheritdoc IChainlinkPriceFeedV3 70 | function getLastValidPrice() external view override returns (uint256) { 71 | return _lastValidPrice; 72 | } 73 | 74 | /// @inheritdoc IChainlinkPriceFeedV3 75 | function getLastValidTimestamp() external view override returns (uint256) { 76 | return _lastValidTimestamp; 77 | } 78 | 79 | /// @inheritdoc IPriceFeed 80 | /// @dev This is the view version of cacheTwap(). 81 | /// If the interval is zero, returns the latest valid price. 82 | /// Else, returns TWAP calculating with the latest valid price and timestamp. 83 | function getPrice(uint256 interval) external view override returns (uint256) { 84 | (uint256 latestValidPrice, uint256 latestValidTime) = _getLatestOrCachedPrice(); 85 | 86 | if (interval == 0) { 87 | return latestValidPrice; 88 | } 89 | 90 | return _getCachedTwap(interval, latestValidPrice, latestValidTime); 91 | } 92 | 93 | /// @inheritdoc IChainlinkPriceFeedV3 94 | function getLatestOrCachedPrice() external view override returns (uint256, uint256) { 95 | return _getLatestOrCachedPrice(); 96 | } 97 | 98 | /// @inheritdoc IChainlinkPriceFeedV3 99 | function isTimedOut() external view override returns (bool) { 100 | // Fetch the latest timstamp instead of _lastValidTimestamp is to prevent stale data 101 | // when the update() doesn't get triggered. 102 | (, uint256 lastestValidTimestamp) = _getLatestOrCachedPrice(); 103 | return lastestValidTimestamp > 0 && lastestValidTimestamp.add(_timeout) < _blockTimestamp(); 104 | } 105 | 106 | /// @inheritdoc IChainlinkPriceFeedV3 107 | function getFreezedReason() external view override returns (FreezedReason) { 108 | ChainlinkResponse memory response = _getChainlinkResponse(); 109 | return _getFreezedReason(response); 110 | } 111 | 112 | /// @inheritdoc IChainlinkPriceFeedV3 113 | function getAggregator() external view override returns (address) { 114 | return address(_aggregator); 115 | } 116 | 117 | /// @inheritdoc IChainlinkPriceFeedV3 118 | function getTimeout() external view override returns (uint256) { 119 | return _timeout; 120 | } 121 | 122 | /// @inheritdoc IPriceFeed 123 | function decimals() external view override returns (uint8) { 124 | return _decimals; 125 | } 126 | 127 | // 128 | // INTERNAL 129 | // 130 | 131 | function _cachePrice() internal returns (bool) { 132 | ChainlinkResponse memory response = _getChainlinkResponse(); 133 | if (_isAlreadyLatestCache(response)) { 134 | return false; 135 | } 136 | 137 | bool isUpdated = false; 138 | FreezedReason freezedReason = _getFreezedReason(response); 139 | if (_isNotFreezed(freezedReason)) { 140 | _lastValidPrice = uint256(response.answer); 141 | _lastValidTimestamp = response.updatedAt; 142 | isUpdated = true; 143 | } 144 | 145 | emit ChainlinkPriceUpdated(_lastValidPrice, _lastValidTimestamp, freezedReason); 146 | return isUpdated; 147 | } 148 | 149 | function _getLatestOrCachedPrice() internal view returns (uint256, uint256) { 150 | ChainlinkResponse memory response = _getChainlinkResponse(); 151 | if (_isAlreadyLatestCache(response)) { 152 | return (_lastValidPrice, _lastValidTimestamp); 153 | } 154 | 155 | FreezedReason freezedReason = _getFreezedReason(response); 156 | if (_isNotFreezed(freezedReason)) { 157 | return (uint256(response.answer), response.updatedAt); 158 | } 159 | 160 | // if freezed 161 | return (_lastValidPrice, _lastValidTimestamp); 162 | } 163 | 164 | function _getChainlinkResponse() internal view returns (ChainlinkResponse memory chainlinkResponse) { 165 | try _aggregator.decimals() returns (uint8 decimals) { 166 | chainlinkResponse.decimals = decimals; 167 | } catch { 168 | // if the call fails, return an empty response with success = false 169 | return chainlinkResponse; 170 | } 171 | 172 | try _aggregator.latestRoundData() returns ( 173 | uint80 roundId, 174 | int256 answer, 175 | uint256, // startedAt 176 | uint256 updatedAt, 177 | uint80 // answeredInRound 178 | ) { 179 | chainlinkResponse.roundId = roundId; 180 | chainlinkResponse.answer = answer; 181 | chainlinkResponse.updatedAt = updatedAt; 182 | chainlinkResponse.success = true; 183 | return chainlinkResponse; 184 | } catch { 185 | // if the call fails, return an empty response with success = false 186 | return chainlinkResponse; 187 | } 188 | } 189 | 190 | function _isAlreadyLatestCache(ChainlinkResponse memory response) internal view returns (bool) { 191 | return _lastValidTimestamp > 0 && _lastValidTimestamp == response.updatedAt; 192 | } 193 | 194 | /// @dev see IChainlinkPriceFeedV3Event.FreezedReason for each FreezedReason 195 | function _getFreezedReason(ChainlinkResponse memory response) internal view returns (FreezedReason) { 196 | if (!response.success) { 197 | return FreezedReason.NoResponse; 198 | } 199 | if (response.decimals != _decimals) { 200 | return FreezedReason.IncorrectDecimals; 201 | } 202 | if (response.roundId == 0) { 203 | return FreezedReason.NoRoundId; 204 | } 205 | if ( 206 | response.updatedAt == 0 || 207 | response.updatedAt < _lastValidTimestamp || 208 | response.updatedAt > _blockTimestamp() 209 | ) { 210 | return FreezedReason.InvalidTimestamp; 211 | } 212 | if (response.answer <= 0) { 213 | return FreezedReason.NonPositiveAnswer; 214 | } 215 | 216 | return FreezedReason.NotFreezed; 217 | } 218 | 219 | function _isNotFreezed(FreezedReason freezedReason) internal pure returns (bool) { 220 | return freezedReason == FreezedReason.NotFreezed; 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /contracts/PriceFeedDispatcher.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.7.6; 3 | 4 | import { Address } from "@openzeppelin/contracts/utils/Address.sol"; 5 | import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; 6 | import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; 7 | import { AggregatorV3Interface } from "@chainlink/contracts/src/v0.6/interfaces/AggregatorV3Interface.sol"; 8 | import { BlockContext } from "./base/BlockContext.sol"; 9 | import { IPriceFeed } from "./interface/IPriceFeed.sol"; 10 | import { IPriceFeedDispatcher } from "./interface/IPriceFeedDispatcher.sol"; 11 | import { UniswapV3PriceFeed } from "./UniswapV3PriceFeed.sol"; 12 | import { ChainlinkPriceFeedV3 } from "./ChainlinkPriceFeedV3.sol"; 13 | 14 | contract PriceFeedDispatcher is IPriceFeed, IPriceFeedDispatcher, Ownable, BlockContext { 15 | using SafeMath for uint256; 16 | using Address for address; 17 | 18 | uint8 private constant _DECIMALS = 18; 19 | 20 | Status internal _status = Status.Chainlink; 21 | UniswapV3PriceFeed internal _uniswapV3PriceFeed; 22 | ChainlinkPriceFeedV3 internal immutable _chainlinkPriceFeedV3; 23 | 24 | // 25 | // EXTERNAL NON-VIEW 26 | // 27 | 28 | constructor(address chainlinkPriceFeedV3) { 29 | // PFD_CNC: ChainlinkPriceFeed is not contract 30 | require(chainlinkPriceFeedV3.isContract(), "PFD_CNC"); 31 | 32 | _chainlinkPriceFeedV3 = ChainlinkPriceFeedV3(chainlinkPriceFeedV3); 33 | } 34 | 35 | /// @inheritdoc IPriceFeedDispatcher 36 | function dispatchPrice(uint256 interval) external override { 37 | if (isToUseUniswapV3PriceFeed()) { 38 | if (_status != Status.UniswapV3) { 39 | _status = Status.UniswapV3; 40 | emit StatusUpdated(_status); 41 | } 42 | return; 43 | } 44 | 45 | _chainlinkPriceFeedV3.cacheTwap(interval); 46 | } 47 | 48 | /// @dev can only be initialized once by the owner 49 | function setUniswapV3PriceFeed(address uniswapV3PriceFeed) external onlyOwner { 50 | // PFD_UCAU: UniswapV3PriceFeed (has to be) a contract and uninitialized 51 | require(address(_uniswapV3PriceFeed) == address(0) && uniswapV3PriceFeed.isContract(), "PFD_UCAU"); 52 | 53 | _uniswapV3PriceFeed = UniswapV3PriceFeed(uniswapV3PriceFeed); 54 | emit UniswapV3PriceFeedUpdated(uniswapV3PriceFeed); 55 | } 56 | 57 | // 58 | // EXTERNAL VIEW 59 | // 60 | 61 | /// @inheritdoc IPriceFeed 62 | function getPrice(uint256 interval) external view override returns (uint256) { 63 | return getDispatchedPrice(interval); 64 | } 65 | 66 | /// @inheritdoc IPriceFeedDispatcher 67 | function getChainlinkPriceFeedV3() external view override returns (address) { 68 | return address(_chainlinkPriceFeedV3); 69 | } 70 | 71 | /// @inheritdoc IPriceFeedDispatcher 72 | function getUniswapV3PriceFeed() external view override returns (address) { 73 | return address(_uniswapV3PriceFeed); 74 | } 75 | 76 | // 77 | // EXTERNAL PURE 78 | // 79 | 80 | /// @inheritdoc IPriceFeed 81 | function decimals() external pure override(IPriceFeed, IPriceFeedDispatcher) returns (uint8) { 82 | return _DECIMALS; 83 | } 84 | 85 | // 86 | // PUBLIC 87 | // 88 | 89 | /// @inheritdoc IPriceFeedDispatcher 90 | function getDispatchedPrice(uint256 interval) public view override returns (uint256) { 91 | if (isToUseUniswapV3PriceFeed()) { 92 | return _formatFromDecimalsToX10_18(_uniswapV3PriceFeed.getPrice(), _uniswapV3PriceFeed.decimals()); 93 | } 94 | 95 | return _formatFromDecimalsToX10_18(_chainlinkPriceFeedV3.getPrice(interval), _chainlinkPriceFeedV3.decimals()); 96 | } 97 | 98 | function isToUseUniswapV3PriceFeed() public view returns (bool) { 99 | return 100 | address(_uniswapV3PriceFeed) != address(0) && 101 | (_chainlinkPriceFeedV3.isTimedOut() || _status == Status.UniswapV3); 102 | } 103 | 104 | // 105 | // INTERNAL 106 | // 107 | 108 | function _formatFromDecimalsToX10_18(uint256 value, uint8 fromDecimals) internal pure returns (uint256) { 109 | uint8 toDecimals = _DECIMALS; 110 | 111 | if (fromDecimals == toDecimals) { 112 | return value; 113 | } 114 | 115 | return 116 | fromDecimals > toDecimals 117 | ? value.div(10**(fromDecimals - toDecimals)) 118 | : value.mul(10**(toDecimals - fromDecimals)); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /contracts/PriceFeedUpdater.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.7.6; 3 | 4 | import { Address } from "@openzeppelin/contracts/utils/Address.sol"; 5 | import { IPriceFeedUpdate } from "./interface/IPriceFeedUpdate.sol"; 6 | 7 | contract PriceFeedUpdater { 8 | using Address for address; 9 | 10 | address[] internal _priceFeeds; 11 | 12 | constructor(address[] memory priceFeedsArg) { 13 | // PFU_PFANC: price feed address is not contract 14 | for (uint256 i = 0; i < priceFeedsArg.length; i++) { 15 | require(priceFeedsArg[i].isContract(), "PFU_PFANC"); 16 | } 17 | 18 | _priceFeeds = priceFeedsArg; 19 | } 20 | 21 | // 22 | // EXTERNAL NON-VIEW 23 | // 24 | 25 | /* solhint-disable payable-fallback */ 26 | fallback() external { 27 | for (uint256 i = 0; i < _priceFeeds.length; i++) { 28 | // Updating PriceFeed might be failed because of price not changed, 29 | // Add try-catch here to update all markets anyway 30 | /* solhint-disable no-empty-blocks */ 31 | try IPriceFeedUpdate(_priceFeeds[i]).update() {} catch {} 32 | } 33 | } 34 | 35 | // 36 | // EXTERNAL VIEW 37 | // 38 | 39 | function getPriceFeeds() external view returns (address[] memory) { 40 | return _priceFeeds; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /contracts/UniswapV3PriceFeed.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.7.6; 3 | 4 | import { Address } from "@openzeppelin/contracts/utils/Address.sol"; 5 | import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; 6 | import { FixedPoint96 } from "@uniswap/v3-core/contracts/libraries/FixedPoint96.sol"; 7 | import { FullMath } from "@uniswap/v3-core/contracts/libraries/FullMath.sol"; 8 | import { TickMath } from "@uniswap/v3-core/contracts/libraries/TickMath.sol"; 9 | import { IUniswapV3PriceFeed } from "./interface/IUniswapV3PriceFeed.sol"; 10 | import { BlockContext } from "./base/BlockContext.sol"; 11 | 12 | contract UniswapV3PriceFeed is IUniswapV3PriceFeed, BlockContext { 13 | using Address for address; 14 | 15 | // 16 | // STATE 17 | // 18 | 19 | uint32 internal constant _TWAP_INTERVAL = 30 * 60; 20 | 21 | address public immutable pool; 22 | 23 | // 24 | // EXTERNAL 25 | // 26 | 27 | constructor(address poolArg) { 28 | // UPF_PANC: pool address is not contract 29 | require(address(poolArg).isContract(), "UPF_PANC"); 30 | 31 | pool = poolArg; 32 | } 33 | 34 | function getPrice() external view override returns (uint256) { 35 | uint256 markTwapX96 = _formatSqrtPriceX96ToPriceX96(_getSqrtMarkTwapX96(_TWAP_INTERVAL)); 36 | return _formatX96ToX10_18(markTwapX96); 37 | } 38 | 39 | function decimals() external pure override returns (uint8) { 40 | return 18; 41 | } 42 | 43 | // 44 | // INTERNAL 45 | // 46 | 47 | /// @dev if twapInterval < 10 (should be less than 1 block), return market price without twap directly 48 | /// as twapInterval is too short and makes getting twap over such a short period meaningless 49 | function _getSqrtMarkTwapX96(uint32 twapInterval) internal view returns (uint160) { 50 | if (twapInterval < 10) { 51 | (uint160 sqrtMarketPrice, , , , , , ) = IUniswapV3Pool(pool).slot0(); 52 | return sqrtMarketPrice; 53 | } 54 | uint32[] memory secondsAgos = new uint32[](2); 55 | 56 | // solhint-disable-next-line not-rely-on-time 57 | secondsAgos[0] = twapInterval; 58 | secondsAgos[1] = 0; 59 | (int56[] memory tickCumulatives, ) = IUniswapV3Pool(pool).observe(secondsAgos); 60 | 61 | // tick(imprecise as it's an integer) to price 62 | return TickMath.getSqrtRatioAtTick(int24((tickCumulatives[1] - tickCumulatives[0]) / twapInterval)); 63 | } 64 | 65 | function _formatSqrtPriceX96ToPriceX96(uint160 sqrtPriceX96) internal pure returns (uint256) { 66 | return FullMath.mulDiv(sqrtPriceX96, sqrtPriceX96, FixedPoint96.Q96); 67 | } 68 | 69 | function _formatX96ToX10_18(uint256 valueX96) internal pure returns (uint256) { 70 | return FullMath.mulDiv(valueX96, 1e18, FixedPoint96.Q96); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /contracts/base/BlockContext.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.7.6; 3 | 4 | abstract contract BlockContext { 5 | function _blockTimestamp() internal view virtual returns (uint256) { 6 | // Reply from Arbitrum 7 | // block.timestamp returns timestamp at the time at which the sequencer receives the tx. 8 | // It may not actually correspond to a particular L1 block 9 | return block.timestamp; 10 | } 11 | 12 | function _blockNumber() internal view virtual returns (uint256) { 13 | return block.number; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /contracts/interface/IChainlinkPriceFeed.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.7.6; 3 | 4 | interface IChainlinkPriceFeed { 5 | function getAggregator() external view returns (address); 6 | 7 | /// @param roundId The roundId that fed into Chainlink aggregator. 8 | function getRoundData(uint80 roundId) external view returns (uint256, uint256); 9 | } 10 | -------------------------------------------------------------------------------- /contracts/interface/IChainlinkPriceFeedR1.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.7.6; 3 | 4 | import "./IChainlinkPriceFeed.sol"; 5 | 6 | interface IChainlinkPriceFeedR1 is IChainlinkPriceFeed { 7 | function getSequencerUptimeFeed() external view returns (address); 8 | } 9 | -------------------------------------------------------------------------------- /contracts/interface/IChainlinkPriceFeedV3.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.7.6; 3 | 4 | interface IChainlinkPriceFeedV3Event { 5 | /// @param NotFreezed Default state: Chainlink is working as expected 6 | /// @param NoResponse Fails to call Chainlink 7 | /// @param IncorrectDecimals Inconsistent decimals between aggregator and price feed 8 | /// @param NoRoundId Zero round Id 9 | /// @param InvalidTimestamp No timestamp or it’s invalid, either outdated or in the future 10 | /// @param NonPositiveAnswer Price is zero or negative 11 | enum FreezedReason { NotFreezed, NoResponse, IncorrectDecimals, NoRoundId, InvalidTimestamp, NonPositiveAnswer } 12 | 13 | event ChainlinkPriceUpdated(uint256 price, uint256 timestamp, FreezedReason freezedReason); 14 | } 15 | 16 | interface IChainlinkPriceFeedV3 is IChainlinkPriceFeedV3Event { 17 | struct ChainlinkResponse { 18 | uint80 roundId; 19 | int256 answer; 20 | uint256 updatedAt; 21 | bool success; 22 | uint8 decimals; 23 | } 24 | 25 | /// @param interval TWAP interval 26 | /// when 0, cache price only, without TWAP; else, cache price & twap 27 | /// @dev This is the non-view version of cacheTwap() without return value 28 | function cacheTwap(uint256 interval) external; 29 | 30 | /// @notice Get the last cached valid price 31 | /// @return price The last cached valid price 32 | function getLastValidPrice() external view returns (uint256 price); 33 | 34 | /// @notice Get the last cached valid timestamp 35 | /// @return timestamp The last cached valid timestamp 36 | function getLastValidTimestamp() external view returns (uint256 timestamp); 37 | 38 | /// @notice Retrieve the latest price and timestamp from Chainlink aggregator, 39 | /// or return the last cached valid price and timestamp if the aggregator hasn't been updated or is frozen. 40 | /// @return price The latest valid price 41 | /// @return timestamp The latest valid timestamp 42 | function getLatestOrCachedPrice() external view returns (uint256 price, uint256 timestamp); 43 | 44 | function isTimedOut() external view returns (bool isTimedOut); 45 | 46 | /// @return reason The freezen reason 47 | function getFreezedReason() external view returns (FreezedReason reason); 48 | 49 | /// @return aggregator The address of Chainlink price feed aggregator 50 | function getAggregator() external view returns (address aggregator); 51 | 52 | /// @return period The timeout period 53 | function getTimeout() external view returns (uint256 period); 54 | } 55 | -------------------------------------------------------------------------------- /contracts/interface/IPriceFeed.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.7.6; 3 | 4 | interface IPriceFeed { 5 | function decimals() external view returns (uint8); 6 | 7 | /// @dev Returns the index price of the token. 8 | /// @param interval The interval represents twap interval. 9 | function getPrice(uint256 interval) external view returns (uint256); 10 | } 11 | -------------------------------------------------------------------------------- /contracts/interface/IPriceFeedDispatcher.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.7.6; 3 | 4 | interface IPriceFeedDispatcherEvent { 5 | enum Status { Chainlink, UniswapV3 } 6 | event StatusUpdated(Status status); 7 | event UniswapV3PriceFeedUpdated(address uniswapV3PriceFeed); 8 | } 9 | 10 | interface IPriceFeedDispatcher is IPriceFeedDispatcherEvent { 11 | /// @notice when Chainlink is down, switch priceFeed source to UniswapV3PriceFeed 12 | /// @dev this method is called by every tx that settles funding in Exchange.settleFunding() -> BaseToken.cacheTwap() 13 | /// @param interval only useful when using Chainlink; UniswapV3PriceFeed has its own fixed interval 14 | function dispatchPrice(uint256 interval) external; 15 | 16 | /// @notice return price from UniswapV3PriceFeed if _uniswapV3PriceFeed is ready to be switched to AND 17 | /// 1. _status is already UniswapV3PriceFeed OR 18 | /// 2. ChainlinkPriceFeedV3.isTimedOut() 19 | /// else, return price from ChainlinkPriceFeedV3 20 | /// @dev decimals of the return value is 18, which can be queried with the function decimals() 21 | /// @param interval only useful when using Chainlink; UniswapV3PriceFeed has its own fixed interval 22 | function getDispatchedPrice(uint256 interval) external view returns (uint256); 23 | 24 | function getChainlinkPriceFeedV3() external view returns (address); 25 | 26 | function getUniswapV3PriceFeed() external view returns (address); 27 | 28 | function decimals() external pure returns (uint8); 29 | } 30 | -------------------------------------------------------------------------------- /contracts/interface/IPriceFeedUpdate.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.7.6; 3 | 4 | interface IPriceFeedUpdate { 5 | /// @dev Update latest price. 6 | function update() external; 7 | } 8 | -------------------------------------------------------------------------------- /contracts/interface/IPriceFeedV2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.7.6; 3 | 4 | import "./IPriceFeed.sol"; 5 | 6 | interface IPriceFeedV2 is IPriceFeed { 7 | /// @dev Returns the cached index price of the token. 8 | /// @param interval The interval represents twap interval. 9 | function cacheTwap(uint256 interval) external returns (uint256); 10 | } 11 | -------------------------------------------------------------------------------- /contracts/interface/IUniswapV3PriceFeed.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.7.6; 3 | 4 | interface IUniswapV3PriceFeed { 5 | function getPrice() external view returns (uint256); 6 | 7 | function decimals() external pure returns (uint8); 8 | } 9 | -------------------------------------------------------------------------------- /contracts/interface/bandProtocol/IStdReference.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.7.6; 3 | pragma experimental ABIEncoderV2; 4 | 5 | // Copy from https://docs.bandchain.org/band-standard-dataset/using-band-dataset/using-band-dataset-evm.html 6 | interface IStdReference { 7 | /// A structure returned whenever someone requests for standard reference data. 8 | struct ReferenceData { 9 | uint256 rate; // base/quote exchange rate, multiplied by 1e18. 10 | uint256 lastUpdatedBase; // UNIX epoch of the last time when base price gets updated. 11 | uint256 lastUpdatedQuote; // UNIX epoch of the last time when quote price gets updated. 12 | } 13 | 14 | /// Returns the price data for the given base/quote pair. Revert if not available. 15 | function getReferenceData(string memory _base, string memory _quote) external view returns (ReferenceData memory); 16 | 17 | /// Similar to getReferenceData, but with multiple base/quote pairs at once. 18 | function getReferenceDataBulk(string[] memory _bases, string[] memory _quotes) 19 | external 20 | view 21 | returns (ReferenceData[] memory); 22 | } 23 | -------------------------------------------------------------------------------- /contracts/test/TestAggregatorV3.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity 0.7.6; 3 | 4 | import { AggregatorV3Interface } from "@chainlink/contracts/src/v0.6/interfaces/AggregatorV3Interface.sol"; 5 | 6 | contract TestAggregatorV3 is AggregatorV3Interface { 7 | struct RoundData { 8 | int256 answer; 9 | uint256 startedAt; 10 | uint256 updatedAt; 11 | uint80 answeredInRound; 12 | } 13 | 14 | mapping(uint80 => RoundData) public roundData; 15 | 16 | uint80 public latestRound; 17 | 18 | constructor() {} 19 | 20 | function decimals() external view virtual override returns (uint8) { 21 | return 18; 22 | } 23 | 24 | function description() external view override returns (string memory) { 25 | revert(); 26 | } 27 | 28 | function version() external view override returns (uint256) { 29 | revert(); 30 | } 31 | 32 | function setRoundData( 33 | uint80 roundId, 34 | int256 answer, 35 | uint256 startedAt, 36 | uint256 updatedAt, 37 | uint80 answeredInRound 38 | ) external { 39 | roundData[roundId] = RoundData({ 40 | answer: answer, 41 | startedAt: startedAt, 42 | updatedAt: updatedAt, 43 | answeredInRound: answeredInRound 44 | }); 45 | latestRound = roundId; 46 | } 47 | 48 | function getRoundData(uint80 _roundId) 49 | external 50 | view 51 | override 52 | returns ( 53 | uint80 roundId, 54 | int256 answer, 55 | uint256 startedAt, 56 | uint256 updatedAt, 57 | uint80 answeredInRound 58 | ) 59 | { 60 | return ( 61 | _roundId, 62 | roundData[_roundId].answer, 63 | roundData[_roundId].startedAt, 64 | roundData[_roundId].updatedAt, 65 | roundData[_roundId].answeredInRound 66 | ); 67 | } 68 | 69 | function latestRoundData() 70 | external 71 | view 72 | virtual 73 | override 74 | returns ( 75 | uint80 roundId, 76 | int256 answer, 77 | uint256 startedAt, 78 | uint256 updatedAt, 79 | uint80 answeredInRound 80 | ) 81 | { 82 | return ( 83 | latestRound, 84 | roundData[latestRound].answer, 85 | roundData[latestRound].startedAt, 86 | roundData[latestRound].updatedAt, 87 | roundData[latestRound].answeredInRound 88 | ); 89 | } 90 | 91 | // won't use in production, just for knowing how roundId works 92 | // https://docs.chain.link/docs/historical-price-data/#roundid-in-proxy 93 | function computeRoundId(uint16 phaseId, uint64 aggregatorRoundId) external pure returns (uint80) { 94 | return uint80((uint256(phaseId) << 64) | aggregatorRoundId); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /contracts/test/TestPriceFeedV2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.7.6; 3 | 4 | import { IPriceFeedV2 } from "../interface/IPriceFeedV2.sol"; 5 | 6 | contract TestPriceFeedV2 { 7 | address public chainlink; 8 | address public bandProtocol; 9 | 10 | uint256 public currentPrice; 11 | 12 | constructor(address _chainlink, address _bandProtocol) { 13 | chainlink = _chainlink; 14 | bandProtocol = _bandProtocol; 15 | currentPrice = 10; 16 | } 17 | 18 | // 19 | // for gas usage testing 20 | // 21 | function fetchChainlinkPrice(uint256 interval) external { 22 | for (uint256 i = 0; i < 17; i++) { 23 | IPriceFeedV2(chainlink).getPrice(interval); 24 | } 25 | currentPrice = IPriceFeedV2(chainlink).getPrice(interval); 26 | } 27 | 28 | function fetchBandProtocolPrice(uint256 interval) external { 29 | for (uint256 i = 0; i < 17; i++) { 30 | IPriceFeedV2(bandProtocol).getPrice(interval); 31 | } 32 | currentPrice = IPriceFeedV2(bandProtocol).getPrice(interval); 33 | } 34 | 35 | function cachedChainlinkPrice(uint256 interval) external { 36 | for (uint256 i = 0; i < 17; i++) { 37 | IPriceFeedV2(chainlink).cacheTwap(interval); 38 | } 39 | currentPrice = IPriceFeedV2(chainlink).cacheTwap(interval); 40 | } 41 | 42 | function cachedBandProtocolPrice(uint256 interval) external { 43 | for (uint256 i = 0; i < 17; i++) { 44 | IPriceFeedV2(bandProtocol).cacheTwap(interval); 45 | } 46 | currentPrice = IPriceFeedV2(bandProtocol).cacheTwap(interval); 47 | } 48 | 49 | // 50 | // for cached twap testing 51 | // 52 | 53 | // having this function for testing getPrice() and cacheTwap() 54 | // timestamp moves if any txs happen in hardhat env and which causes cacheTwap() will recalculate all the time 55 | function getPrice(uint256 interval) external returns (uint256 twap, uint256 cachedTwap) { 56 | twap = IPriceFeedV2(bandProtocol).getPrice(interval); 57 | cachedTwap = IPriceFeedV2(bandProtocol).cacheTwap(interval); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /contracts/test/TestStdReference.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity 0.7.6; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import { IStdReference } from "../interface/bandProtocol/IStdReference.sol"; 6 | 7 | contract TestStdReference is IStdReference { 8 | ReferenceData public refData; 9 | 10 | constructor() {} 11 | 12 | function setReferenceData(ReferenceData memory _refData) public { 13 | refData = _refData; 14 | } 15 | 16 | function getReferenceData(string memory _base, string memory _quote) 17 | external 18 | view 19 | override 20 | returns (ReferenceData memory) 21 | { 22 | return refData; 23 | } 24 | 25 | function getReferenceDataBulk(string[] memory _bases, string[] memory _quotes) 26 | external 27 | view 28 | override 29 | returns (ReferenceData[] memory) 30 | { 31 | revert(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /contracts/twap/CachedTwap.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.7.6; 3 | 4 | import { CumulativeTwap } from "./CumulativeTwap.sol"; 5 | 6 | abstract contract CachedTwap is CumulativeTwap { 7 | uint256 internal _cachedTwap; 8 | uint160 internal _lastUpdatedAt; 9 | uint80 internal _interval; 10 | 11 | constructor(uint80 interval) { 12 | _interval = interval; 13 | } 14 | 15 | function _cacheTwap( 16 | uint256 interval, 17 | uint256 latestPrice, 18 | uint256 latestUpdatedTimestamp 19 | ) internal virtual returns (uint256) { 20 | // always help update price for CumulativeTwap 21 | _update(latestPrice, latestUpdatedTimestamp); 22 | 23 | // if interval is not the same as _interval, won't update _lastUpdatedAt & _cachedTwap 24 | // and if interval == 0, return latestPrice directly as there won't be twap 25 | if (_interval != interval) { 26 | return interval == 0 ? latestPrice : _getTwap(interval, latestPrice, latestUpdatedTimestamp); 27 | } 28 | 29 | // only calculate twap and cache it when there's a new timestamp 30 | if (_blockTimestamp() != _lastUpdatedAt) { 31 | _lastUpdatedAt = uint160(_blockTimestamp()); 32 | _cachedTwap = _getTwap(interval, latestPrice, latestUpdatedTimestamp); 33 | } 34 | 35 | return _cachedTwap; 36 | } 37 | 38 | function _getCachedTwap( 39 | uint256 interval, 40 | uint256 latestPrice, 41 | uint256 latestUpdatedTimestamp 42 | ) internal view returns (uint256) { 43 | if (_blockTimestamp() == _lastUpdatedAt && interval == _interval) { 44 | return _cachedTwap; 45 | } 46 | return _getTwap(interval, latestPrice, latestUpdatedTimestamp); 47 | } 48 | 49 | /// @dev since we're plugging this contract to an existing system, we cannot return 0 upon the first call 50 | /// thus, return the latest price instead 51 | function _getTwap( 52 | uint256 interval, 53 | uint256 latestPrice, 54 | uint256 latestUpdatedTimestamp 55 | ) internal view returns (uint256) { 56 | uint256 twap = _calculateTwap(interval, latestPrice, latestUpdatedTimestamp); 57 | return twap == 0 ? latestPrice : twap; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /contracts/twap/CumulativeTwap.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.7.6; 3 | 4 | import { BlockContext } from "../base/BlockContext.sol"; 5 | import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; 6 | 7 | contract CumulativeTwap is BlockContext { 8 | using SafeMath for uint256; 9 | 10 | // 11 | // STRUCT 12 | // 13 | 14 | struct Observation { 15 | uint256 price; 16 | uint256 priceCumulative; 17 | uint256 timestamp; 18 | } 19 | 20 | // 21 | // STATE 22 | // 23 | 24 | uint16 public currentObservationIndex; 25 | uint16 internal constant MAX_OBSERVATION = 1800; 26 | // let's use 15 mins and 1 hr twap as example 27 | // if the price is updated every 2 secs, 1hr twap Observation should have 60 / 2 * 60 = 1800 slots 28 | Observation[MAX_OBSERVATION] public observations; 29 | 30 | // 31 | // INTERNAL 32 | // 33 | 34 | function _update(uint256 price, uint256 lastUpdatedTimestamp) internal returns (bool) { 35 | // for the first time updating 36 | if (currentObservationIndex == 0 && observations[0].timestamp == 0) { 37 | observations[0] = Observation({ price: price, priceCumulative: 0, timestamp: lastUpdatedTimestamp }); 38 | return true; 39 | } 40 | 41 | Observation memory lastObservation = observations[currentObservationIndex]; 42 | 43 | // CT_IT: invalid timestamp 44 | require(lastUpdatedTimestamp >= lastObservation.timestamp, "CT_IT"); 45 | 46 | // DO NOT accept same timestamp and different price 47 | // CT_IPWU: invalid price when update 48 | if (lastUpdatedTimestamp == lastObservation.timestamp) { 49 | require(price == lastObservation.price, "CT_IPWU"); 50 | } 51 | 52 | // if the price remains still, there's no need for update 53 | if (price == lastObservation.price) { 54 | return false; 55 | } 56 | 57 | // ring buffer index, make sure the currentObservationIndex is less than MAX_OBSERVATION 58 | currentObservationIndex = (currentObservationIndex + 1) % MAX_OBSERVATION; 59 | 60 | uint256 timestampDiff = lastUpdatedTimestamp - lastObservation.timestamp; 61 | observations[currentObservationIndex] = Observation({ 62 | priceCumulative: lastObservation.priceCumulative + (lastObservation.price * timestampDiff), 63 | timestamp: lastUpdatedTimestamp, 64 | price: price 65 | }); 66 | return true; 67 | } 68 | 69 | /// @dev This function will return 0 in following cases: 70 | /// 1. Not enough historical data (0 observation) 71 | /// 2. Not enough historical data (not enough observation) 72 | /// 3. interval == 0 73 | function _calculateTwap( 74 | uint256 interval, 75 | uint256 price, 76 | uint256 latestUpdatedTimestamp 77 | ) internal view returns (uint256) { 78 | // for the first time calculating 79 | if ((currentObservationIndex == 0 && observations[0].timestamp == 0) || interval == 0) { 80 | return 0; 81 | } 82 | 83 | Observation memory latestObservation = observations[currentObservationIndex]; 84 | 85 | // DO NOT accept same timestamp and different price 86 | // CT_IPWCT: invalid price when calculating twap 87 | // it's to be consistent with the logic of _update 88 | if (latestObservation.timestamp == latestUpdatedTimestamp) { 89 | require(price == latestObservation.price, "CT_IPWCT"); 90 | } 91 | 92 | uint256 currentTimestamp = _blockTimestamp(); 93 | uint256 targetTimestamp = currentTimestamp.sub(interval); 94 | uint256 currentCumulativePrice = 95 | latestObservation.priceCumulative.add( 96 | (latestObservation.price.mul(latestUpdatedTimestamp.sub(latestObservation.timestamp))).add( 97 | price.mul(currentTimestamp.sub(latestUpdatedTimestamp)) 98 | ) 99 | ); 100 | 101 | // case 1 102 | // beforeOrAt (it doesn't matter) 103 | // targetTimestamp atOrAfter 104 | // ------------------+-------------+---------------+-----------------> 105 | 106 | // case 2 107 | // (it doesn't matter) atOrAfter 108 | // beforeOrAt targetTimestamp 109 | // ------------------+-------------+---------------------------------> 110 | 111 | // case 3 112 | // beforeOrAt targetTimestamp atOrAfter 113 | // ------------------+-------------+---------------+-----------------> 114 | 115 | // atOrAfter 116 | // beforeOrAt targetTimestamp 117 | // ------------------+-------------+---------------+-----------------> 118 | 119 | (Observation memory beforeOrAt, Observation memory atOrAfter) = _getSurroundingObservations(targetTimestamp); 120 | uint256 targetCumulativePrice; 121 | 122 | // case1. left boundary 123 | if (targetTimestamp == beforeOrAt.timestamp) { 124 | targetCumulativePrice = beforeOrAt.priceCumulative; 125 | } 126 | // case2. right boundary 127 | else if (atOrAfter.timestamp == targetTimestamp) { 128 | targetCumulativePrice = atOrAfter.priceCumulative; 129 | } 130 | // not enough historical data 131 | else if (beforeOrAt.timestamp == atOrAfter.timestamp) { 132 | return 0; 133 | } 134 | // case3. in the middle 135 | else { 136 | // atOrAfter.timestamp == 0 implies beforeOrAt = observations[currentObservationIndex] 137 | // which means there's no atOrAfter from _getSurroundingObservations 138 | // and atOrAfter.priceCumulative should eaual to targetCumulativePrice 139 | if (atOrAfter.timestamp == 0) { 140 | targetCumulativePrice = 141 | beforeOrAt.priceCumulative + 142 | (beforeOrAt.price * (targetTimestamp - beforeOrAt.timestamp)); 143 | } else { 144 | uint256 targetTimeDelta = targetTimestamp - beforeOrAt.timestamp; 145 | uint256 observationTimeDelta = atOrAfter.timestamp - beforeOrAt.timestamp; 146 | 147 | targetCumulativePrice = beforeOrAt.priceCumulative.add( 148 | ((atOrAfter.priceCumulative.sub(beforeOrAt.priceCumulative)).mul(targetTimeDelta)).div( 149 | observationTimeDelta 150 | ) 151 | ); 152 | } 153 | } 154 | 155 | return currentCumulativePrice.sub(targetCumulativePrice).div(interval); 156 | } 157 | 158 | function _getSurroundingObservations(uint256 targetTimestamp) 159 | internal 160 | view 161 | returns (Observation memory beforeOrAt, Observation memory atOrAfter) 162 | { 163 | beforeOrAt = observations[currentObservationIndex]; 164 | 165 | // if the target is chronologically at or after the newest observation, we can early return 166 | if (observations[currentObservationIndex].timestamp <= targetTimestamp) { 167 | // if the observation is the same as the targetTimestamp 168 | // atOrAfter doesn't matter 169 | // if the observation is less than the targetTimestamp 170 | // simply return empty atOrAfter 171 | // atOrAfter repesents latest price and timestamp 172 | return (beforeOrAt, atOrAfter); 173 | } 174 | 175 | // now, set before to the oldest observation 176 | beforeOrAt = observations[(currentObservationIndex + 1) % MAX_OBSERVATION]; 177 | if (beforeOrAt.timestamp == 0) { 178 | beforeOrAt = observations[0]; 179 | } 180 | 181 | // ensure that the target is chronologically at or after the oldest observation 182 | // if no enough historical data, simply return two beforeOrAt and return 0 at _calculateTwap 183 | if (beforeOrAt.timestamp > targetTimestamp) { 184 | return (beforeOrAt, beforeOrAt); 185 | } 186 | 187 | return _binarySearch(targetTimestamp); 188 | } 189 | 190 | function _binarySearch(uint256 targetTimestamp) 191 | private 192 | view 193 | returns (Observation memory beforeOrAt, Observation memory atOrAfter) 194 | { 195 | uint256 l = (currentObservationIndex + 1) % MAX_OBSERVATION; // oldest observation 196 | uint256 r = l + MAX_OBSERVATION - 1; // newest observation 197 | uint256 i; 198 | 199 | while (true) { 200 | i = (l + r) / 2; 201 | 202 | beforeOrAt = observations[i % MAX_OBSERVATION]; 203 | 204 | // we've landed on an uninitialized observation, keep searching higher (more recently) 205 | if (beforeOrAt.timestamp == 0) { 206 | l = i + 1; 207 | continue; 208 | } 209 | 210 | atOrAfter = observations[(i + 1) % MAX_OBSERVATION]; 211 | 212 | bool targetAtOrAfter = beforeOrAt.timestamp <= targetTimestamp; 213 | 214 | // check if we've found the answer! 215 | if (targetAtOrAfter && targetTimestamp <= atOrAfter.timestamp) break; 216 | 217 | if (!targetAtOrAfter) r = i - 1; 218 | else l = i + 1; 219 | } 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = 'contracts' 3 | out = 'out' 4 | libs = ['node_modules', 'lib'] 5 | test = 'test/foundry' 6 | cache_path = 'forge-cache' 7 | no_match_path = 'contracts/test/*' 8 | optimizer = false 9 | 10 | # See more config options https://github.com/foundry-rs/foundry/tree/master/config 11 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import "@nomiclabs/hardhat-ethers" 2 | import "@nomiclabs/hardhat-waffle" 3 | import "@typechain/hardhat" 4 | import "hardhat-dependency-compiler" 5 | import "hardhat-gas-reporter" 6 | import { HardhatUserConfig } from "hardhat/config" 7 | import "solidity-coverage" 8 | 9 | const config: HardhatUserConfig = { 10 | solidity: { 11 | version: "0.7.6", 12 | settings: { 13 | optimizer: { enabled: true, runs: 200 }, 14 | evmVersion: "berlin", 15 | // for smock to mock contracts 16 | outputSelection: { 17 | "*": { 18 | "*": ["storageLayout"], 19 | }, 20 | }, 21 | }, 22 | }, 23 | mocha: { 24 | timeout: 100000, 25 | }, 26 | networks: { 27 | hardhat: { 28 | allowUnlimitedContractSize: true, 29 | }, 30 | }, 31 | dependencyCompiler: { 32 | // We have to compile from source since UniswapV3 doesn't provide artifacts in their npm package 33 | paths: ["@uniswap/v3-core/contracts/UniswapV3Factory.sol", "@uniswap/v3-core/contracts/UniswapV3Pool.sol"], 34 | }, 35 | gasReporter: { 36 | enabled: true, 37 | }, 38 | } 39 | 40 | export default config 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@perp/perp-oracle-contract", 3 | "version": "0.6.8", 4 | "description": "Perpetual Protocol Curie (v2) oracle contracts - v0.5.0 is not an audited version", 5 | "license": "GPL-3.0-or-later", 6 | "author": { 7 | "name": "Perpetual Protocol", 8 | "email": "hi@perp.fi", 9 | "url": "https://perp.com/" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/perpetual-protocol/perp-oracle-contract.git" 14 | }, 15 | "homepage": "https://perp.com/", 16 | "keywords": [ 17 | "perpetual-protocol", 18 | "perpetual-protocol-v2", 19 | "perp", 20 | "oracle", 21 | "contracts", 22 | "artifacts" 23 | ], 24 | "main": "index.js", 25 | "files": [ 26 | "contracts", 27 | "!contracts/test", 28 | "artifacts/contracts/**/*.json", 29 | "!artifacts/contracts/**/*.dbg.json", 30 | "!artifacts/contracts/test/**/*", 31 | "!artifacts/contracts/base/**/*" 32 | ], 33 | "scripts": { 34 | "clean": "rm -rf typechain && rm -rf artifacts && rm -rf cache", 35 | "test": "hardhat test", 36 | "foundry-test": "forge test -vvv", 37 | "build": "hardhat compile", 38 | "prepare": "husky install", 39 | "coverage": "forge coverage", 40 | "coverage:report": "forge coverage --report lcov; genhtml lcov.info --output-directory coverage-out", 41 | "lint": "npm run lint-contracts && npm run lint-tests", 42 | "lint-contracts": "solhint 'contracts/**/*.sol'", 43 | "lint-tests": "if grep -qr 'test' -e '.only('; then echo 'found .only() in tests'; exit 1; else echo 'not found .only() in tests'; fi", 44 | "lint-staged": "lint-staged", 45 | "flatten": "ts-node --files scripts/flatten.ts", 46 | "slither": "ts-node --files scripts/slither.ts" 47 | }, 48 | "devDependencies": { 49 | "@chainlink/contracts": "0.1.7", 50 | "@defi-wonderland/smock": "2.2.0", 51 | "@nomiclabs/hardhat-ethers": "2.0.5", 52 | "@nomiclabs/hardhat-waffle": "2.0.3", 53 | "@openzeppelin/contracts": "3.4.0", 54 | "@typechain/ethers-v5": "7.2.0", 55 | "@typechain/hardhat": "2.3.1", 56 | "@types/chai": "4.3.0", 57 | "@types/mocha": "9.0.0", 58 | "@types/node": "15.6.1", 59 | "@uniswap/v3-core": "https://github.com/Uniswap/uniswap-v3-core/tarball/v1.0.0", 60 | "chai": "4.3.6", 61 | "eslint-config-prettier": "8.3.0", 62 | "ethereum-waffle": "3.4.4", 63 | "ethers": "5.6.1", 64 | "hardhat": "2.9.9", 65 | "hardhat-dependency-compiler": "1.1.1", 66 | "hardhat-gas-reporter": "1.0.8", 67 | "husky": "6.0.0", 68 | "lint-staged": "11.0.0", 69 | "mocha": "9.1.1", 70 | "prettier": "2.3.0", 71 | "prettier-plugin-solidity": "1.0.0-beta.11", 72 | "solc": "0.7.6", 73 | "solhint": "3.3.6", 74 | "solhint-plugin-prettier": "0.0.5", 75 | "solidity-coverage": "0.7.17", 76 | "truffle-flatten": "1.0.8", 77 | "ts-generator": "0.1.1", 78 | "ts-node": "10.0.0", 79 | "typechain": "5.2.0", 80 | "typescript": "4.3.2" 81 | }, 82 | "lint-staged": { 83 | "*.ts": [ 84 | "prettier --write" 85 | ], 86 | "*.sol": [ 87 | "prettier --write", 88 | "solhint" 89 | ] 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | @chainlink/=node_modules/@chainlink/ 2 | @ensdomains/=node_modules/@ensdomains/ 3 | @openzeppelin/=node_modules/@openzeppelin/ 4 | @uniswap/=node_modules/@uniswap/ 5 | ds-test/=lib/forge-std/lib/ds-test/src/ 6 | eth-gas-reporter/=node_modules/eth-gas-reporter/ 7 | forge-std/=lib/forge-std/src/ 8 | hardhat/=node_modules/hardhat/ 9 | -------------------------------------------------------------------------------- /scripts/flatten.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path" 2 | import { mkdir, ShellString } from "shelljs" 3 | import { asyncExec } from "./helper" 4 | import { ContractNameAndDir, getAllDeployedContractsNamesAndDirs } from "./path" 5 | 6 | export const FLATTEN_BASE_DIR = "./flattened" 7 | 8 | export async function flatten( 9 | fromDir: string, 10 | toDir: string, 11 | filename: string, 12 | toFilename: string = filename, 13 | ): Promise { 14 | let licenseDeclared = false 15 | let versionDeclared = false 16 | let abiV2ExpDeclared = false 17 | let abiV2Declared = false 18 | 19 | const fromFile = join(fromDir, filename) 20 | const toFile = join(toDir, toFilename) 21 | 22 | mkdir("-p", toDir) 23 | const flattened = await asyncExec(`truffle-flattener ${fromFile}`) 24 | 25 | const trimmed = flattened.split("\n").filter(line => { 26 | if (line.indexOf("SPDX-License-Identifier") !== -1) { 27 | if (!licenseDeclared) { 28 | licenseDeclared = true 29 | return true 30 | } else { 31 | return false 32 | } 33 | } else if (line.indexOf("pragma solidity") !== -1) { 34 | if (!versionDeclared) { 35 | versionDeclared = true 36 | return true 37 | } else { 38 | return false 39 | } 40 | } else if (line.indexOf("pragma experimental ABIEncoderV2") !== -1) { 41 | if (!abiV2ExpDeclared) { 42 | abiV2ExpDeclared = true 43 | return true 44 | } else { 45 | return false 46 | } 47 | } else if (line.indexOf("pragma abicoder v2") !== -1) { 48 | if (!abiV2Declared) { 49 | abiV2Declared = true 50 | return true 51 | } else { 52 | return false 53 | } 54 | } else { 55 | return true 56 | } 57 | }) 58 | 59 | ShellString(trimmed.join("\n")).to(toFile) 60 | } 61 | 62 | export async function flattenAll() { 63 | const filesArr: ContractNameAndDir[] = getAllDeployedContractsNamesAndDirs() 64 | 65 | for (let i = 0; i < filesArr.length; i++) { 66 | const file = filesArr[i] 67 | await flatten(file.dir, "./flattened", file.name) 68 | } 69 | } 70 | 71 | async function main(): Promise { 72 | const fileNameAndDir: string = process.argv[2] as string 73 | // toDir is not necessary, thus placing as the last one 74 | const toDir: string = (process.argv[3] as string) ? process.argv[3] : "./flattened" 75 | 76 | // split file name and dir 77 | const arr = fileNameAndDir.split("/") 78 | const fileName = arr[arr.length - 1] 79 | arr.splice(arr.length - 1) 80 | const fromDir = arr.join("/") 81 | 82 | await flatten(fromDir, toDir, fileName) 83 | } 84 | 85 | if (require.main === module) { 86 | main() 87 | .then(() => process.exit(0)) 88 | .catch(error => { 89 | console.error(error) 90 | process.exit(1) 91 | }) 92 | } 93 | -------------------------------------------------------------------------------- /scripts/helper.ts: -------------------------------------------------------------------------------- 1 | import { ExecOptions } from "child_process" 2 | import { resolve } from "path" 3 | import { exec, test } from "shelljs" 4 | 5 | export function getNpmBin(cwd?: string) { 6 | const options: { [key: string]: any } = { silent: true } 7 | if (cwd) { 8 | options.cwd = cwd 9 | } 10 | 11 | return exec("npm bin", options).toString().trim() 12 | } 13 | 14 | /** 15 | * Execute command in in local node_modules directory 16 | * @param commandAndArgs command with arguments 17 | */ 18 | export function asyncExec(commandAndArgs: string, options?: ExecOptions): Promise { 19 | const [command, ...args] = commandAndArgs.split(" ") 20 | const cwd = options ? options.cwd : undefined 21 | const npmBin = resolve(getNpmBin(cwd), command) 22 | const realCommand = test("-e", npmBin) ? `${npmBin} ${args.join(" ")}` : commandAndArgs 23 | console.log(`> ${realCommand}`) 24 | return new Promise((resolve, reject) => { 25 | const cb = (code: number, stdout: string, stderr: string) => { 26 | if (code !== 0) { 27 | reject(stderr) 28 | } else { 29 | resolve(stdout) 30 | } 31 | } 32 | 33 | if (options) { 34 | exec(realCommand, options, cb) 35 | } else { 36 | exec(realCommand, cb) 37 | } 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /scripts/path.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs" 2 | 3 | const CONTRACT_DIT = "./contracts" 4 | 5 | export interface ContractNameAndDir { 6 | name: string 7 | dir: string 8 | } 9 | 10 | export function getAllDeployedContractsNamesAndDirs(): ContractNameAndDir[] { 11 | const contracts: ContractNameAndDir[] = [] 12 | fs.readdirSync(CONTRACT_DIT) 13 | .filter(file => file.includes(".sol")) 14 | .forEach(file => { 15 | contracts.push({ name: file, dir: CONTRACT_DIT }) 16 | }) 17 | return contracts 18 | } 19 | -------------------------------------------------------------------------------- /scripts/slither.ts: -------------------------------------------------------------------------------- 1 | import * as child_process from "child_process" 2 | import * as fs from "fs" 3 | import { join } from "path" 4 | import { mkdir } from "shelljs" 5 | import { flattenAll } from "./flatten" 6 | 7 | export const FLATTEN_BASE_DIR = "./flattened" 8 | 9 | enum IncludeOptions { 10 | LOW = "Low", 11 | MEDIUM = "Medium", 12 | HIGH = "High", 13 | } 14 | 15 | export async function slither( 16 | fromDir: string, 17 | toDir: string, 18 | filename: string, 19 | includeOption: IncludeOptions, 20 | toFilename: string = filename, 21 | ): Promise { 22 | const from = join(fromDir, filename) 23 | 24 | let excludeOptions: string 25 | const excludeLow: string = "--exclude-low" 26 | const excludeMedium: string = "--exclude-medium" 27 | const excludeHigh: string = "--exclude-high" 28 | if (includeOption === IncludeOptions.LOW) { 29 | excludeOptions = excludeMedium.concat(" ").concat(excludeHigh) 30 | } else if (includeOption === IncludeOptions.MEDIUM) { 31 | excludeOptions = excludeLow.concat(" ").concat(excludeHigh) 32 | } else { 33 | excludeOptions = excludeLow.concat(" ").concat(excludeMedium) 34 | } 35 | 36 | const arr = toFilename.split(".") 37 | arr[0] = arr[0].concat(`-${includeOption}`) 38 | arr[1] = "txt" 39 | const outputFileName = arr.join(".") 40 | const to = join(toDir, outputFileName) 41 | 42 | const cmd = `slither ${from} --exclude-optimization --exclude-informational ${excludeOptions} &> ${to}` 43 | await new Promise((res, rej) => { 44 | child_process.exec(cmd, (err, out) => res(out)) 45 | }) 46 | console.log(`${includeOption} impact concerns of ${filename} scanned!`) 47 | } 48 | 49 | async function runAll(): Promise { 50 | // can skip this step if there are already flattened files 51 | await flattenAll() 52 | const filesArr = fs.readdirSync("./flattened") 53 | 54 | fs.rmdirSync("./slither", { recursive: true }) 55 | mkdir("-p", "./slither") 56 | 57 | for (let i = 0; i < filesArr.length; i++) { 58 | const file = filesArr[i] 59 | await slither("./flattened", "./slither", file, IncludeOptions.MEDIUM) 60 | await slither("./flattened", "./slither", file, IncludeOptions.HIGH) 61 | } 62 | } 63 | 64 | // The following steps are required to use this script: 65 | // 1. pip3 install slither-analyzer 66 | // 2. pip3 install solc-select 67 | // 3. solc-select install 0.7.6 (check hardhat.config.ts) 68 | // 4. solc-select use 0.7.6 69 | if (require.main === module) { 70 | runAll() 71 | .then(() => process.exit(0)) 72 | .catch(error => { 73 | console.error(error) 74 | process.exit(1) 75 | }) 76 | } 77 | -------------------------------------------------------------------------------- /test/BandPriceFeed.spec.ts: -------------------------------------------------------------------------------- 1 | import { MockContract, smock } from "@defi-wonderland/smock" 2 | import { expect } from "chai" 3 | import { parseEther } from "ethers/lib/utils" 4 | import { ethers, waffle } from "hardhat" 5 | import { BandPriceFeed, TestStdReference, TestStdReference__factory } from "../typechain" 6 | 7 | interface BandPriceFeedFixture { 8 | bandPriceFeed: BandPriceFeed 9 | bandReference: MockContract 10 | baseAsset: string 11 | } 12 | 13 | async function bandPriceFeedFixture(): Promise { 14 | const [admin] = await ethers.getSigners() 15 | const testStdReferenceFactory = await smock.mock("TestStdReference", admin) 16 | const testStdReference = await testStdReferenceFactory.deploy() 17 | 18 | const baseAsset = "ETH" 19 | const bandPriceFeedFactory = await ethers.getContractFactory("BandPriceFeed") 20 | const bandPriceFeed = (await bandPriceFeedFactory.deploy(testStdReference.address, baseAsset, 900)) as BandPriceFeed 21 | 22 | return { bandPriceFeed, bandReference: testStdReference, baseAsset } 23 | } 24 | 25 | describe("BandPriceFeed/CumulativeTwap Spec", () => { 26 | const [admin] = waffle.provider.getWallets() 27 | const loadFixture: ReturnType = waffle.createFixtureLoader([admin]) 28 | let bandPriceFeed: BandPriceFeed 29 | let bandReference: MockContract 30 | let currentTime: number 31 | let roundData: any[] 32 | 33 | async function updatePrice(price: number, forward: boolean = true): Promise { 34 | roundData.push([parseEther(price.toString()), currentTime, currentTime]) 35 | bandReference.getReferenceData.returns(() => { 36 | return roundData[roundData.length - 1] 37 | }) 38 | await bandPriceFeed.update() 39 | 40 | if (forward) { 41 | currentTime += 15 42 | await ethers.provider.send("evm_setNextBlockTimestamp", [currentTime]) 43 | await ethers.provider.send("evm_mine", []) 44 | } 45 | } 46 | 47 | beforeEach(async () => { 48 | const _fixture = await loadFixture(bandPriceFeedFixture) 49 | bandReference = _fixture.bandReference 50 | bandPriceFeed = _fixture.bandPriceFeed 51 | roundData = [] 52 | }) 53 | 54 | describe("update", () => { 55 | beforeEach(async () => { 56 | currentTime = (await waffle.provider.getBlock("latest")).timestamp 57 | }) 58 | 59 | it("update price once", async () => { 60 | roundData.push([parseEther("400"), currentTime, currentTime]) 61 | bandReference.getReferenceData.returns(() => { 62 | return roundData[roundData.length - 1] 63 | }) 64 | 65 | expect(await bandPriceFeed.update()) 66 | .to.be.emit(bandPriceFeed, "PriceUpdated") 67 | .withArgs(parseEther("400"), currentTime, 0) 68 | 69 | const observation = await bandPriceFeed.observations(0) 70 | const round = roundData[0] 71 | expect(observation.price).to.eq(round[0]) 72 | expect(observation.timestamp).to.eq(round[1]) 73 | expect(observation.priceCumulative).to.eq(0) 74 | }) 75 | 76 | it("update price twice", async () => { 77 | await updatePrice(400, false) 78 | 79 | roundData.push([parseEther("440"), currentTime + 15, currentTime + 15]) 80 | bandReference.getReferenceData.returns(() => { 81 | return roundData[roundData.length - 1] 82 | }) 83 | await bandPriceFeed.update() 84 | 85 | const observation = await bandPriceFeed.observations(1) 86 | const round = roundData[roundData.length - 1] 87 | expect(observation.price).to.eq(round[0]) 88 | expect(observation.timestamp).to.eq(round[1]) 89 | expect(observation.priceCumulative).to.eq(parseEther("6000")) 90 | }) 91 | 92 | it("force error, the second update is the same price and timestamp", async () => { 93 | await updatePrice(400, false) 94 | 95 | roundData.push([parseEther("400"), currentTime, currentTime]) 96 | bandReference.getReferenceData.returns(() => { 97 | return roundData[roundData.length - 1] 98 | }) 99 | await expect(bandPriceFeed.update()).to.be.revertedWith("BPF_NU") 100 | }) 101 | 102 | it("force error, the second update is the same timestamp but different price", async () => { 103 | await updatePrice(400, false) 104 | 105 | roundData.push([parseEther("440"), currentTime, currentTime]) 106 | bandReference.getReferenceData.returns(() => { 107 | return roundData[roundData.length - 1] 108 | }) 109 | await expect(bandPriceFeed.update()).to.be.revertedWith("CT_IPWU") 110 | }) 111 | }) 112 | 113 | describe("twap", () => { 114 | beforeEach(async () => { 115 | // `base` = now - _interval 116 | // bandReference's answer 117 | // timestamp(base + 0) : 400 118 | // timestamp(base + 15) : 405 119 | // timestamp(base + 30) : 410 120 | // now = base + 45 121 | // 122 | // --+------+-----+-----+-----+-----+-----+ 123 | // base now 124 | const latestTimestamp = (await waffle.provider.getBlock("latest")).timestamp 125 | currentTime = latestTimestamp 126 | 127 | await updatePrice(400) 128 | await updatePrice(405) 129 | await updatePrice(410) 130 | }) 131 | 132 | describe("getPrice", () => { 133 | it("return latest price if interval is zero", async () => { 134 | const price = await bandPriceFeed.getPrice(0) 135 | expect(price).to.eq(parseEther("410")) 136 | }) 137 | 138 | it("twap price", async () => { 139 | const price = await bandPriceFeed.getPrice(45) 140 | expect(price).to.eq(parseEther("405")) 141 | }) 142 | 143 | it("asking interval more than bandReference has", async () => { 144 | const price = await bandPriceFeed.getPrice(46) // should directly return latest price 145 | await expect(price).to.eq(parseEther("410")) 146 | }) 147 | 148 | it("asking interval less than bandReference has", async () => { 149 | const price = await bandPriceFeed.getPrice(44) 150 | expect(price).to.eq("405113636363636363636") 151 | }) 152 | 153 | it("asking interval less the timestamp of the latest observation", async () => { 154 | const price = await bandPriceFeed.getPrice(14) 155 | expect(price).to.eq(parseEther("410")) 156 | }) 157 | 158 | it("the latest band reference data is not being updated to observation", async () => { 159 | currentTime += 15 160 | await updatePrice(415) 161 | 162 | // (415 * 15 + 410 * 30) / 45 = 411.666666 163 | const price = await bandPriceFeed.getPrice(45) 164 | expect(price).to.eq("411666666666666666666") 165 | }) 166 | 167 | it("given variant price period", async () => { 168 | roundData.push([parseEther("420"), currentTime + 30, currentTime + 30]) 169 | await ethers.provider.send("evm_setNextBlockTimestamp", [currentTime + 50]) 170 | await ethers.provider.send("evm_mine", []) 171 | // twap price should be ((400 * 15) + (405 * 15) + (410 * 45) + (420 * 20)) / 95 = 409.736 172 | const price = await bandPriceFeed.getPrice(95) 173 | expect(price).to.eq("409736842105263157894") 174 | }) 175 | 176 | it("latest price update time is earlier than the request, return the latest price", async () => { 177 | await ethers.provider.send("evm_setNextBlockTimestamp", [currentTime + 100]) 178 | await ethers.provider.send("evm_mine", []) 179 | 180 | // latest update time is base + 30, but now is base + 145 and asking for (now - 45) 181 | // should return the latest price directly 182 | const price = await bandPriceFeed.getPrice(45) 183 | expect(price).to.eq(parseEther("410")) 184 | }) 185 | }) 186 | }) 187 | 188 | describe("circular observations", () => { 189 | let currentTimeBefore: number 190 | let beginPrice = 400 191 | 192 | beforeEach(async () => { 193 | currentTimeBefore = currentTime = (await waffle.provider.getBlock("latest")).timestamp 194 | 195 | // fill up 1799 observations and the final price will be observations[1798] = 1798 + 400 = 2198, 196 | // and observations[1799] is empty 197 | for (let i = 0; i < 1799; i++) { 198 | await updatePrice(beginPrice + i) 199 | } 200 | }) 201 | 202 | it("verify status", async () => { 203 | expect(await bandPriceFeed.currentObservationIndex()).to.eq(1798) 204 | 205 | // observations[1799] shouldn't be updated since we only run 1799 times in for loop 206 | const observation1799 = await bandPriceFeed.observations(1799) 207 | expect(observation1799.price).to.eq(0) 208 | expect(observation1799.priceCumulative).to.eq(0) 209 | expect(observation1799.timestamp).to.eq(0) 210 | 211 | const observation1798 = await bandPriceFeed.observations(1798) 212 | expect(observation1798.price).to.eq(parseEther("2198")) 213 | expect(observation1798.timestamp).to.eq(currentTimeBefore + 15 * 1798) 214 | 215 | // (2196 * 15 + 2197 * 15 + 2198 * 15) / 45 = 2197 216 | const price = await bandPriceFeed.getPrice(45) 217 | expect(price).to.eq(parseEther("2197")) 218 | }) 219 | 220 | it("get price after currentObservationIndex is rotated to 0", async () => { 221 | // increase currentObservationIndex to 1799 222 | await updatePrice(beginPrice + 1799) 223 | 224 | // increase (rotate) currentObservationIndex to 0 225 | // which will override the first observation which is observations[0] 226 | await updatePrice(beginPrice + 1800) 227 | 228 | expect(await bandPriceFeed.currentObservationIndex()).to.eq(0) 229 | 230 | // (2200 * 15 + 2199 * 15 + 2198 * 15) / 45 = 2199 231 | const price = await bandPriceFeed.getPrice(45) 232 | expect(price).to.eq(parseEther("2199")) 233 | }) 234 | 235 | it("get price after currentObservationIndex is rotated to 10", async () => { 236 | await updatePrice(beginPrice + 1799) 237 | for (let i = 0; i < 10; i++) { 238 | await updatePrice(beginPrice + 1800 + i) 239 | } 240 | 241 | expect(await bandPriceFeed.currentObservationIndex()).to.eq(9) 242 | 243 | // (2207 * 15 + 2208 * 15 + 2209 * 15) / 45 = 2208 244 | const price = await bandPriceFeed.getPrice(45) 245 | expect(price).to.eq(parseEther("2208")) 246 | }) 247 | 248 | it("asking interval is exact the same as max allowable interval", async () => { 249 | // update 2 more times to rotate currentObservationIndex to 0 250 | await updatePrice(beginPrice + 1799) 251 | 252 | // this one will override the first observation which is observations[0] 253 | await updatePrice(beginPrice + 1800, false) 254 | 255 | expect(await bandPriceFeed.currentObservationIndex()).to.eq(0) 256 | 257 | // (((401 + 2199) / 2) * (26986-1) + 2200 * 1 ) / 26986 = 1300.0333506263 258 | const price = await bandPriceFeed.getPrice(1799 * 15 + 1) 259 | expect(price).to.eq("1300033350626250648484") 260 | }) 261 | 262 | it("get the latest price, if asking interval more than observation has", async () => { 263 | // update 2 more times to rotate currentObservationIndex to 0 264 | await updatePrice(beginPrice + 1799) 265 | 266 | // this one will override the first observation which is observations[0] 267 | await updatePrice(beginPrice + 1800, false) 268 | 269 | expect(await bandPriceFeed.currentObservationIndex()).to.eq(0) 270 | 271 | // the longest interval = 1799 * 15 = 26985, it should be revert when interval >= 26986 272 | // here, we set interval to 26987 because hardhat increases the timestamp by 1 when any tx happens 273 | const price = await bandPriceFeed.getPrice(1799 * 15 + 2) 274 | const priceWith0Interval = await bandPriceFeed.getPrice(0) 275 | await expect(price).to.eq(priceWith0Interval) 276 | }) 277 | }) 278 | 279 | describe("price is not updated yet", () => { 280 | const price = "100" 281 | 282 | beforeEach(async () => { 283 | currentTime = (await waffle.provider.getBlock("latest")).timestamp 284 | roundData.push([parseEther(price), currentTime, currentTime]) 285 | bandReference.getReferenceData.returns(() => { 286 | return roundData[roundData.length - 1] 287 | }) 288 | }) 289 | 290 | it("get spot price", async () => { 291 | expect(await bandPriceFeed.getPrice(0)).to.eq(parseEther(price)) 292 | }) 293 | 294 | it("get twap price", async () => { 295 | // if observation has no data, we'll get latest price 296 | expect(await bandPriceFeed.getPrice(900)).to.eq(parseEther(price)) 297 | }) 298 | }) 299 | }) 300 | -------------------------------------------------------------------------------- /test/CachedTwap.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai" 2 | import { parseEther } from "ethers/lib/utils" 3 | import { ethers, waffle } from "hardhat" 4 | import { BandPriceFeed, ChainlinkPriceFeedV2, TestAggregatorV3, TestPriceFeedV2, TestStdReference } from "../typechain" 5 | 6 | interface PriceFeedFixture { 7 | bandPriceFeed: BandPriceFeed 8 | bandReference: TestStdReference 9 | baseAsset: string 10 | 11 | chainlinkPriceFeed: ChainlinkPriceFeedV2 12 | aggregator: TestAggregatorV3 13 | } 14 | async function priceFeedFixture(): Promise { 15 | const twapInterval = 45 16 | // band protocol 17 | const testStdReferenceFactory = await ethers.getContractFactory("TestStdReference") 18 | const testStdReference = await testStdReferenceFactory.deploy() 19 | 20 | const baseAsset = "ETH" 21 | const bandPriceFeedFactory = await ethers.getContractFactory("BandPriceFeed") 22 | const bandPriceFeed = (await bandPriceFeedFactory.deploy( 23 | testStdReference.address, 24 | baseAsset, 25 | twapInterval, 26 | )) as BandPriceFeed 27 | 28 | // chainlink 29 | const testAggregatorFactory = await ethers.getContractFactory("TestAggregatorV3") 30 | const testAggregator = await testAggregatorFactory.deploy() 31 | 32 | const chainlinkPriceFeedFactory = await ethers.getContractFactory("ChainlinkPriceFeedV2") 33 | const chainlinkPriceFeed = (await chainlinkPriceFeedFactory.deploy( 34 | testAggregator.address, 35 | twapInterval, 36 | )) as ChainlinkPriceFeedV2 37 | 38 | return { bandPriceFeed, bandReference: testStdReference, baseAsset, chainlinkPriceFeed, aggregator: testAggregator } 39 | } 40 | 41 | describe("Cached Twap Spec", () => { 42 | const [admin] = waffle.provider.getWallets() 43 | const loadFixture: ReturnType = waffle.createFixtureLoader([admin]) 44 | let bandPriceFeed: BandPriceFeed 45 | let bandReference: TestStdReference 46 | let chainlinkPriceFeed: ChainlinkPriceFeedV2 47 | let aggregator: TestAggregatorV3 48 | let currentTime: number 49 | let testPriceFeed: TestPriceFeedV2 50 | let round: number 51 | 52 | async function setNextBlockTimestamp(timestamp: number) { 53 | await ethers.provider.send("evm_setNextBlockTimestamp", [timestamp]) 54 | await ethers.provider.send("evm_mine", []) 55 | } 56 | 57 | async function updatePrice(price: number, forward: boolean = true): Promise { 58 | await bandReference.setReferenceData({ 59 | rate: parseEther(price.toString()), 60 | lastUpdatedBase: currentTime, 61 | lastUpdatedQuote: currentTime, 62 | }) 63 | await bandPriceFeed.update() 64 | 65 | await aggregator.setRoundData(round, parseEther(price.toString()), currentTime, currentTime, round) 66 | await chainlinkPriceFeed.update() 67 | 68 | if (forward) { 69 | currentTime += 15 70 | await setNextBlockTimestamp(currentTime) 71 | } 72 | } 73 | 74 | beforeEach(async () => { 75 | const _fixture = await loadFixture(priceFeedFixture) 76 | bandReference = _fixture.bandReference 77 | bandPriceFeed = _fixture.bandPriceFeed 78 | chainlinkPriceFeed = _fixture.chainlinkPriceFeed 79 | aggregator = _fixture.aggregator 80 | round = 0 81 | 82 | const TestPriceFeedFactory = await ethers.getContractFactory("TestPriceFeedV2") 83 | testPriceFeed = (await TestPriceFeedFactory.deploy( 84 | chainlinkPriceFeed.address, 85 | bandPriceFeed.address, 86 | )) as TestPriceFeedV2 87 | 88 | currentTime = (await waffle.provider.getBlock("latest")).timestamp 89 | await updatePrice(400) 90 | await updatePrice(405) 91 | await updatePrice(410) 92 | 93 | await bandReference.setReferenceData({ 94 | rate: parseEther("415"), 95 | lastUpdatedBase: currentTime, 96 | lastUpdatedQuote: currentTime, 97 | }) 98 | }) 99 | 100 | describe("cacheTwap should be exactly the same getPrice()", () => { 101 | it("return latest price if interval is zero", async () => { 102 | const price = await testPriceFeed.callStatic.getPrice(0) 103 | expect(price.twap).to.eq(price.cachedTwap) 104 | expect(price.twap).to.eq(await bandPriceFeed.getPrice(0)) 105 | }) 106 | 107 | it("if cached twap found, twap price should equal cached twap", async () => { 108 | const price = await testPriceFeed.callStatic.getPrice(45) 109 | expect(price.twap).to.eq(price.cachedTwap) 110 | // `getPrice` here is no a view function, it mocked function in TestPriceFeed 111 | // and it will update the cache if necessary 112 | expect(price.twap).to.eq(await bandPriceFeed.getPrice(45)) 113 | }) 114 | 115 | it("if no cached twap found, twap price should equal cached twap", async () => { 116 | const price = await testPriceFeed.callStatic.getPrice(46) 117 | expect(price.twap).to.eq(price.cachedTwap) 118 | expect(price.twap).to.eq(await bandPriceFeed.getPrice(46)) 119 | }) 120 | 121 | it("re-calculate cached twap if timestamp moves", async () => { 122 | const price1 = await testPriceFeed.callStatic.getPrice(45) 123 | // timestamp changes due to cacheTwap() 124 | await testPriceFeed.getPrice(45) 125 | 126 | const price2 = await chainlinkPriceFeed.callStatic.getPrice(45) 127 | expect(price1.cachedTwap).to.not.eq(price2) 128 | }) 129 | 130 | it("re-calculate twap if block timestamp is different from last cached twap timestamp", async () => { 131 | const price1 = await testPriceFeed.callStatic.getPrice(45) 132 | await testPriceFeed.getPrice(45) 133 | 134 | // forword block timestamp 15sec 135 | currentTime += 15 136 | await setNextBlockTimestamp(currentTime) 137 | 138 | const price2 = await bandPriceFeed.getPrice(45) 139 | expect(price2).to.not.eq(price1.twap) 140 | }) 141 | 142 | it("re-calculate twap if interval is different from interval of cached twap", async () => { 143 | await bandPriceFeed.cacheTwap(45) 144 | const price1 = await bandPriceFeed.getPrice(45) 145 | const price2 = await bandPriceFeed.getPrice(15) 146 | // shoule re-calculate twap 147 | expect(price2).to.not.eq(price1) 148 | }) 149 | 150 | it("re-calculate twap if timestamp doesn't change", async () => { 151 | const price1 = await testPriceFeed.getPrice(45) 152 | 153 | // forword block timestamp 15sec 154 | currentTime += 15 155 | await setNextBlockTimestamp(currentTime) 156 | 157 | const price2 = await testPriceFeed.getPrice(45) 158 | expect(price2).to.not.eq(price1) 159 | }) 160 | }) 161 | }) 162 | -------------------------------------------------------------------------------- /test/ChainlinkPriceFeed.spec.ts: -------------------------------------------------------------------------------- 1 | import { MockContract, smock } from "@defi-wonderland/smock" 2 | import { expect } from "chai" 3 | import { BigNumber } from "ethers" 4 | import { parseEther, parseUnits } from "ethers/lib/utils" 5 | import { ethers, waffle } from "hardhat" 6 | import { ChainlinkPriceFeed, TestAggregatorV3, TestAggregatorV3__factory } from "../typechain" 7 | import { computeRoundId } from "./shared/chainlink" 8 | 9 | interface ChainlinkPriceFeedFixture { 10 | chainlinkPriceFeed: ChainlinkPriceFeed 11 | aggregator: MockContract 12 | chainlinkPriceFeed2: ChainlinkPriceFeed 13 | aggregator2: MockContract 14 | } 15 | 16 | async function chainlinkPriceFeedFixture(): Promise { 17 | const [admin] = await ethers.getSigners(); 18 | const aggregatorFactory = await smock.mock("TestAggregatorV3", admin) 19 | const aggregator = await aggregatorFactory.deploy() 20 | aggregator.decimals.returns(() => 18) 21 | 22 | const chainlinkPriceFeedFactory = await ethers.getContractFactory("ChainlinkPriceFeed") 23 | const chainlinkPriceFeed = (await chainlinkPriceFeedFactory.deploy(aggregator.address)) as ChainlinkPriceFeed 24 | 25 | const aggregatorFactory2 = await smock.mock("TestAggregatorV3", admin) 26 | const aggregator2 = await aggregatorFactory2.deploy() 27 | aggregator2.decimals.returns(() => 8) 28 | 29 | const chainlinkPriceFeedFactory2 = await ethers.getContractFactory("ChainlinkPriceFeed") 30 | const chainlinkPriceFeed2 = (await chainlinkPriceFeedFactory2.deploy(aggregator2.address)) as ChainlinkPriceFeed 31 | 32 | return { chainlinkPriceFeed, aggregator, chainlinkPriceFeed2, aggregator2 } 33 | } 34 | 35 | describe("ChainlinkPriceFeed Spec", () => { 36 | const [admin] = waffle.provider.getWallets() 37 | const loadFixture: ReturnType = waffle.createFixtureLoader([admin]) 38 | let chainlinkPriceFeed: ChainlinkPriceFeed 39 | let aggregator: MockContract 40 | let priceFeedDecimals: number 41 | let chainlinkPriceFeed2: ChainlinkPriceFeed 42 | let aggregator2: MockContract 43 | let priceFeedDecimals2: number 44 | 45 | beforeEach(async () => { 46 | const _fixture = await loadFixture(chainlinkPriceFeedFixture) 47 | chainlinkPriceFeed = _fixture.chainlinkPriceFeed 48 | aggregator = _fixture.aggregator 49 | priceFeedDecimals = await chainlinkPriceFeed.decimals() 50 | chainlinkPriceFeed2 = _fixture.chainlinkPriceFeed2 51 | aggregator2 = _fixture.aggregator2 52 | priceFeedDecimals2 = await chainlinkPriceFeed2.decimals() 53 | }) 54 | 55 | describe("twap edge cases, have the same timestamp for several rounds", () => { 56 | let currentTime: number 57 | let roundData: any[] 58 | 59 | beforeEach(async () => { 60 | // `base` = now - _interval 61 | // aggregator's answer 62 | // timestamp(base + 0) : 400 63 | // timestamp(base + 15) : 405 64 | // timestamp(base + 30) : 410 65 | // now = base + 45 66 | // 67 | // --+------+-----+-----+-----+-----+-----+ 68 | // base now 69 | currentTime = (await waffle.provider.getBlock("latest")).timestamp 70 | 71 | roundData = [ 72 | // [roundId, answer, startedAt, updatedAt, answeredInRound] 73 | ] 74 | 75 | // have the same timestamp for rounds 76 | roundData.push([0, parseEther("400"), currentTime, currentTime, 0]) 77 | roundData.push([1, parseEther("405"), currentTime, currentTime, 1]) 78 | roundData.push([2, parseEther("410"), currentTime, currentTime, 2]) 79 | 80 | aggregator.latestRoundData.returns(() => { 81 | return roundData[roundData.length - 1] 82 | }) 83 | aggregator.getRoundData.returns(round => { 84 | return roundData[round] 85 | }) 86 | 87 | currentTime += 15 88 | await ethers.provider.send("evm_setNextBlockTimestamp", [currentTime]) 89 | await ethers.provider.send("evm_mine", []) 90 | }) 91 | 92 | it("get the latest price", async () => { 93 | const price = await chainlinkPriceFeed.getPrice(45) 94 | expect(price).to.eq(parseEther("410")) 95 | }) 96 | 97 | it("asking interval more than aggregator has", async () => { 98 | const price = await chainlinkPriceFeed.getPrice(46) 99 | expect(price).to.eq(parseEther("410")) 100 | }) 101 | }) 102 | 103 | describe("twap", () => { 104 | let currentTime: number 105 | let roundData: any[] 106 | 107 | beforeEach(async () => { 108 | // `base` = now - _interval 109 | // aggregator's answer 110 | // timestamp(base + 0) : 400 111 | // timestamp(base + 15) : 405 112 | // timestamp(base + 30) : 410 113 | // now = base + 45 114 | // 115 | // --+------+-----+-----+-----+-----+-----+ 116 | // base now 117 | currentTime = (await waffle.provider.getBlock("latest")).timestamp 118 | 119 | roundData = [ 120 | // [roundId, answer, startedAt, updatedAt, answeredInRound] 121 | ] 122 | 123 | currentTime += 0 124 | roundData.push([0, parseEther("400"), currentTime, currentTime, 0]) 125 | 126 | currentTime += 15 127 | roundData.push([1, parseEther("405"), currentTime, currentTime, 1]) 128 | 129 | currentTime += 15 130 | roundData.push([2, parseEther("410"), currentTime, currentTime, 2]) 131 | 132 | aggregator.latestRoundData.returns(() => { 133 | return roundData[roundData.length - 1] 134 | }) 135 | aggregator.getRoundData.returns(round => { 136 | return roundData[round] 137 | }) 138 | 139 | currentTime += 15 140 | await ethers.provider.send("evm_setNextBlockTimestamp", [currentTime]) 141 | await ethers.provider.send("evm_mine", []) 142 | }) 143 | 144 | it("twap price", async () => { 145 | const price = await chainlinkPriceFeed.getPrice(45) 146 | expect(price).to.eq(parseEther("405")) 147 | }) 148 | 149 | it("asking interval more than aggregator has", async () => { 150 | const price = await chainlinkPriceFeed.getPrice(46) 151 | expect(price).to.eq(parseEther("405")) 152 | }) 153 | 154 | it("asking interval less than aggregator has", async () => { 155 | const price = await chainlinkPriceFeed.getPrice(44) 156 | expect(price).to.eq("405113636363636363636") 157 | }) 158 | 159 | it("given variant price period", async () => { 160 | roundData.push([4, parseEther("420"), currentTime + 30, currentTime + 30, 4]) 161 | await ethers.provider.send("evm_setNextBlockTimestamp", [currentTime + 50]) 162 | await ethers.provider.send("evm_mine", []) 163 | // twap price should be ((400 * 15) + (405 * 15) + (410 * 45) + (420 * 20)) / 95 = 409.736 164 | const price = await chainlinkPriceFeed.getPrice(95) 165 | expect(price).to.eq("409736842105263157894") 166 | }) 167 | 168 | it("latest price update time is earlier than the request, return the latest price", async () => { 169 | await ethers.provider.send("evm_setNextBlockTimestamp", [currentTime + 100]) 170 | await ethers.provider.send("evm_mine", []) 171 | 172 | // latest update time is base + 30, but now is base + 145 and asking for (now - 45) 173 | // should return the latest price directly 174 | const price = await chainlinkPriceFeed.getPrice(45) 175 | expect(price).to.eq(parseEther("410")) 176 | }) 177 | 178 | it("if current price < 0, ignore the current price", async () => { 179 | roundData.push([3, parseEther("-10"), 250, 250, 3]) 180 | const price = await chainlinkPriceFeed.getPrice(45) 181 | expect(price).to.eq(parseEther("405")) 182 | }) 183 | 184 | it("if there is a negative price in the middle, ignore that price", async () => { 185 | roundData.push([3, parseEther("-100"), currentTime + 20, currentTime + 20, 3]) 186 | roundData.push([4, parseEther("420"), currentTime + 30, currentTime + 30, 4]) 187 | await ethers.provider.send("evm_setNextBlockTimestamp", [currentTime + 50]) 188 | await ethers.provider.send("evm_mine", []) 189 | 190 | // twap price should be ((400 * 15) + (405 * 15) + (410 * 45) + (420 * 20)) / 95 = 409.736 191 | const price = await chainlinkPriceFeed.getPrice(95) 192 | expect(price).to.eq("409736842105263157894") 193 | }) 194 | 195 | it("return latest price if interval is zero", async () => { 196 | const price = await chainlinkPriceFeed.getPrice(0) 197 | expect(price).to.eq(parseEther("410")) 198 | }) 199 | }) 200 | 201 | describe("getRoundData", async () => { 202 | let currentTime: number 203 | 204 | beforeEach(async () => { 205 | currentTime = (await waffle.provider.getBlock("latest")).timestamp 206 | 207 | await aggregator2.setRoundData( 208 | computeRoundId(1, 1), 209 | parseUnits("1800", priceFeedDecimals2), 210 | BigNumber.from(currentTime), 211 | BigNumber.from(currentTime), 212 | computeRoundId(1, 1), 213 | ) 214 | await aggregator2.setRoundData( 215 | computeRoundId(1, 2), 216 | parseUnits("1900", priceFeedDecimals2), 217 | BigNumber.from(currentTime + 15), 218 | BigNumber.from(currentTime + 15), 219 | computeRoundId(1, 2), 220 | ) 221 | await aggregator2.setRoundData( 222 | computeRoundId(2, 10000), 223 | parseUnits("1700", priceFeedDecimals2), 224 | BigNumber.from(currentTime + 30), 225 | BigNumber.from(currentTime + 30), 226 | computeRoundId(2, 10000), 227 | ) 228 | 229 | // updatedAt is 0 means the round is not complete and should not be used 230 | await aggregator2.setRoundData( 231 | computeRoundId(2, 20000), 232 | parseUnits("-0.1", priceFeedDecimals2), 233 | BigNumber.from(currentTime + 45), 234 | BigNumber.from(0), 235 | computeRoundId(2, 20000), 236 | ) 237 | 238 | // updatedAt is 0 means the round is not complete and should not be used 239 | await aggregator2.setRoundData( 240 | computeRoundId(2, 20001), 241 | parseUnits("5000", priceFeedDecimals2), 242 | BigNumber.from(currentTime + 45), 243 | BigNumber.from(0), 244 | computeRoundId(2, 20001), 245 | ) 246 | }) 247 | 248 | it("computeRoundId", async () => { 249 | expect(computeRoundId(1, 1)).to.be.eq(await aggregator2.computeRoundId(1, 1)) 250 | expect(computeRoundId(1, 2)).to.be.eq(await aggregator2.computeRoundId(1, 2)) 251 | expect(computeRoundId(2, 10000)).to.be.eq(await aggregator2.computeRoundId(2, 10000)) 252 | }) 253 | 254 | it("getRoundData with valid roundId", async () => { 255 | expect(await chainlinkPriceFeed2.getRoundData(computeRoundId(1, 1))).to.be.deep.eq([ 256 | parseUnits("1800", priceFeedDecimals2), 257 | BigNumber.from(currentTime), 258 | ]) 259 | 260 | expect(await chainlinkPriceFeed2.getRoundData(computeRoundId(1, 2))).to.be.deep.eq([ 261 | parseUnits("1900", priceFeedDecimals2), 262 | BigNumber.from(currentTime + 15), 263 | ]) 264 | 265 | expect(await chainlinkPriceFeed2.getRoundData(computeRoundId(2, 10000))).to.be.deep.eq([ 266 | parseUnits("1700", priceFeedDecimals2), 267 | BigNumber.from(currentTime + 30), 268 | ]) 269 | }) 270 | 271 | it("force error, getRoundData when price <= 0", async () => { 272 | // price < 0 273 | await expect(chainlinkPriceFeed2.getRoundData(computeRoundId(2, 20000))).to.be.revertedWith("CPF_IP") 274 | 275 | // price = 0 276 | await expect(chainlinkPriceFeed2.getRoundData("123")).to.be.revertedWith("CPF_IP") 277 | }) 278 | 279 | it("force error, getRoundData when round is not complete", async () => { 280 | await expect(chainlinkPriceFeed2.getRoundData(computeRoundId(2, 20001))).to.be.revertedWith("CPF_RINC") 281 | }) 282 | }) 283 | 284 | it("getAggregator", async () => { 285 | expect(await chainlinkPriceFeed2.getAggregator()).to.be.eq(aggregator2.address) 286 | }) 287 | }) 288 | -------------------------------------------------------------------------------- /test/ChainlinkPriceFeedV1R1.spec.ts: -------------------------------------------------------------------------------- 1 | import { MockContract, smock } from "@defi-wonderland/smock" 2 | import { expect } from "chai" 3 | import { BigNumber } from "ethers" 4 | import { parseEther, parseUnits } from "ethers/lib/utils" 5 | import { ethers, waffle } from "hardhat" 6 | import { ChainlinkPriceFeedV1R1, TestAggregatorV3, TestAggregatorV3__factory } from "../typechain" 7 | import { computeRoundId } from "./shared/chainlink" 8 | 9 | interface ChainlinkPriceFeedFixture { 10 | chainlinkPriceFeedV1R1: ChainlinkPriceFeedV1R1 11 | aggregator: MockContract 12 | chainlinkPriceFeedV1R1_2: ChainlinkPriceFeedV1R1 13 | aggregator2: MockContract 14 | sequencerUptimeFeed: MockContract 15 | } 16 | 17 | async function chainlinkPriceFeedV1R1Fixture(): Promise { 18 | const [admin] = await ethers.getSigners() 19 | const aggregatorFactory = await smock.mock("TestAggregatorV3", admin) 20 | const aggregator = await aggregatorFactory.deploy() 21 | aggregator.decimals.returns(() => 18) 22 | 23 | const sequencerUptimeFeed = await aggregatorFactory.deploy() 24 | 25 | const chainlinkPriceFeedV1R1Factory = await ethers.getContractFactory("ChainlinkPriceFeedV1R1") 26 | const chainlinkPriceFeedV1R1 = (await chainlinkPriceFeedV1R1Factory.deploy( 27 | aggregator.address, 28 | sequencerUptimeFeed.address, 29 | )) as ChainlinkPriceFeedV1R1 30 | 31 | const aggregatorFactory2 = await smock.mock("TestAggregatorV3", admin) 32 | const aggregator2 = await aggregatorFactory2.deploy() 33 | aggregator2.decimals.returns(() => 8) 34 | 35 | const chainlinkPriceFeedV1R1Factory2 = await ethers.getContractFactory("ChainlinkPriceFeedV1R1") 36 | const chainlinkPriceFeedV1R1_2 = (await chainlinkPriceFeedV1R1Factory2.deploy( 37 | aggregator2.address, 38 | sequencerUptimeFeed.address, 39 | )) as ChainlinkPriceFeedV1R1 40 | 41 | return { chainlinkPriceFeedV1R1, aggregator, chainlinkPriceFeedV1R1_2, aggregator2, sequencerUptimeFeed } 42 | } 43 | 44 | describe("ChainlinkPriceFeedV1R1 Spec", () => { 45 | const [admin] = waffle.provider.getWallets() 46 | const loadFixture: ReturnType = waffle.createFixtureLoader([admin]) 47 | let chainlinkPriceFeed: ChainlinkPriceFeedV1R1 48 | let aggregator: MockContract 49 | let priceFeedDecimals: number 50 | let chainlinkPriceFeed2: ChainlinkPriceFeedV1R1 51 | let aggregator2: MockContract 52 | let priceFeedDecimals2: number 53 | let sequencerUptimeFeed: MockContract 54 | 55 | beforeEach(async () => { 56 | const _fixture = await loadFixture(chainlinkPriceFeedV1R1Fixture) 57 | chainlinkPriceFeed = _fixture.chainlinkPriceFeedV1R1 58 | aggregator = _fixture.aggregator 59 | priceFeedDecimals = await chainlinkPriceFeed.decimals() 60 | chainlinkPriceFeed2 = _fixture.chainlinkPriceFeedV1R1_2 61 | aggregator2 = _fixture.aggregator2 62 | priceFeedDecimals2 = await chainlinkPriceFeed2.decimals() 63 | sequencerUptimeFeed = _fixture.sequencerUptimeFeed 64 | }) 65 | 66 | describe("twap edge cases, have the same timestamp for several rounds", () => { 67 | let currentTime: number 68 | let roundData: any[] 69 | 70 | beforeEach(async () => { 71 | // `base` = now - _interval 72 | // aggregator's answer 73 | // timestamp(base + 0) : 400 74 | // timestamp(base + 15) : 405 75 | // timestamp(base + 30) : 410 76 | // now = base + 45 77 | // 78 | // --+------+-----+-----+-----+-----+-----+ 79 | // base now 80 | currentTime = (await waffle.provider.getBlock("latest")).timestamp 81 | 82 | roundData = [ 83 | // [roundId, answer, startedAt, updatedAt, answeredInRound] 84 | ] 85 | 86 | // have the same timestamp for rounds 87 | roundData.push([0, parseEther("400"), currentTime, currentTime, 0]) 88 | roundData.push([1, parseEther("405"), currentTime, currentTime, 1]) 89 | roundData.push([2, parseEther("410"), currentTime, currentTime, 2]) 90 | 91 | aggregator.latestRoundData.returns(() => { 92 | return roundData[roundData.length - 1] 93 | }) 94 | aggregator.getRoundData.returns(round => { 95 | return roundData[round] 96 | }) 97 | 98 | sequencerUptimeFeed.latestRoundData.returns(() => { 99 | // [roundId, answer, startedAt, updatedAt, answeredInRound] 100 | // Set startedAt before current time - GRACE_PERIOD_TIME so it passes the check. 101 | return [0, 0, currentTime - 4000, currentTime, 0] 102 | }) 103 | 104 | currentTime += 15 105 | await ethers.provider.send("evm_setNextBlockTimestamp", [currentTime]) 106 | await ethers.provider.send("evm_mine", []) 107 | }) 108 | 109 | it("get the latest price", async () => { 110 | const price = await chainlinkPriceFeed.getPrice(45) 111 | expect(price).to.eq(parseEther("410")) 112 | }) 113 | 114 | it("asking interval more than aggregator has", async () => { 115 | const price = await chainlinkPriceFeed.getPrice(46) 116 | expect(price).to.eq(parseEther("410")) 117 | }) 118 | }) 119 | 120 | describe("twap", () => { 121 | let currentTime: number 122 | let roundData: any[] 123 | 124 | beforeEach(async () => { 125 | // `base` = now - _interval 126 | // aggregator's answer 127 | // timestamp(base + 0) : 400 128 | // timestamp(base + 15) : 405 129 | // timestamp(base + 30) : 410 130 | // now = base + 45 131 | // 132 | // --+------+-----+-----+-----+-----+-----+ 133 | // base now 134 | currentTime = (await waffle.provider.getBlock("latest")).timestamp 135 | 136 | roundData = [ 137 | // [roundId, answer, startedAt, updatedAt, answeredInRound] 138 | ] 139 | 140 | currentTime += 0 141 | roundData.push([0, parseEther("400"), currentTime, currentTime, 0]) 142 | 143 | currentTime += 15 144 | roundData.push([1, parseEther("405"), currentTime, currentTime, 1]) 145 | 146 | currentTime += 15 147 | roundData.push([2, parseEther("410"), currentTime, currentTime, 2]) 148 | 149 | aggregator.latestRoundData.returns(() => { 150 | return roundData[roundData.length - 1] 151 | }) 152 | aggregator.getRoundData.returns(round => { 153 | return roundData[round] 154 | }) 155 | 156 | sequencerUptimeFeed.latestRoundData.returns(() => { 157 | // [roundId, answer, startedAt, updatedAt, answeredInRound] 158 | // Set startedAt before current time - GRACE_PERIOD_TIME so it passes the check. 159 | return [0, 0, currentTime - 4000, currentTime, 0] 160 | }) 161 | 162 | currentTime += 15 163 | await ethers.provider.send("evm_setNextBlockTimestamp", [currentTime]) 164 | await ethers.provider.send("evm_mine", []) 165 | }) 166 | 167 | it("twap price", async () => { 168 | const price = await chainlinkPriceFeed.getPrice(45) 169 | expect(price).to.eq(parseEther("405")) 170 | }) 171 | 172 | it("asking interval more than aggregator has", async () => { 173 | const price = await chainlinkPriceFeed.getPrice(46) 174 | expect(price).to.eq(parseEther("405")) 175 | }) 176 | 177 | it("asking interval less than aggregator has", async () => { 178 | const price = await chainlinkPriceFeed.getPrice(44) 179 | expect(price).to.eq("405113636363636363636") 180 | }) 181 | 182 | it("given variant price period", async () => { 183 | roundData.push([4, parseEther("420"), currentTime + 30, currentTime + 30, 4]) 184 | await ethers.provider.send("evm_setNextBlockTimestamp", [currentTime + 50]) 185 | await ethers.provider.send("evm_mine", []) 186 | // twap price should be ((400 * 15) + (405 * 15) + (410 * 45) + (420 * 20)) / 95 = 409.736 187 | const price = await chainlinkPriceFeed.getPrice(95) 188 | expect(price).to.eq("409736842105263157894") 189 | }) 190 | 191 | it("latest price update time is earlier than the request, return the latest price", async () => { 192 | await ethers.provider.send("evm_setNextBlockTimestamp", [currentTime + 100]) 193 | await ethers.provider.send("evm_mine", []) 194 | 195 | // latest update time is base + 30, but now is base + 145 and asking for (now - 45) 196 | // should return the latest price directly 197 | const price = await chainlinkPriceFeed.getPrice(45) 198 | expect(price).to.eq(parseEther("410")) 199 | }) 200 | 201 | it("if current price < 0, ignore the current price", async () => { 202 | roundData.push([3, parseEther("-10"), 250, 250, 3]) 203 | const price = await chainlinkPriceFeed.getPrice(45) 204 | expect(price).to.eq(parseEther("405")) 205 | }) 206 | 207 | it("if there is a negative price in the middle, ignore that price", async () => { 208 | roundData.push([3, parseEther("-100"), currentTime + 20, currentTime + 20, 3]) 209 | roundData.push([4, parseEther("420"), currentTime + 30, currentTime + 30, 4]) 210 | await ethers.provider.send("evm_setNextBlockTimestamp", [currentTime + 50]) 211 | await ethers.provider.send("evm_mine", []) 212 | 213 | // twap price should be ((400 * 15) + (405 * 15) + (410 * 45) + (420 * 20)) / 95 = 409.736 214 | const price = await chainlinkPriceFeed.getPrice(95) 215 | expect(price).to.eq("409736842105263157894") 216 | }) 217 | 218 | it("return latest price if interval is zero", async () => { 219 | const price = await chainlinkPriceFeed.getPrice(0) 220 | expect(price).to.eq(parseEther("410")) 221 | }) 222 | }) 223 | 224 | describe("getRoundData", async () => { 225 | let currentTime: number 226 | 227 | beforeEach(async () => { 228 | currentTime = (await waffle.provider.getBlock("latest")).timestamp 229 | 230 | await aggregator2.setRoundData( 231 | computeRoundId(1, 1), 232 | parseUnits("1800", priceFeedDecimals2), 233 | BigNumber.from(currentTime), 234 | BigNumber.from(currentTime), 235 | computeRoundId(1, 1), 236 | ) 237 | await aggregator2.setRoundData( 238 | computeRoundId(1, 2), 239 | parseUnits("1900", priceFeedDecimals2), 240 | BigNumber.from(currentTime + 15), 241 | BigNumber.from(currentTime + 15), 242 | computeRoundId(1, 2), 243 | ) 244 | await aggregator2.setRoundData( 245 | computeRoundId(2, 10000), 246 | parseUnits("1700", priceFeedDecimals2), 247 | BigNumber.from(currentTime + 30), 248 | BigNumber.from(currentTime + 30), 249 | computeRoundId(2, 10000), 250 | ) 251 | 252 | // updatedAt is 0 means the round is not complete and should not be used 253 | await aggregator2.setRoundData( 254 | computeRoundId(2, 20000), 255 | parseUnits("-0.1", priceFeedDecimals2), 256 | BigNumber.from(currentTime + 45), 257 | BigNumber.from(0), 258 | computeRoundId(2, 20000), 259 | ) 260 | 261 | // updatedAt is 0 means the round is not complete and should not be used 262 | await aggregator2.setRoundData( 263 | computeRoundId(2, 20001), 264 | parseUnits("5000", priceFeedDecimals2), 265 | BigNumber.from(currentTime + 45), 266 | BigNumber.from(0), 267 | computeRoundId(2, 20001), 268 | ) 269 | }) 270 | 271 | it("computeRoundId", async () => { 272 | expect(computeRoundId(1, 1)).to.be.eq(await aggregator2.computeRoundId(1, 1)) 273 | expect(computeRoundId(1, 2)).to.be.eq(await aggregator2.computeRoundId(1, 2)) 274 | expect(computeRoundId(2, 10000)).to.be.eq(await aggregator2.computeRoundId(2, 10000)) 275 | }) 276 | 277 | it("getRoundData with valid roundId", async () => { 278 | expect(await chainlinkPriceFeed2.getRoundData(computeRoundId(1, 1))).to.be.deep.eq([ 279 | parseUnits("1800", priceFeedDecimals2), 280 | BigNumber.from(currentTime), 281 | ]) 282 | 283 | expect(await chainlinkPriceFeed2.getRoundData(computeRoundId(1, 2))).to.be.deep.eq([ 284 | parseUnits("1900", priceFeedDecimals2), 285 | BigNumber.from(currentTime + 15), 286 | ]) 287 | 288 | expect(await chainlinkPriceFeed2.getRoundData(computeRoundId(2, 10000))).to.be.deep.eq([ 289 | parseUnits("1700", priceFeedDecimals2), 290 | BigNumber.from(currentTime + 30), 291 | ]) 292 | }) 293 | 294 | it("force error, getRoundData when price <= 0", async () => { 295 | // price < 0 296 | await expect(chainlinkPriceFeed2.getRoundData(computeRoundId(2, 20000))).to.be.revertedWith("CPF_IP") 297 | 298 | // price = 0 299 | await expect(chainlinkPriceFeed2.getRoundData("123")).to.be.revertedWith("CPF_IP") 300 | }) 301 | 302 | it("force error, getRoundData when round is not complete", async () => { 303 | await expect(chainlinkPriceFeed2.getRoundData(computeRoundId(2, 20001))).to.be.revertedWith("CPF_RINC") 304 | }) 305 | }) 306 | 307 | it("getAggregator", async () => { 308 | expect(await chainlinkPriceFeed2.getAggregator()).to.be.eq(aggregator2.address) 309 | }) 310 | 311 | it("getSequencerUptimeFeed", async () => { 312 | expect(await chainlinkPriceFeed2.getSequencerUptimeFeed()).to.be.eq(sequencerUptimeFeed.address) 313 | }) 314 | 315 | describe("sequencer status check", async () => { 316 | let currentTime 317 | beforeEach(async () => { 318 | currentTime = (await waffle.provider.getBlock("latest")).timestamp 319 | }) 320 | 321 | it("force error, sequencer status is DOWN", async () => { 322 | sequencerUptimeFeed.latestRoundData.returns(() => { 323 | // [roundId, answer, startedAt, updatedAt, answeredInRound] 324 | return [0, 1, currentTime, currentTime, 0] 325 | }) 326 | await expect(chainlinkPriceFeed.getPrice(30)).to.be.revertedWith("CPF_SD") 327 | }) 328 | 329 | it("force, error, sequencer uptime duration is less than GRACE_PERIOD_TIME", async () => { 330 | sequencerUptimeFeed.latestRoundData.returns(() => { 331 | return [0, 0, currentTime - 1800, currentTime, 0] 332 | }) 333 | await expect(chainlinkPriceFeed.getPrice(30)).to.be.revertedWith("CPF_GPNO") 334 | }) 335 | 336 | it("return latest price when sequencer is up and ready", async () => { 337 | aggregator.latestRoundData.returns(() => { 338 | return [0, parseEther("1000"), currentTime - 50, currentTime - 50, 0] 339 | }) 340 | 341 | sequencerUptimeFeed.latestRoundData.returns(() => { 342 | // Set startedAt before current time - GRACE_PERIOD_TIME so it passes the check. 343 | return [0, 0, currentTime - 4000, currentTime, 0] 344 | }) 345 | 346 | expect(await chainlinkPriceFeed.getPrice(30)).to.be.eq(parseEther("1000")) 347 | }) 348 | }) 349 | }) 350 | -------------------------------------------------------------------------------- /test/ChainlinkPriceFeedV2.spec.ts: -------------------------------------------------------------------------------- 1 | import { MockContract, smock } from "@defi-wonderland/smock" 2 | import { expect } from "chai" 3 | import { BigNumber } from "ethers" 4 | import { parseEther, parseUnits } from "ethers/lib/utils" 5 | import { ethers, waffle } from "hardhat" 6 | import { ChainlinkPriceFeedV2, TestAggregatorV3, TestAggregatorV3__factory } from "../typechain" 7 | import { computeRoundId } from "./shared/chainlink" 8 | 9 | interface ChainlinkPriceFeedFixture { 10 | chainlinkPriceFeed: ChainlinkPriceFeedV2 11 | aggregator: MockContract 12 | chainlinkPriceFeed2: ChainlinkPriceFeedV2 13 | aggregator2: MockContract 14 | } 15 | 16 | async function chainlinkPriceFeedFixture(): Promise { 17 | const [admin] = await ethers.getSigners() 18 | const aggregatorFactory = await smock.mock("TestAggregatorV3", admin) 19 | const aggregator = await aggregatorFactory.deploy() 20 | aggregator.decimals.returns(() => 18) 21 | 22 | const chainlinkPriceFeedFactory = await ethers.getContractFactory("ChainlinkPriceFeedV2") 23 | const chainlinkPriceFeed = (await chainlinkPriceFeedFactory.deploy(aggregator.address, 900)) as ChainlinkPriceFeedV2 24 | 25 | const aggregatorFactory2 = await smock.mock("TestAggregatorV3", admin) 26 | const aggregator2 = await aggregatorFactory2.deploy() 27 | aggregator2.decimals.returns(() => 8) 28 | 29 | const chainlinkPriceFeedFactory2 = await ethers.getContractFactory("ChainlinkPriceFeedV2") 30 | const chainlinkPriceFeed2 = (await chainlinkPriceFeedFactory2.deploy( 31 | aggregator2.address, 32 | 900, 33 | )) as ChainlinkPriceFeedV2 34 | 35 | return { chainlinkPriceFeed, aggregator, chainlinkPriceFeed2, aggregator2 } 36 | } 37 | 38 | describe("ChainlinkPriceFeedV2 Spec", () => { 39 | const [admin] = waffle.provider.getWallets() 40 | const loadFixture: ReturnType = waffle.createFixtureLoader([admin]) 41 | let chainlinkPriceFeed: ChainlinkPriceFeedV2 42 | let aggregator: MockContract 43 | let priceFeedDecimals: number 44 | let chainlinkPriceFeed2: ChainlinkPriceFeedV2 45 | let aggregator2: MockContract 46 | let priceFeedDecimals2: number 47 | 48 | beforeEach(async () => { 49 | const _fixture = await loadFixture(chainlinkPriceFeedFixture) 50 | chainlinkPriceFeed = _fixture.chainlinkPriceFeed 51 | aggregator = _fixture.aggregator 52 | priceFeedDecimals = await chainlinkPriceFeed.decimals() 53 | chainlinkPriceFeed2 = _fixture.chainlinkPriceFeed2 54 | aggregator2 = _fixture.aggregator2 55 | priceFeedDecimals2 = await chainlinkPriceFeed2.decimals() 56 | }) 57 | 58 | describe("edge cases, have the same timestamp for several rounds", () => { 59 | let currentTime: number 60 | let roundData: any[] 61 | 62 | async function updatePrice(index: number, price: number, forward: boolean = true): Promise { 63 | roundData.push([index, parseEther(price.toString()), currentTime, currentTime, index]) 64 | aggregator.latestRoundData.returns(() => { 65 | return roundData[roundData.length - 1] 66 | }) 67 | await chainlinkPriceFeed.update() 68 | 69 | if (forward) { 70 | currentTime += 15 71 | await ethers.provider.send("evm_setNextBlockTimestamp", [currentTime]) 72 | await ethers.provider.send("evm_mine", []) 73 | } 74 | } 75 | 76 | it("force error, can't update if timestamp is the same", async () => { 77 | currentTime = (await waffle.provider.getBlock("latest")).timestamp 78 | roundData = [ 79 | // [roundId, answer, startedAt, updatedAt, answeredInRound] 80 | ] 81 | // set first round data 82 | roundData.push([0, parseEther("399"), currentTime, currentTime, 0]) 83 | aggregator.latestRoundData.returns(() => { 84 | return roundData[roundData.length - 1] 85 | }) 86 | 87 | // update without forward timestamp 88 | await updatePrice(0, 400, false) 89 | await expect(chainlinkPriceFeed.update()).to.be.revertedWith("CPF_NU") 90 | }) 91 | }) 92 | 93 | describe("getRoundData", async () => { 94 | let currentTime: number 95 | 96 | beforeEach(async () => { 97 | currentTime = (await waffle.provider.getBlock("latest")).timestamp 98 | 99 | await aggregator2.setRoundData( 100 | computeRoundId(1, 1), 101 | parseUnits("1800", priceFeedDecimals2), 102 | BigNumber.from(currentTime), 103 | BigNumber.from(currentTime), 104 | computeRoundId(1, 1), 105 | ) 106 | await aggregator2.setRoundData( 107 | computeRoundId(1, 2), 108 | parseUnits("1900", priceFeedDecimals2), 109 | BigNumber.from(currentTime + 15), 110 | BigNumber.from(currentTime + 15), 111 | computeRoundId(1, 2), 112 | ) 113 | await aggregator2.setRoundData( 114 | computeRoundId(2, 10000), 115 | parseUnits("1700", priceFeedDecimals2), 116 | BigNumber.from(currentTime + 30), 117 | BigNumber.from(currentTime + 30), 118 | computeRoundId(2, 10000), 119 | ) 120 | 121 | // updatedAt is 0 means the round is not complete and should not be used 122 | await aggregator2.setRoundData( 123 | computeRoundId(2, 20000), 124 | parseUnits("-0.1", priceFeedDecimals2), 125 | BigNumber.from(currentTime + 45), 126 | BigNumber.from(0), 127 | computeRoundId(2, 20000), 128 | ) 129 | 130 | // updatedAt is 0 means the round is not complete and should not be used 131 | await aggregator2.setRoundData( 132 | computeRoundId(2, 20001), 133 | parseUnits("5000", priceFeedDecimals2), 134 | BigNumber.from(currentTime + 45), 135 | BigNumber.from(0), 136 | computeRoundId(2, 20001), 137 | ) 138 | }) 139 | 140 | it("computeRoundId", async () => { 141 | expect(computeRoundId(1, 1)).to.be.eq(await aggregator2.computeRoundId(1, 1)) 142 | expect(computeRoundId(1, 2)).to.be.eq(await aggregator2.computeRoundId(1, 2)) 143 | expect(computeRoundId(2, 10000)).to.be.eq(await aggregator2.computeRoundId(2, 10000)) 144 | }) 145 | 146 | it("getRoundData with valid roundId", async () => { 147 | expect(await chainlinkPriceFeed2.getRoundData(computeRoundId(1, 1))).to.be.deep.eq([ 148 | parseUnits("1800", priceFeedDecimals2), 149 | BigNumber.from(currentTime), 150 | ]) 151 | 152 | expect(await chainlinkPriceFeed2.getRoundData(computeRoundId(1, 2))).to.be.deep.eq([ 153 | parseUnits("1900", priceFeedDecimals2), 154 | BigNumber.from(currentTime + 15), 155 | ]) 156 | 157 | expect(await chainlinkPriceFeed2.getRoundData(computeRoundId(2, 10000))).to.be.deep.eq([ 158 | parseUnits("1700", priceFeedDecimals2), 159 | BigNumber.from(currentTime + 30), 160 | ]) 161 | }) 162 | 163 | it("force error, getRoundData when price <= 0", async () => { 164 | // price < 0 165 | await expect(chainlinkPriceFeed2.getRoundData(computeRoundId(2, 20000))).to.be.revertedWith("CPF_IP") 166 | 167 | // price = 0 168 | await expect(chainlinkPriceFeed2.getRoundData("123")).to.be.revertedWith("CPF_IP") 169 | }) 170 | 171 | it("force error, getRoundData when round is not complete", async () => { 172 | await expect(chainlinkPriceFeed2.getRoundData(computeRoundId(2, 20001))).to.be.revertedWith("CPF_RINC") 173 | }) 174 | }) 175 | 176 | it("getAggregator", async () => { 177 | expect(await chainlinkPriceFeed2.getAggregator()).to.be.eq(aggregator2.address) 178 | }) 179 | }) 180 | -------------------------------------------------------------------------------- /test/PriceFeed.gas.test.ts: -------------------------------------------------------------------------------- 1 | import { parseEther } from "ethers/lib/utils" 2 | import { ethers, waffle } from "hardhat" 3 | import { BandPriceFeed, ChainlinkPriceFeedV2, TestAggregatorV3, TestPriceFeedV2, TestStdReference } from "../typechain" 4 | 5 | const twapInterval = 900 6 | interface PriceFeedFixture { 7 | bandPriceFeed: BandPriceFeed 8 | bandReference: TestStdReference 9 | baseAsset: string 10 | 11 | chainlinkPriceFeed: ChainlinkPriceFeedV2 12 | aggregator: TestAggregatorV3 13 | } 14 | 15 | async function priceFeedFixture(): Promise { 16 | // band protocol 17 | const testStdReferenceFactory = await ethers.getContractFactory("TestStdReference") 18 | const testStdReference = await testStdReferenceFactory.deploy() 19 | 20 | const baseAsset = "ETH" 21 | const bandPriceFeedFactory = await ethers.getContractFactory("BandPriceFeed") 22 | const bandPriceFeed = (await bandPriceFeedFactory.deploy( 23 | testStdReference.address, 24 | baseAsset, 25 | twapInterval, 26 | )) as BandPriceFeed 27 | 28 | // chainlink 29 | const testAggregatorFactory = await ethers.getContractFactory("TestAggregatorV3") 30 | const testAggregator = await testAggregatorFactory.deploy() 31 | 32 | const chainlinkPriceFeedFactory = await ethers.getContractFactory("ChainlinkPriceFeedV2") 33 | const chainlinkPriceFeed = (await chainlinkPriceFeedFactory.deploy( 34 | testAggregator.address, 35 | twapInterval, 36 | )) as ChainlinkPriceFeedV2 37 | 38 | return { bandPriceFeed, bandReference: testStdReference, baseAsset, chainlinkPriceFeed, aggregator: testAggregator } 39 | } 40 | 41 | describe.skip("Price feed gas test", () => { 42 | const [admin] = waffle.provider.getWallets() 43 | const loadFixture: ReturnType = waffle.createFixtureLoader([admin]) 44 | let bandPriceFeed: BandPriceFeed 45 | let bandReference: TestStdReference 46 | let chainlinkPriceFeed: ChainlinkPriceFeedV2 47 | let aggregator: TestAggregatorV3 48 | let currentTime: number 49 | let testPriceFeed: TestPriceFeedV2 50 | let beginPrice = 400 51 | let round: number 52 | 53 | async function updatePrice(price: number, forward: boolean = true): Promise { 54 | await bandReference.setReferenceData({ 55 | rate: parseEther(price.toString()), 56 | lastUpdatedBase: currentTime, 57 | lastUpdatedQuote: currentTime, 58 | }) 59 | await bandPriceFeed.update() 60 | 61 | await aggregator.setRoundData(round, parseEther(price.toString()), currentTime, currentTime, round) 62 | await chainlinkPriceFeed.update() 63 | 64 | if (forward) { 65 | currentTime += 15 66 | await ethers.provider.send("evm_setNextBlockTimestamp", [currentTime]) 67 | await ethers.provider.send("evm_mine", []) 68 | } 69 | } 70 | 71 | before(async () => { 72 | const _fixture = await loadFixture(priceFeedFixture) 73 | bandReference = _fixture.bandReference 74 | bandPriceFeed = _fixture.bandPriceFeed 75 | chainlinkPriceFeed = _fixture.chainlinkPriceFeed 76 | aggregator = _fixture.aggregator 77 | round = 0 78 | 79 | const TestPriceFeedFactory = await ethers.getContractFactory("TestPriceFeedV2") 80 | testPriceFeed = (await TestPriceFeedFactory.deploy( 81 | chainlinkPriceFeed.address, 82 | bandPriceFeed.address, 83 | )) as TestPriceFeedV2 84 | 85 | currentTime = (await waffle.provider.getBlock("latest")).timestamp 86 | for (let i = 0; i < 255; i++) { 87 | round = i 88 | await updatePrice(beginPrice + i) 89 | } 90 | }) 91 | 92 | describe("900 seconds twapInterval", () => { 93 | it("band protocol ", async () => { 94 | await testPriceFeed.fetchBandProtocolPrice(twapInterval) 95 | }) 96 | 97 | it("band protocol - cached", async () => { 98 | await testPriceFeed.cachedBandProtocolPrice(twapInterval) 99 | }) 100 | 101 | it("chainlink", async () => { 102 | await testPriceFeed.fetchChainlinkPrice(twapInterval) 103 | }) 104 | 105 | it("chainlink - cached", async () => { 106 | await testPriceFeed.cachedChainlinkPrice(twapInterval) 107 | }) 108 | }) 109 | }) 110 | -------------------------------------------------------------------------------- /test/PriceFeedUpdater.spec.ts: -------------------------------------------------------------------------------- 1 | import { MockContract, smock } from "@defi-wonderland/smock" 2 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers" 3 | import chai, { expect } from "chai" 4 | import { parseEther } from "ethers/lib/utils" 5 | import { ethers, waffle } from "hardhat" 6 | import { 7 | ChainlinkPriceFeedV3, 8 | ChainlinkPriceFeedV3__factory, 9 | PriceFeedUpdater, 10 | TestAggregatorV3__factory, 11 | } from "../typechain" 12 | 13 | chai.use(smock.matchers) 14 | 15 | interface PriceFeedUpdaterFixture { 16 | ethPriceFeed: MockContract 17 | btcPriceFeed: MockContract 18 | priceFeedUpdater: PriceFeedUpdater 19 | admin: SignerWithAddress 20 | alice: SignerWithAddress 21 | } 22 | 23 | describe("PriceFeedUpdater Spec", () => { 24 | const loadFixture: ReturnType = waffle.createFixtureLoader() 25 | let fixture: PriceFeedUpdaterFixture 26 | 27 | beforeEach(async () => { 28 | fixture = await loadFixture(createFixture) 29 | }) 30 | afterEach(async () => { 31 | fixture.btcPriceFeed.update.reset() 32 | fixture.ethPriceFeed.update.reset() 33 | }) 34 | 35 | async function executeFallback(priceFeedUpdater: PriceFeedUpdater) { 36 | const { alice } = fixture 37 | await alice.sendTransaction({ 38 | to: priceFeedUpdater.address, 39 | value: 0, 40 | gasLimit: 150000, // Give gas limit to force run transaction without dry run 41 | }) 42 | } 43 | 44 | async function createFixture(): Promise { 45 | const [admin, alice] = await ethers.getSigners() 46 | 47 | const aggregatorFactory = await smock.mock("TestAggregatorV3", admin) 48 | const aggregator = await aggregatorFactory.deploy() 49 | 50 | const chainlinkPriceFeedV2Factory = await smock.mock( 51 | "ChainlinkPriceFeedV3", 52 | admin, 53 | ) 54 | const ethPriceFeed = await chainlinkPriceFeedV2Factory.deploy(aggregator.address, 40 * 60, 30 * 60) 55 | const btcPriceFeed = await chainlinkPriceFeedV2Factory.deploy(aggregator.address, 40 * 60, 30 * 60) 56 | 57 | await ethPriceFeed.deployed() 58 | await btcPriceFeed.deployed() 59 | 60 | const priceFeedUpdaterFactory = await ethers.getContractFactory("PriceFeedUpdater") 61 | const priceFeedUpdater = (await priceFeedUpdaterFactory.deploy([ 62 | ethPriceFeed.address, 63 | btcPriceFeed.address, 64 | ])) as PriceFeedUpdater 65 | 66 | return { ethPriceFeed, btcPriceFeed, priceFeedUpdater, admin, alice } 67 | } 68 | it("the result of getPriceFeeds should be same as priceFeeds given when deployment", async () => { 69 | const { ethPriceFeed, btcPriceFeed, priceFeedUpdater } = fixture 70 | const priceFeeds = await priceFeedUpdater.getPriceFeeds() 71 | expect(priceFeeds).deep.equals([ethPriceFeed.address, btcPriceFeed.address]) 72 | }) 73 | 74 | it("force error, when someone sent eth to contract", async () => { 75 | const { alice, priceFeedUpdater } = fixture 76 | const tx = alice.sendTransaction({ 77 | to: priceFeedUpdater.address, 78 | value: parseEther("0.1"), 79 | gasLimit: 150000, // Give gas limit to force run transaction without dry run 80 | }) 81 | await expect(tx).to.be.reverted 82 | }) 83 | 84 | describe("When priceFeedUpdater fallback execute", () => { 85 | it("should success if all priceFeed are updated successfully", async () => { 86 | const { ethPriceFeed, btcPriceFeed, priceFeedUpdater } = fixture 87 | 88 | await executeFallback(priceFeedUpdater) 89 | 90 | expect(ethPriceFeed.update).to.have.been.calledOnce 91 | expect(btcPriceFeed.update).to.have.been.calledOnce 92 | }) 93 | it("should still success if any one of priceFeed is updated fail", async () => { 94 | const { ethPriceFeed, btcPriceFeed, priceFeedUpdater } = fixture 95 | 96 | ethPriceFeed.update.reverts() 97 | await executeFallback(priceFeedUpdater) 98 | 99 | expect(ethPriceFeed.update).to.have.been.calledOnce 100 | expect(btcPriceFeed.update).to.have.been.calledOnce 101 | }) 102 | }) 103 | }) 104 | -------------------------------------------------------------------------------- /test/UniswapV3PriceFeed.spec.ts: -------------------------------------------------------------------------------- 1 | import { FakeContract, smock } from "@defi-wonderland/smock" 2 | import { expect } from "chai" 3 | import { BigNumber } from "ethers" 4 | import { parseEther } from "ethers/lib/utils" 5 | import { ethers, waffle } from "hardhat" 6 | import { UniswapV3Pool, UniswapV3PriceFeed } from "../typechain" 7 | 8 | interface UniswapV3PriceFeedFixture { 9 | uniswapV3PriceFeed: UniswapV3PriceFeed 10 | uniswapV3Pool: FakeContract 11 | } 12 | 13 | async function uniswapV3PriceFeedFixture(): Promise { 14 | const [admin] = await ethers.getSigners() 15 | const uniswapV3Pool = await smock.fake("UniswapV3Pool", admin) 16 | 17 | const uniswapV3PriceFeedFactory = await ethers.getContractFactory("UniswapV3PriceFeed") 18 | const uniswapV3PriceFeed = (await uniswapV3PriceFeedFactory.deploy(uniswapV3Pool.address)) as UniswapV3PriceFeed 19 | 20 | return { uniswapV3PriceFeed, uniswapV3Pool } 21 | } 22 | 23 | describe("UniswapV3PriceFeed Spec", () => { 24 | const [admin] = waffle.provider.getWallets() 25 | const loadFixture: ReturnType = waffle.createFixtureLoader([admin]) 26 | let uniswapV3PriceFeed: UniswapV3PriceFeed 27 | let uniswapV3Pool: FakeContract 28 | 29 | it("force error, pool address has to be a contract", async () => { 30 | const uniswapV3PriceFeedFactory = await ethers.getContractFactory("UniswapV3PriceFeed") 31 | await expect(uniswapV3PriceFeedFactory.deploy(admin.address)).to.be.revertedWith("UPF_PANC") 32 | }) 33 | 34 | describe("pool address is contract", () => { 35 | beforeEach(async () => { 36 | const _fixture = await loadFixture(uniswapV3PriceFeedFixture) 37 | uniswapV3PriceFeed = _fixture.uniswapV3PriceFeed 38 | uniswapV3Pool = _fixture.uniswapV3Pool 39 | }) 40 | 41 | describe("decimals()", () => { 42 | it("decimals should be 18", async () => { 43 | expect(await uniswapV3PriceFeed.decimals()).to.be.eq(18) 44 | }) 45 | }) 46 | 47 | describe("getPrice()", () => { 48 | it("twap", async () => { 49 | uniswapV3Pool.observe.returns([[BigNumber.from(0), BigNumber.from(82800000)], []]) 50 | // twapTick = (82800000-0) / 1800 = 46000 51 | // twap = 1.0001^46000 = 99.4614384055 52 | const indexPrice = await uniswapV3PriceFeed.getPrice() 53 | expect(indexPrice).to.be.eq(parseEther("99.461438405455592365")) 54 | }) 55 | }) 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /test/foundry/CachedTwap.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.7.6; 2 | 3 | import "forge-std/Test.sol"; 4 | import { CachedTwap } from "../../contracts/twap/CachedTwap.sol"; 5 | 6 | contract TestCachedTwap is CachedTwap { 7 | constructor(uint80 interval) CachedTwap(interval) {} 8 | 9 | function cacheTwap( 10 | uint256 interval, 11 | uint256 latestPrice, 12 | uint256 latestUpdatedTimestamp 13 | ) external returns (uint256 cachedTwap) { 14 | return _cacheTwap(interval, latestPrice, latestUpdatedTimestamp); 15 | } 16 | } 17 | 18 | contract CachedTwapTest is Test { 19 | uint80 internal constant _INTERVAL = 900; 20 | 21 | uint256 internal constant _INIT_BLOCK_TIMESTAMP = 1000; 22 | 23 | TestCachedTwap internal _testCachedTwap; 24 | 25 | function setUp() public { 26 | vm.warp(_INIT_BLOCK_TIMESTAMP); 27 | 28 | _testCachedTwap = new TestCachedTwap(_INTERVAL); 29 | } 30 | 31 | function test_cacheTwap_will_update_latestPrice_and_cachedTwap_when_valid_timestamp_and_price() public { 32 | // t1 t2 t3 33 | // -----+--------+--------+ 34 | // 1200s 1200s 35 | // price: 100 120 140 36 | uint256 cachedTwap; 37 | 38 | uint256 t1 = _INIT_BLOCK_TIMESTAMP; 39 | uint256 p1 = 100 * 1e8; 40 | 41 | cachedTwap = _testCachedTwap.cacheTwap(_INTERVAL, p1, t1); 42 | assertEq(cachedTwap, p1); 43 | 44 | uint256 t2 = t1 + 1200; 45 | uint256 p2 = 120 * 1e8; 46 | vm.warp(t2); 47 | 48 | cachedTwap = _testCachedTwap.cacheTwap(_INTERVAL, p2, t2); 49 | assertEq(cachedTwap, p1); 50 | 51 | uint256 t3 = t2 + 1200; 52 | uint256 p3 = 140 * 1e8; 53 | vm.warp(t3); 54 | 55 | cachedTwap = _testCachedTwap.cacheTwap(_INTERVAL, p3, t3); 56 | assertEq(cachedTwap, p2); 57 | } 58 | 59 | function test_cacheTwap_wont_update_latestPrice_but_update_cachedTwap_when_same_timestamp_and_price() public { 60 | // t1 t2 61 | // -----+--------+------ 62 | // 1200s 63 | // price: 100 120 64 | uint256 cachedTwap; 65 | 66 | uint256 t1 = _INIT_BLOCK_TIMESTAMP; 67 | uint256 p1 = 100 * 1e8; 68 | 69 | cachedTwap = _testCachedTwap.cacheTwap(_INTERVAL, p1, t1); 70 | assertEq(cachedTwap, p1); 71 | 72 | uint256 t2 = t1 + 1200; 73 | uint256 p2 = 120 * 1e8; 74 | vm.warp(t2); 75 | 76 | cachedTwap = _testCachedTwap.cacheTwap(_INTERVAL, p2, t2); 77 | assertEq(cachedTwap, p1); 78 | 79 | vm.warp(t2 + 1200); 80 | 81 | cachedTwap = _testCachedTwap.cacheTwap(_INTERVAL, p2, t2); 82 | assertEq(cachedTwap, p2); 83 | } 84 | 85 | function test_revert_cacheTwap_when_same_timestamp_and_different_price() public { 86 | // t1 t2 87 | // -----+--------+------ 88 | // 1200s 89 | // price: 100 120 90 | uint256 cachedTwap; 91 | 92 | uint256 t1 = _INIT_BLOCK_TIMESTAMP; 93 | uint256 p1 = 100 * 1e8; 94 | 95 | cachedTwap = _testCachedTwap.cacheTwap(_INTERVAL, p1, t1); 96 | assertEq(cachedTwap, p1); 97 | 98 | uint256 t2 = t1 + 1200; 99 | uint256 p2 = 120 * 1e8; 100 | vm.warp(t2); 101 | 102 | cachedTwap = _testCachedTwap.cacheTwap(_INTERVAL, p2, t2); 103 | assertEq(cachedTwap, p1); 104 | 105 | uint256 p3 = 140 * 1e8; 106 | vm.warp(t2 + 1200); 107 | 108 | vm.expectRevert(bytes("CT_IPWU")); 109 | _testCachedTwap.cacheTwap(_INTERVAL, p3, t2); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /test/foundry/ChainlinkPriceFeedV3.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.7.6; 2 | pragma abicoder v2; 3 | 4 | import "forge-std/Test.sol"; 5 | import "@openzeppelin/contracts/math/SafeMath.sol"; 6 | import "./Setup.sol"; 7 | import "../../contracts/interface/IChainlinkPriceFeedV3.sol"; 8 | import "../../contracts/test/TestAggregatorV3.sol"; 9 | import { CumulativeTwap } from "../../contracts/twap/CumulativeTwap.sol"; 10 | 11 | contract ChainlinkPriceFeedV3ConstructorTest is Setup { 12 | function test_CPF_ANC() public { 13 | vm.expectRevert(bytes("CPF_ANC")); 14 | _chainlinkPriceFeedV3 = new ChainlinkPriceFeedV3(TestAggregatorV3(0), _timeout, _twapInterval); 15 | } 16 | } 17 | 18 | contract ChainlinkPriceFeedV3Common is IChainlinkPriceFeedV3Event, Setup { 19 | using stdStorage for StdStorage; 20 | uint24 internal constant _ONE_HUNDRED_PERCENT_RATIO = 1e6; 21 | uint256 internal _timestamp = 10000000; 22 | uint256 internal _price = 1000 * 1e8; 23 | uint256 internal _prefilledPrice = _price - 5e8; 24 | uint256 internal _prefilledTimestamp = _timestamp - _twapInterval; 25 | uint256 internal _roundId = 5; 26 | 27 | function setUp() public virtual override { 28 | Setup.setUp(); 29 | 30 | // we need Aggregator's decimals() function in the constructor of ChainlinkPriceFeedV3 31 | vm.mockCall(address(_testAggregator), abi.encodeWithSelector(_testAggregator.decimals.selector), abi.encode(8)); 32 | _chainlinkPriceFeedV3 = _create_ChainlinkPriceFeedV3(_testAggregator); 33 | 34 | vm.warp(_timestamp); 35 | _mock_call_latestRoundData(_roundId, int256(_price), _timestamp); 36 | } 37 | 38 | function _chainlinkPriceFeedV3_prefill_observation_to_make_twap_calculatable() internal { 39 | // to make sure that twap price is calculatable 40 | uint256 roundId = _roundId - 1; 41 | 42 | _mock_call_latestRoundData(roundId, int256(_prefilledPrice), _prefilledTimestamp); 43 | vm.warp(_prefilledTimestamp); 44 | _chainlinkPriceFeedV3.update(); 45 | 46 | vm.warp(_timestamp); 47 | _mock_call_latestRoundData(_roundId, int256(_price), _timestamp); 48 | } 49 | 50 | function _chainlinkPriceFeedV3_cacheTwap_and_assert_eq(uint256 interval, uint256 price) internal { 51 | _chainlinkPriceFeedV3.cacheTwap(interval); 52 | assertEq(_chainlinkPriceFeedV3.getPrice(interval), price); 53 | } 54 | 55 | function _getFreezedReason_and_assert_eq(ChainlinkPriceFeedV3 priceFeed, FreezedReason reason) internal { 56 | assertEq(uint256(priceFeed.getFreezedReason()), uint256(reason)); 57 | } 58 | 59 | function _getLatestOrCachedPrice_and_assert_eq( 60 | ChainlinkPriceFeedV3 priceFeed, 61 | uint256 price, 62 | uint256 time 63 | ) internal { 64 | (uint256 p, uint256 t) = priceFeed.getLatestOrCachedPrice(); 65 | assertEq(p, price); 66 | assertEq(t, time); 67 | } 68 | 69 | function _chainlinkPriceFeedV3Broken_cacheTwap_and_assert_eq(uint256 interval, uint256 price) internal { 70 | _chainlinkPriceFeedV3Broken.cacheTwap(interval); 71 | assertEq(_chainlinkPriceFeedV3Broken.getPrice(interval), price); 72 | } 73 | 74 | function _mock_call_latestRoundData( 75 | uint256 roundId, 76 | int256 answer, 77 | uint256 timestamp 78 | ) internal { 79 | vm.mockCall( 80 | address(_testAggregator), 81 | abi.encodeWithSelector(_testAggregator.latestRoundData.selector), 82 | abi.encode(roundId, answer, timestamp, timestamp, roundId) 83 | ); 84 | } 85 | 86 | function _expect_emit_event_from_ChainlinkPriceFeedV3() internal { 87 | vm.expectEmit(false, false, false, true, address(_chainlinkPriceFeedV3)); 88 | } 89 | } 90 | 91 | contract ChainlinkPriceFeedV3GetterTest is ChainlinkPriceFeedV3Common { 92 | function test_getAggregator() public { 93 | assertEq(_chainlinkPriceFeedV3.getAggregator(), address(_testAggregator)); 94 | } 95 | 96 | function test_getTimeout() public { 97 | assertEq(_chainlinkPriceFeedV3.getTimeout(), _timeout); 98 | } 99 | 100 | function test_getLastValidPrice_is_0_when_initialized() public { 101 | assertEq(_chainlinkPriceFeedV3.getLastValidPrice(), 0); 102 | } 103 | 104 | function test_getLastValidTimestamp_is_0_when_initialized() public { 105 | assertEq(_chainlinkPriceFeedV3.getLastValidTimestamp(), 0); 106 | } 107 | 108 | function test_decimals() public { 109 | assertEq(uint256(_chainlinkPriceFeedV3.decimals()), uint256(_testAggregator.decimals())); 110 | } 111 | 112 | function test_isTimedOut_is_false_when_initialized() public { 113 | assertEq(_chainlinkPriceFeedV3.isTimedOut(), false); 114 | } 115 | 116 | function test_isTimedOut() public { 117 | _chainlinkPriceFeedV3_cacheTwap_and_assert_eq(0, _price); 118 | vm.warp(_timestamp + _timeout); 119 | assertEq(_chainlinkPriceFeedV3.isTimedOut(), false); 120 | vm.warp(_timestamp + _timeout + 1); 121 | assertEq(_chainlinkPriceFeedV3.isTimedOut(), true); 122 | } 123 | 124 | function test_isTimedOut_without_calling_update_and_with_chainlink_valid_data() public { 125 | _chainlinkPriceFeedV3_cacheTwap_and_assert_eq(0, _price); 126 | vm.warp(_timestamp + _timeout); 127 | assertEq(_chainlinkPriceFeedV3.isTimedOut(), false); 128 | // chain link get updated with a valid data but update doesn't get called 129 | _mock_call_latestRoundData(_roundId + 1, int256(_price + 1), _timestamp + _timeout); 130 | // time after the _lastValidTimestamp + timeout period 131 | vm.warp(_timestamp + _timeout + 1); 132 | assertEq(_chainlinkPriceFeedV3.isTimedOut(), false); 133 | // time after the last valid oracle price's updated time + timeout period 134 | vm.warp(_timestamp + _timeout + _timeout + 1); 135 | assertEq(_chainlinkPriceFeedV3.isTimedOut(), true); 136 | } 137 | 138 | function test_isTimedOut_without_calling_update_and_with_chainlink_invalid_data() public { 139 | _chainlinkPriceFeedV3_cacheTwap_and_assert_eq(0, _price); 140 | vm.warp(_timestamp + _timeout); 141 | assertEq(_chainlinkPriceFeedV3.isTimedOut(), false); 142 | // chain link get updated with an invalid data but update doesn't get called 143 | _mock_call_latestRoundData(_roundId + 1, int256(_price + 1), 0); 144 | vm.warp(_timestamp + _timeout + 1); 145 | // we should make sure that 146 | assertEq(_chainlinkPriceFeedV3.isTimedOut(), true); 147 | } 148 | 149 | function test_getLatestOrCachedPrice() public { 150 | (uint256 price, uint256 time) = _chainlinkPriceFeedV3.getLatestOrCachedPrice(); 151 | assertEq(price, _price); 152 | assertEq(time, _timestamp); 153 | } 154 | } 155 | 156 | contract ChainlinkPriceFeedV3CacheTwapIntervalIsZeroTest is ChainlinkPriceFeedV3Common { 157 | using SafeMath for uint256; 158 | 159 | function test_cacheTwap_first_time_caching_with_valid_price() public { 160 | _expect_emit_event_from_ChainlinkPriceFeedV3(); 161 | emit ChainlinkPriceUpdated(_price, _timestamp, FreezedReason.NotFreezed); 162 | 163 | _chainlinkPriceFeedV3_cacheTwap_and_assert_eq(0, _price); 164 | 165 | assertEq(_chainlinkPriceFeedV3.getLastValidPrice(), _price); 166 | assertEq(_chainlinkPriceFeedV3.getLastValidTimestamp(), _timestamp); 167 | _getFreezedReason_and_assert_eq(_chainlinkPriceFeedV3, FreezedReason.NotFreezed); 168 | _getLatestOrCachedPrice_and_assert_eq(_chainlinkPriceFeedV3, _price, _timestamp); 169 | } 170 | 171 | function test_getPrice_with_valid_price_after_a_second() public { 172 | uint256 latestPrice = _price + 1e8; 173 | _chainlinkPriceFeedV3.cacheTwap(0); 174 | vm.warp(_timestamp + 1); 175 | _mock_call_latestRoundData(_roundId + 1, int256(latestPrice), _timestamp + 1); 176 | assertEq(_chainlinkPriceFeedV3.getPrice(0), latestPrice); 177 | _getLatestOrCachedPrice_and_assert_eq(_chainlinkPriceFeedV3, latestPrice, _timestamp + 1); 178 | } 179 | 180 | function test_cacheTwap_wont_update_when_the_new_timestamp_is_the_same() public { 181 | _chainlinkPriceFeedV3_prefill_observation_to_make_twap_calculatable(); 182 | _chainlinkPriceFeedV3.cacheTwap(0); 183 | 184 | uint256 t2 = _timestamp + 60; 185 | 186 | _mock_call_latestRoundData(_roundId, 2000 * 1e8, t2); 187 | vm.warp(t2); 188 | _chainlinkPriceFeedV3.cacheTwap(0); 189 | 190 | uint256 currentObservationIndexBefore = _chainlinkPriceFeedV3.currentObservationIndex(); 191 | (uint256 priceBefore, , ) = _chainlinkPriceFeedV3.observations(currentObservationIndexBefore); 192 | uint256 twapBefore = _chainlinkPriceFeedV3.getPrice(_twapInterval); 193 | 194 | // giving a different price but the same old timestamp 195 | _mock_call_latestRoundData(_roundId, 2500 * 1e8, t2); 196 | vm.warp(t2 + 1); 197 | 198 | _chainlinkPriceFeedV3.cacheTwap(0); 199 | 200 | uint256 currentObservationIndexAfter = _chainlinkPriceFeedV3.currentObservationIndex(); 201 | (uint256 priceAfter, , ) = _chainlinkPriceFeedV3.observations(currentObservationIndexAfter); 202 | uint256 twapAfter = _chainlinkPriceFeedV3.getPrice(_twapInterval); 203 | 204 | // latest price will not update 205 | assertEq(currentObservationIndexAfter, currentObservationIndexBefore); 206 | assertEq(priceAfter, priceBefore); 207 | _getLatestOrCachedPrice_and_assert_eq(_chainlinkPriceFeedV3, 2000 * 1e8, t2); 208 | 209 | // twap will be re-caulculated 210 | assertEq(twapAfter != twapBefore, true); 211 | } 212 | 213 | function test_cacheTwap_freezedReason_is_NoResponse() public { 214 | // note that it's _chainlinkPriceFeedV3Broken here, not _chainlinkPriceFeedV3 215 | vm.expectEmit(false, false, false, true, address(_chainlinkPriceFeedV3Broken)); 216 | emit ChainlinkPriceUpdated(0, 0, FreezedReason.NoResponse); 217 | 218 | _chainlinkPriceFeedV3Broken_cacheTwap_and_assert_eq(0, 0); 219 | assertEq(_chainlinkPriceFeedV3Broken.getLastValidTimestamp(), 0); 220 | _getFreezedReason_and_assert_eq(_chainlinkPriceFeedV3Broken, FreezedReason.NoResponse); 221 | _getLatestOrCachedPrice_and_assert_eq(_chainlinkPriceFeedV3Broken, 0, 0); 222 | } 223 | 224 | function test_cacheTwap_freezedReason_is_IncorrectDecimals() public { 225 | vm.mockCall(address(_testAggregator), abi.encodeWithSelector(_testAggregator.decimals.selector), abi.encode(7)); 226 | 227 | _expect_emit_event_from_ChainlinkPriceFeedV3(); 228 | emit ChainlinkPriceUpdated(0, 0, FreezedReason.IncorrectDecimals); 229 | _chainlinkPriceFeedV3_cacheTwap_and_assert_eq(0, 0); 230 | _getFreezedReason_and_assert_eq(_chainlinkPriceFeedV3, FreezedReason.IncorrectDecimals); 231 | _getLatestOrCachedPrice_and_assert_eq(_chainlinkPriceFeedV3, 0, 0); 232 | } 233 | 234 | function test_cacheTwap_freezedReason_is_NoRoundId() public { 235 | _mock_call_latestRoundData(0, int256(_price), _timestamp); 236 | 237 | _expect_emit_event_from_ChainlinkPriceFeedV3(); 238 | emit ChainlinkPriceUpdated(0, 0, FreezedReason.NoRoundId); 239 | _chainlinkPriceFeedV3.cacheTwap(0); 240 | 241 | _getFreezedReason_and_assert_eq(_chainlinkPriceFeedV3, FreezedReason.NoRoundId); 242 | _getLatestOrCachedPrice_and_assert_eq(_chainlinkPriceFeedV3, 0, 0); 243 | } 244 | 245 | function test_cacheTwap_freezedReason_is_InvalidTimestamp_with_zero_timestamp() public { 246 | // zero timestamp 247 | _mock_call_latestRoundData(_roundId, int256(_price), 0); 248 | 249 | _expect_emit_event_from_ChainlinkPriceFeedV3(); 250 | emit ChainlinkPriceUpdated(0, 0, FreezedReason.InvalidTimestamp); 251 | _chainlinkPriceFeedV3.cacheTwap(0); 252 | 253 | _getFreezedReason_and_assert_eq(_chainlinkPriceFeedV3, FreezedReason.InvalidTimestamp); 254 | _getLatestOrCachedPrice_and_assert_eq(_chainlinkPriceFeedV3, 0, 0); 255 | } 256 | 257 | function test_cacheTwap_freezedReason_is_InvalidTimestamp_with_future_timestamp() public { 258 | // future 259 | _mock_call_latestRoundData(_roundId, int256(_price), _timestamp + 1); 260 | 261 | _expect_emit_event_from_ChainlinkPriceFeedV3(); 262 | emit ChainlinkPriceUpdated(0, 0, FreezedReason.InvalidTimestamp); 263 | _chainlinkPriceFeedV3.cacheTwap(0); 264 | 265 | _getFreezedReason_and_assert_eq(_chainlinkPriceFeedV3, FreezedReason.InvalidTimestamp); 266 | _getLatestOrCachedPrice_and_assert_eq(_chainlinkPriceFeedV3, 0, 0); 267 | } 268 | 269 | function test_cacheTwap_freezedReason_is_InvalidTimestamp_with_past_timestamp() public { 270 | _chainlinkPriceFeedV3.cacheTwap(0); 271 | 272 | // < _lastValidTimestamp 273 | _mock_call_latestRoundData(_roundId, int256(_price), _timestamp - 1); 274 | _expect_emit_event_from_ChainlinkPriceFeedV3(); 275 | emit ChainlinkPriceUpdated(_price, _timestamp, FreezedReason.InvalidTimestamp); 276 | _chainlinkPriceFeedV3.cacheTwap(_twapInterval); 277 | 278 | _getFreezedReason_and_assert_eq(_chainlinkPriceFeedV3, FreezedReason.InvalidTimestamp); 279 | _getLatestOrCachedPrice_and_assert_eq(_chainlinkPriceFeedV3, _price, _timestamp); 280 | } 281 | 282 | function test_cacheTwap_freezedReason_is_NonPositiveAnswer() public { 283 | _mock_call_latestRoundData(_roundId, -1, _timestamp); 284 | 285 | _expect_emit_event_from_ChainlinkPriceFeedV3(); 286 | emit ChainlinkPriceUpdated(0, 0, FreezedReason.NonPositiveAnswer); 287 | _chainlinkPriceFeedV3.cacheTwap(0); 288 | 289 | _getFreezedReason_and_assert_eq(_chainlinkPriceFeedV3, FreezedReason.NonPositiveAnswer); 290 | _getLatestOrCachedPrice_and_assert_eq(_chainlinkPriceFeedV3, 0, 0); 291 | } 292 | } 293 | 294 | contract ChainlinkPriceFeedV3CacheTwapIntervalIsNotZeroTest is ChainlinkPriceFeedV3Common { 295 | using SafeMath for uint256; 296 | 297 | function setUp() public virtual override { 298 | ChainlinkPriceFeedV3Common.setUp(); 299 | 300 | _chainlinkPriceFeedV3_prefill_observation_to_make_twap_calculatable(); 301 | } 302 | 303 | function test_cacheTwap_first_time_caching_with_valid_price() public { 304 | _expect_emit_event_from_ChainlinkPriceFeedV3(); 305 | emit ChainlinkPriceUpdated(_price, _timestamp, FreezedReason.NotFreezed); 306 | 307 | _chainlinkPriceFeedV3_cacheTwap_and_assert_eq(_twapInterval, _prefilledPrice); 308 | _getFreezedReason_and_assert_eq(_chainlinkPriceFeedV3, FreezedReason.NotFreezed); 309 | _getLatestOrCachedPrice_and_assert_eq(_chainlinkPriceFeedV3, _price, _timestamp); 310 | } 311 | 312 | function test_getPrice_first_time_without_cacheTwap_yet() public { 313 | assertEq(_chainlinkPriceFeedV3.getPrice(_twapInterval), _prefilledPrice); 314 | _getLatestOrCachedPrice_and_assert_eq(_chainlinkPriceFeedV3, _price, _timestamp); 315 | } 316 | 317 | function test_getPrice_first_time_without_cacheTwap_yet_and_after_a_second() public { 318 | // make sure that even if there's no cache observation, CumulativeTwap won't calculate a TWAP 319 | vm.warp(_timestamp + 1); 320 | 321 | // (995 * 1799 + 1000 * 1) / 1800 = 995.00277777 322 | assertEq(_chainlinkPriceFeedV3.getPrice(_twapInterval), 995.00277777 * 1e8); 323 | _getLatestOrCachedPrice_and_assert_eq(_chainlinkPriceFeedV3, _price, _timestamp); 324 | } 325 | 326 | function test_getPrice_with_valid_price_after_a_second() public { 327 | _chainlinkPriceFeedV3.cacheTwap(_twapInterval); 328 | vm.warp(_timestamp + 1); 329 | // observation0 = 0 * 0 = 0 330 | // 0 331 | // observation1 = 995 * 1800 = 1,791,000 332 | // 1800 333 | 334 | // (995 * 1800 + 1000 * 1) / 1801 = 995.0027762354 335 | assertApproxEqAbs(_chainlinkPriceFeedV3.getPrice(_twapInterval), 995.00277 * 1e8, 1e6); 336 | _getLatestOrCachedPrice_and_assert_eq(_chainlinkPriceFeedV3, _price, _timestamp); 337 | } 338 | 339 | function test_getPrice_with_valid_price_after_several_seconds() public { 340 | _chainlinkPriceFeedV3.cacheTwap(_twapInterval); 341 | vm.warp(_timestamp + 1); 342 | _mock_call_latestRoundData(_roundId + 1, int256(_price + 1e8), _timestamp + 1); 343 | _chainlinkPriceFeedV3.cacheTwap(_twapInterval); 344 | vm.warp(_timestamp + 2); 345 | // (995 * 1800 + 1000 * 1 + 1001 * 1) / 1802 = 995.0061043285 346 | assertApproxEqAbs(_chainlinkPriceFeedV3.getPrice(_twapInterval), 995.0061 * 1e8, 1e6); 347 | _getLatestOrCachedPrice_and_assert_eq(_chainlinkPriceFeedV3, _price + 1e8, _timestamp + 1); 348 | } 349 | 350 | function test_getPrice_with_valid_price_after_several_seconds_without_cacheTwap() public { 351 | _chainlinkPriceFeedV3.cacheTwap(_twapInterval); 352 | vm.warp(_timestamp + 2); 353 | _mock_call_latestRoundData(_roundId + 1, int256(_price + 1e8), _timestamp + 1); 354 | // (995 * 1800 + 1000 * 1 + 1001 * 1) / 1802 = 995.0061043285 355 | assertApproxEqAbs(_chainlinkPriceFeedV3.getPrice(_twapInterval), 995.0061 * 1e8, 1e6); 356 | _getLatestOrCachedPrice_and_assert_eq(_chainlinkPriceFeedV3, _price + 1e8, _timestamp + 1); 357 | } 358 | 359 | function test_cacheTwap_wont_update_when_the_new_timestamp_is_the_same() public { 360 | uint256 t2 = _timestamp + 60; 361 | 362 | _mock_call_latestRoundData(_roundId, 2000 * 1e8, t2); 363 | vm.warp(t2); 364 | _chainlinkPriceFeedV3.cacheTwap(_twapInterval); 365 | 366 | uint256 currentObservationIndexBefore = _chainlinkPriceFeedV3.currentObservationIndex(); 367 | (uint256 priceBefore, , ) = _chainlinkPriceFeedV3.observations(currentObservationIndexBefore); 368 | uint256 twapBefore = _chainlinkPriceFeedV3.getPrice(_twapInterval); 369 | 370 | // giving a different price but the same old timestamp 371 | _mock_call_latestRoundData(_roundId, 2500 * 1e8, t2); 372 | vm.warp(t2 + 1); 373 | 374 | // will update _cachedTwap 375 | _chainlinkPriceFeedV3.cacheTwap(_twapInterval); 376 | 377 | uint256 currentObservationIndexAfter = _chainlinkPriceFeedV3.currentObservationIndex(); 378 | (uint256 priceAfter, , ) = _chainlinkPriceFeedV3.observations(currentObservationIndexAfter); 379 | uint256 twapAfter = _chainlinkPriceFeedV3.getPrice(_twapInterval); 380 | _getLatestOrCachedPrice_and_assert_eq(_chainlinkPriceFeedV3, 2000 * 1e8, t2); 381 | 382 | // latest price will not update 383 | assertEq(currentObservationIndexAfter, currentObservationIndexBefore); 384 | assertEq(priceAfter, priceBefore); 385 | 386 | assertEq(twapAfter != twapBefore, true); 387 | } 388 | 389 | function test_cacheTwap_freezedReason_is_NoResponse() public { 390 | // note that it's _chainlinkPriceFeedV3Broken here, not _chainlinkPriceFeedV3 391 | vm.expectEmit(false, false, false, true, address(_chainlinkPriceFeedV3Broken)); 392 | emit ChainlinkPriceUpdated(0, 0, FreezedReason.NoResponse); 393 | 394 | _chainlinkPriceFeedV3Broken_cacheTwap_and_assert_eq(_twapInterval, 0); 395 | assertEq(_chainlinkPriceFeedV3Broken.getLastValidTimestamp(), 0); 396 | _getFreezedReason_and_assert_eq(_chainlinkPriceFeedV3Broken, FreezedReason.NoResponse); 397 | _getLatestOrCachedPrice_and_assert_eq(_chainlinkPriceFeedV3, _price, _timestamp); 398 | } 399 | 400 | function test_cacheTwap_freezedReason_is_IncorrectDecimals() public { 401 | vm.mockCall(address(_testAggregator), abi.encodeWithSelector(_testAggregator.decimals.selector), abi.encode(7)); 402 | 403 | _expect_emit_event_from_ChainlinkPriceFeedV3(); 404 | emit ChainlinkPriceUpdated(_prefilledPrice, _prefilledTimestamp, FreezedReason.IncorrectDecimals); 405 | _chainlinkPriceFeedV3.cacheTwap(_twapInterval); 406 | _getFreezedReason_and_assert_eq(_chainlinkPriceFeedV3, FreezedReason.IncorrectDecimals); 407 | _getLatestOrCachedPrice_and_assert_eq(_chainlinkPriceFeedV3, _prefilledPrice, _prefilledTimestamp); 408 | } 409 | 410 | function test_cacheTwap_freezedReason_is_NoRoundId() public { 411 | _mock_call_latestRoundData(0, int256(_price), _timestamp); 412 | 413 | _expect_emit_event_from_ChainlinkPriceFeedV3(); 414 | emit ChainlinkPriceUpdated(_prefilledPrice, _prefilledTimestamp, FreezedReason.NoRoundId); 415 | _chainlinkPriceFeedV3.cacheTwap(_twapInterval); 416 | _getFreezedReason_and_assert_eq(_chainlinkPriceFeedV3, FreezedReason.NoRoundId); 417 | _getLatestOrCachedPrice_and_assert_eq(_chainlinkPriceFeedV3, _prefilledPrice, _prefilledTimestamp); 418 | } 419 | 420 | function test_cacheTwap_freezedReason_is_InvalidTimestamp_with_zero_timestamp() public { 421 | // zero timestamp 422 | _mock_call_latestRoundData(_roundId, int256(_price), 0); 423 | 424 | _expect_emit_event_from_ChainlinkPriceFeedV3(); 425 | emit ChainlinkPriceUpdated(_prefilledPrice, _prefilledTimestamp, FreezedReason.InvalidTimestamp); 426 | _chainlinkPriceFeedV3.cacheTwap(_twapInterval); 427 | _getFreezedReason_and_assert_eq(_chainlinkPriceFeedV3, FreezedReason.InvalidTimestamp); 428 | _getLatestOrCachedPrice_and_assert_eq(_chainlinkPriceFeedV3, _prefilledPrice, _prefilledTimestamp); 429 | } 430 | 431 | function test_cacheTwap_freezedReason_is_InvalidTimestamp_with_future_timestamp() public { 432 | // future 433 | _mock_call_latestRoundData(_roundId, int256(_price), _timestamp + 1); 434 | 435 | _expect_emit_event_from_ChainlinkPriceFeedV3(); 436 | emit ChainlinkPriceUpdated(_prefilledPrice, _prefilledTimestamp, FreezedReason.InvalidTimestamp); 437 | _chainlinkPriceFeedV3.cacheTwap(_twapInterval); 438 | _getFreezedReason_and_assert_eq(_chainlinkPriceFeedV3, FreezedReason.InvalidTimestamp); 439 | _getLatestOrCachedPrice_and_assert_eq(_chainlinkPriceFeedV3, _prefilledPrice, _prefilledTimestamp); 440 | } 441 | 442 | function test_cacheTwap_freezedReason_is_InvalidTimestamp_with_past_timestamp() public { 443 | _chainlinkPriceFeedV3.cacheTwap(0); 444 | 445 | // < _lastValidTimestamp 446 | _mock_call_latestRoundData(_roundId, int256(_price), _timestamp - 1); 447 | _expect_emit_event_from_ChainlinkPriceFeedV3(); 448 | emit ChainlinkPriceUpdated(_price, _timestamp, FreezedReason.InvalidTimestamp); 449 | _chainlinkPriceFeedV3.cacheTwap(_twapInterval); 450 | 451 | _getFreezedReason_and_assert_eq(_chainlinkPriceFeedV3, FreezedReason.InvalidTimestamp); 452 | _getLatestOrCachedPrice_and_assert_eq(_chainlinkPriceFeedV3, _price, _timestamp); 453 | } 454 | 455 | function test_cacheTwap_freezedReason_is_NonPositiveAnswer() public { 456 | _mock_call_latestRoundData(_roundId, -1, _timestamp); 457 | 458 | _expect_emit_event_from_ChainlinkPriceFeedV3(); 459 | emit ChainlinkPriceUpdated(_prefilledPrice, _prefilledTimestamp, FreezedReason.NonPositiveAnswer); 460 | _chainlinkPriceFeedV3.cacheTwap(_twapInterval); 461 | 462 | _getFreezedReason_and_assert_eq(_chainlinkPriceFeedV3, FreezedReason.NonPositiveAnswer); 463 | _getLatestOrCachedPrice_and_assert_eq(_chainlinkPriceFeedV3, _prefilledPrice, _prefilledTimestamp); 464 | } 465 | } 466 | 467 | contract ChainlinkPriceFeedV3CacheTwapIntegrationTest is ChainlinkPriceFeedV3Common { 468 | using SafeMath for uint256; 469 | 470 | function test_integration_of_ChainlinkPriceFeedV3_CachedTwap_and_CumulativeTwap() public { 471 | _chainlinkPriceFeedV3_prefill_observation_to_make_twap_calculatable(); 472 | 473 | _chainlinkPriceFeedV3.cacheTwap(_twapInterval); 474 | 475 | int256 price1 = 960 * 1e8; 476 | uint256 timestamp1 = _timestamp + 10; 477 | _mock_call_latestRoundData(_roundId + 1, price1, timestamp1); 478 | vm.warp(timestamp1); 479 | // (995*1790+1000*10)/1800=995.0276243094 480 | assertEq(_chainlinkPriceFeedV3.getPrice(_twapInterval), 995.02777777 * 1e8); 481 | _expect_emit_event_from_ChainlinkPriceFeedV3(); 482 | emit ChainlinkPriceUpdated(uint256(price1), timestamp1, FreezedReason.NotFreezed); 483 | _chainlinkPriceFeedV3_cacheTwap_and_assert_eq(_twapInterval, 995.02777777 * 1e8); 484 | _getFreezedReason_and_assert_eq(_chainlinkPriceFeedV3, FreezedReason.NotFreezed); 485 | _getLatestOrCachedPrice_and_assert_eq(_chainlinkPriceFeedV3, uint256(price1), timestamp1); 486 | 487 | int256 price2 = 920 * 1e8; 488 | uint256 timestamp2 = timestamp1 + 20; 489 | _mock_call_latestRoundData(_roundId + 2, price2, timestamp2); 490 | vm.warp(timestamp2); 491 | // check interval = 0 is still cacheable 492 | // (995*1770+1000*10+960*20)/1800=994.6448087432 493 | assertEq(_chainlinkPriceFeedV3.getPrice(_twapInterval), 994.63888888 * 1e8); 494 | _expect_emit_event_from_ChainlinkPriceFeedV3(); 495 | emit ChainlinkPriceUpdated(uint256(price2), timestamp2, FreezedReason.NotFreezed); 496 | _chainlinkPriceFeedV3_cacheTwap_and_assert_eq(0, uint256(price2)); 497 | _getFreezedReason_and_assert_eq(_chainlinkPriceFeedV3, FreezedReason.NotFreezed); 498 | // and twap still calculable as the same as above one 499 | assertEq(_chainlinkPriceFeedV3.getPrice(_twapInterval), 994.63888888 * 1e8); 500 | vm.warp(timestamp2 + 10); 501 | // twap (by using latest price) = (995 * 1760 + 1000 * 10 + 960 * 20 + 920 * 10) / 1800 = 994.2222222222 502 | assertEq(_chainlinkPriceFeedV3.getPrice(_twapInterval), 994.22222222 * 1e8); 503 | _getLatestOrCachedPrice_and_assert_eq(_chainlinkPriceFeedV3, uint256(price2), timestamp2); 504 | 505 | int256 price3 = 900 * 1e8; 506 | uint256 timestamp3 = timestamp2 + 20; 507 | _mock_call_latestRoundData(_roundId + 3, price3, timestamp3); 508 | vm.warp(timestamp3); 509 | // twap = (995 * 1750 + 1000 * 10 + 960 * 20 + 920 * 20) / 1800 = 993.80555555 510 | assertEq(_chainlinkPriceFeedV3.getPrice(_twapInterval), 993.80555555 * 1e8); 511 | _expect_emit_event_from_ChainlinkPriceFeedV3(); 512 | emit ChainlinkPriceUpdated(uint256(price3), timestamp3, FreezedReason.NotFreezed); 513 | _chainlinkPriceFeedV3_cacheTwap_and_assert_eq(_twapInterval, 993.80555555 * 1e8); 514 | _getFreezedReason_and_assert_eq(_chainlinkPriceFeedV3, FreezedReason.NotFreezed); 515 | _getLatestOrCachedPrice_and_assert_eq(_chainlinkPriceFeedV3, uint256(price3), timestamp3); 516 | 517 | uint256 timestamp4 = timestamp3 + _timeout; 518 | vm.warp(timestamp4); 519 | assertEq(_chainlinkPriceFeedV3.isTimedOut(), false); 520 | _getLatestOrCachedPrice_and_assert_eq(_chainlinkPriceFeedV3, uint256(price3), timestamp3); 521 | 522 | uint256 timestamp5 = timestamp4 + 1; 523 | vm.warp(timestamp5); 524 | assertEq(_chainlinkPriceFeedV3.isTimedOut(), true); 525 | _getLatestOrCachedPrice_and_assert_eq(_chainlinkPriceFeedV3, uint256(price3), timestamp3); 526 | } 527 | } 528 | 529 | contract ChainlinkPriceFeedV3UpdateTest is ChainlinkPriceFeedV3Common { 530 | using SafeMath for uint256; 531 | 532 | function test_update_first_time_caching_with_valid_price() public { 533 | // init price: 1000 * 1e8 534 | _expect_emit_event_from_ChainlinkPriceFeedV3(); 535 | emit ChainlinkPriceUpdated(_price, _timestamp, FreezedReason.NotFreezed); 536 | 537 | _chainlinkPriceFeedV3.update(); 538 | 539 | _assert_LastValidPrice_LastValidTimestamp_and_FreezedReason( 540 | _chainlinkPriceFeedV3, 541 | _price, 542 | _timestamp, 543 | FreezedReason.NotFreezed 544 | ); 545 | 546 | vm.warp(_timestamp + 1); 547 | assertEq(_chainlinkPriceFeedV3.getPrice(_twapInterval), _price); 548 | } 549 | 550 | function test_update_when_the_diff_price_and_diff_timestamp() public { 551 | // init price: 1000 * 1e8 552 | _chainlinkPriceFeedV3.update(); 553 | 554 | // second update: diff price and diff timestamp 555 | // t1 t2 now 556 | // -----+--------+-------+------ 557 | // 1200s 600s 558 | // price: 1000 1010 559 | 560 | uint256 t2 = _timestamp + 1200; 561 | uint256 p2 = _price + 10 * 1e8; 562 | _mock_call_latestRoundData(_roundId + 1, int256(p2), t2); 563 | vm.warp(t2); 564 | _chainlinkPriceFeedV3.update(); 565 | 566 | _assert_LastValidPrice_LastValidTimestamp_and_FreezedReason( 567 | _chainlinkPriceFeedV3, 568 | p2, 569 | t2, 570 | FreezedReason.NotFreezed 571 | ); 572 | 573 | vm.warp(t2 + 600); 574 | 575 | // (1000*1200 + 1010*600) / 1800 = 1003.33333333 576 | assertEq(_chainlinkPriceFeedV3.getPrice(_twapInterval), 1003.33333333 * 1e8); 577 | } 578 | 579 | function test_update_when_the_same_price_and_diff_timestamp() public { 580 | // init price: 1000 * 1e8 581 | _chainlinkPriceFeedV3.update(); 582 | 583 | // second update: same price and diff timestamp 584 | // t1 t2 now 585 | // -----+--------+-------+------ 586 | // 1200s 600s 587 | // price: 1000 1000 588 | uint256 t2 = _timestamp + 1200; 589 | _mock_call_latestRoundData(_roundId + 1, int256(_price), t2); 590 | vm.warp(t2); 591 | _chainlinkPriceFeedV3.update(); 592 | 593 | _assert_LastValidPrice_LastValidTimestamp_and_FreezedReason( 594 | _chainlinkPriceFeedV3, 595 | _price, 596 | t2, 597 | FreezedReason.NotFreezed 598 | ); 599 | 600 | vm.warp(t2 + 600); 601 | assertEq(_chainlinkPriceFeedV3.getPrice(_twapInterval), _price); 602 | } 603 | 604 | function test_revert_update_when_same_price_and_same_timestamp() public { 605 | // init price: 1000 * 1e8 606 | _chainlinkPriceFeedV3.update(); 607 | 608 | // second update: same price and same timestamp 609 | vm.warp(_timestamp + 1200); 610 | vm.expectRevert(bytes("CPF_NU")); 611 | _chainlinkPriceFeedV3.update(); 612 | } 613 | 614 | function test_revert_update_when_the_diff_price_and_same_timestamp() public { 615 | // init price: 1000 * 1e8 616 | _chainlinkPriceFeedV3.update(); 617 | 618 | // second update: diff price and same timestamp 619 | int256 p2 = int256(_price) + 10 * 1e8; 620 | _mock_call_latestRoundData(_roundId + 1, p2, _timestamp); 621 | vm.warp(_timestamp + 1200); 622 | vm.expectRevert(bytes("CPF_NU")); 623 | _chainlinkPriceFeedV3.update(); 624 | } 625 | 626 | function test_revert_update_freezedReason_is_NoResponse() public { 627 | // note that it's _chainlinkPriceFeedV3Broken here, not _chainlinkPriceFeedV3 628 | vm.expectRevert(bytes("CPF_NU")); 629 | _chainlinkPriceFeedV3Broken.update(); 630 | 631 | _assert_LastValidPrice_LastValidTimestamp_and_FreezedReason( 632 | _chainlinkPriceFeedV3Broken, 633 | 0, 634 | 0, 635 | FreezedReason.NoResponse 636 | ); 637 | } 638 | 639 | function test_revert_update_freezedReason_is_IncorrectDecimals() public { 640 | vm.mockCall(address(_testAggregator), abi.encodeWithSelector(_testAggregator.decimals.selector), abi.encode(7)); 641 | 642 | vm.expectRevert(bytes("CPF_NU")); 643 | _chainlinkPriceFeedV3.update(); 644 | 645 | _assert_LastValidPrice_LastValidTimestamp_and_FreezedReason( 646 | _chainlinkPriceFeedV3, 647 | 0, 648 | 0, 649 | FreezedReason.IncorrectDecimals 650 | ); 651 | } 652 | 653 | function test_revert_update_freezedReason_is_NoRoundId() public { 654 | _mock_call_latestRoundData(0, int256(_price), _timestamp); 655 | 656 | vm.expectRevert(bytes("CPF_NU")); 657 | _chainlinkPriceFeedV3.update(); 658 | 659 | _assert_LastValidPrice_LastValidTimestamp_and_FreezedReason( 660 | _chainlinkPriceFeedV3, 661 | 0, 662 | 0, 663 | FreezedReason.NoRoundId 664 | ); 665 | } 666 | 667 | function test_revert_update_freezedReason_is_InvalidTimestamp_with_zero_timestamp() public { 668 | // zero timestamp 669 | _mock_call_latestRoundData(_roundId, int256(_price), 0); 670 | 671 | vm.expectRevert(bytes("CPF_NU")); 672 | _chainlinkPriceFeedV3.update(); 673 | 674 | _assert_LastValidPrice_LastValidTimestamp_and_FreezedReason( 675 | _chainlinkPriceFeedV3, 676 | 0, 677 | 0, 678 | FreezedReason.InvalidTimestamp 679 | ); 680 | } 681 | 682 | function test_revert_update_freezedReason_is_InvalidTimestamp_with_future_timestamp() public { 683 | // future 684 | _mock_call_latestRoundData(_roundId, int256(_price), _timestamp + 1); 685 | 686 | vm.expectRevert(bytes("CPF_NU")); 687 | _chainlinkPriceFeedV3.update(); 688 | 689 | _assert_LastValidPrice_LastValidTimestamp_and_FreezedReason( 690 | _chainlinkPriceFeedV3, 691 | 0, 692 | 0, 693 | FreezedReason.InvalidTimestamp 694 | ); 695 | } 696 | 697 | function test_revert_update_freezedReason_is_InvalidTimestamp_with_past_timestamp() public { 698 | _chainlinkPriceFeedV3.update(); 699 | 700 | // < _lastValidTimestamp 701 | _mock_call_latestRoundData(_roundId + 1, int256(_price), _timestamp - 1); 702 | 703 | vm.expectRevert(bytes("CPF_NU")); 704 | _chainlinkPriceFeedV3.update(); 705 | 706 | _assert_LastValidPrice_LastValidTimestamp_and_FreezedReason( 707 | _chainlinkPriceFeedV3, 708 | _price, 709 | _timestamp, 710 | FreezedReason.InvalidTimestamp 711 | ); 712 | } 713 | 714 | function test_revert_update_freezedReason_is_NonPositiveAnswer() public { 715 | _mock_call_latestRoundData(_roundId + 1, -1, _timestamp); 716 | 717 | vm.expectRevert(bytes("CPF_NU")); 718 | _chainlinkPriceFeedV3.update(); 719 | 720 | _assert_LastValidPrice_LastValidTimestamp_and_FreezedReason( 721 | _chainlinkPriceFeedV3, 722 | 0, 723 | 0, 724 | FreezedReason.NonPositiveAnswer 725 | ); 726 | } 727 | 728 | function _assert_LastValidPrice_LastValidTimestamp_and_FreezedReason( 729 | ChainlinkPriceFeedV3 priceFeed, 730 | uint256 price, 731 | uint256 timestamp, 732 | FreezedReason reason 733 | ) internal { 734 | assertEq(priceFeed.getLastValidPrice(), price); 735 | assertEq(priceFeed.getLastValidTimestamp(), timestamp); 736 | _getFreezedReason_and_assert_eq(priceFeed, reason); 737 | _getLatestOrCachedPrice_and_assert_eq(priceFeed, price, timestamp); 738 | } 739 | } 740 | -------------------------------------------------------------------------------- /test/foundry/CumulativeTwap.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.7.6; 2 | 3 | import "forge-std/Test.sol"; 4 | import { CumulativeTwap } from "../../contracts/twap/CumulativeTwap.sol"; 5 | 6 | contract TestCumulativeTwap is CumulativeTwap { 7 | function update(uint256 price, uint256 lastUpdatedTimestamp) external returns (bool isUpdated) { 8 | return _update(price, lastUpdatedTimestamp); 9 | } 10 | 11 | function calculateTwap( 12 | uint256 interval, 13 | uint256 price, 14 | uint256 latestUpdatedTimestamp 15 | ) external returns (uint256 twap) { 16 | return _calculateTwap(interval, price, latestUpdatedTimestamp); 17 | } 18 | 19 | function getObservationLength() external returns (uint256) { 20 | return MAX_OBSERVATION; 21 | } 22 | } 23 | 24 | contract CumulativeTwapSetup is Test { 25 | TestCumulativeTwap internal _testCumulativeTwap; 26 | 27 | struct Observation { 28 | uint256 price; 29 | uint256 priceCumulative; 30 | uint256 timestamp; 31 | } 32 | 33 | function _updatePrice(uint256 price, bool forward) internal { 34 | _testCumulativeTwap.update(price, block.timestamp); 35 | 36 | if (forward) { 37 | vm.warp(block.timestamp + 15); 38 | vm.roll(block.number + 1); 39 | } 40 | } 41 | 42 | function _isObservationEqualTo( 43 | uint256 index, 44 | uint256 expectedPrice, 45 | uint256 expectedPriceCumulative, 46 | uint256 expectedTimestamp 47 | ) internal { 48 | (uint256 _price, uint256 _priceCumulative, uint256 _timestamp) = _testCumulativeTwap.observations(index); 49 | assertEq(_price, expectedPrice); 50 | assertEq(_priceCumulative, expectedPriceCumulative); 51 | assertEq(_timestamp, expectedTimestamp); 52 | } 53 | 54 | function _getTwap(uint256 interval) internal returns (uint256) { 55 | uint256 currentIndex = _testCumulativeTwap.currentObservationIndex(); 56 | (uint256 price, uint256 _, uint256 time) = _testCumulativeTwap.observations(currentIndex); 57 | return _testCumulativeTwap.calculateTwap(interval, price, time); 58 | } 59 | 60 | function setUp() public virtual { 61 | _testCumulativeTwap = new TestCumulativeTwap(); 62 | } 63 | } 64 | 65 | contract CumulativeTwapUpdateTest is CumulativeTwapSetup { 66 | uint256 internal constant _INIT_BLOCK_TIMESTAMP = 1000; 67 | 68 | function setUp() public virtual override { 69 | vm.warp(_INIT_BLOCK_TIMESTAMP); 70 | 71 | CumulativeTwapSetup.setUp(); 72 | } 73 | 74 | function test_update_correctly() public { 75 | uint256 t1 = _INIT_BLOCK_TIMESTAMP; 76 | uint256 p1 = 100 * 1e8; 77 | 78 | bool result1 = _testCumulativeTwap.update(p1, t1); 79 | 80 | uint256 observationIndex1 = _testCumulativeTwap.currentObservationIndex(); 81 | 82 | (uint256 price1, uint256 priceCumulative1, uint256 timestamp1) = 83 | _testCumulativeTwap.observations(observationIndex1); 84 | 85 | assertTrue(result1); 86 | assertEq(observationIndex1, 0); 87 | assertEq(price1, p1); 88 | assertEq(priceCumulative1, 0); 89 | assertEq(timestamp1, t1); 90 | 91 | uint256 t2 = _INIT_BLOCK_TIMESTAMP + 10; 92 | uint256 p2 = 110 * 1e8; 93 | vm.warp(t2); 94 | 95 | bool result2 = _testCumulativeTwap.update(p2, t2); 96 | 97 | uint256 observationIndex2 = _testCumulativeTwap.currentObservationIndex(); 98 | 99 | (uint256 price2, uint256 priceCumulative2, uint256 timestamp2) = 100 | _testCumulativeTwap.observations(observationIndex2); 101 | 102 | assertTrue(result2); 103 | assertEq(observationIndex2, 1); 104 | assertEq(price2, p2); 105 | assertEq(priceCumulative2, 1000 * 1e8); // 10 * 100 * 1e8 106 | assertEq(timestamp2, t2); 107 | } 108 | 109 | function test_revert_update_when_timestamp_is_less_than_last_observation() public { 110 | uint256 t1 = _INIT_BLOCK_TIMESTAMP; 111 | uint256 p1 = 100 * 1e8; 112 | assertEq(_testCumulativeTwap.update(p1, t1), true); 113 | 114 | vm.expectRevert(bytes("CT_IT")); 115 | assertEq(_testCumulativeTwap.update(p1, t1 - 10), false); 116 | } 117 | 118 | function test_update_when_price_is_the_same_as_last_opservation() public { 119 | uint256 t1 = _INIT_BLOCK_TIMESTAMP; 120 | uint256 p1 = 100 * 1e8; 121 | 122 | // first update 123 | assertEq(_testCumulativeTwap.update(p1, t1), true); 124 | 125 | uint256 latestObservationIndex = _testCumulativeTwap.currentObservationIndex(); 126 | (uint256 priceBefore, uint256 priceCumulativeBefore, uint256 timestampBefore) = 127 | _testCumulativeTwap.observations(latestObservationIndex); 128 | 129 | // second update won't update 130 | assertEq(_testCumulativeTwap.update(p1, t1 + 10), false); 131 | assertEq(_testCumulativeTwap.currentObservationIndex(), latestObservationIndex); 132 | 133 | (uint256 priceAfter, uint256 priceCumulativeAfter, uint256 timestampAfter) = 134 | _testCumulativeTwap.observations(latestObservationIndex); 135 | assertEq(priceBefore, priceAfter); 136 | assertEq(priceCumulativeBefore, priceCumulativeAfter); 137 | assertEq(timestampBefore, timestampAfter); 138 | } 139 | 140 | function test_update_when_timestamp_and_price_is_the_same_as_last_opservation() public { 141 | uint256 t1 = _INIT_BLOCK_TIMESTAMP; 142 | uint256 p1 = 100 * 1e8; 143 | 144 | // first update 145 | assertEq(_testCumulativeTwap.update(p1, t1), true); 146 | 147 | uint256 latestObservationIndex = _testCumulativeTwap.currentObservationIndex(); 148 | (uint256 priceBefore, uint256 priceCumulativeBefore, uint256 timestampBefore) = 149 | _testCumulativeTwap.observations(latestObservationIndex); 150 | 151 | // second update won't update 152 | assertEq(_testCumulativeTwap.update(p1, t1), false); 153 | assertEq(_testCumulativeTwap.currentObservationIndex(), latestObservationIndex); 154 | 155 | (uint256 priceAfter, uint256 priceCumulativeAfter, uint256 timestampAfter) = 156 | _testCumulativeTwap.observations(latestObservationIndex); 157 | assertEq(priceBefore, priceAfter); 158 | assertEq(priceCumulativeBefore, priceCumulativeAfter); 159 | assertEq(timestampBefore, timestampAfter); 160 | } 161 | } 162 | 163 | contract CumulativeTwapCalculateTwapBase is CumulativeTwapSetup { 164 | uint256 internal constant _BEGIN_PRICE = 400; 165 | uint256 internal _BEGIN_TIME = block.timestamp; 166 | uint256 internal observationLength; 167 | 168 | function setUp() public virtual override { 169 | vm.warp(_BEGIN_TIME); 170 | 171 | CumulativeTwapSetup.setUp(); 172 | 173 | observationLength = _testCumulativeTwap.getObservationLength(); 174 | } 175 | } 176 | 177 | contract CumulativeTwapCalculateTwapTestWithoutObservation is CumulativeTwapCalculateTwapBase { 178 | function test_calculateTwap_should_return_0_when_observations_is_empty() public { 179 | assertEq(_testCumulativeTwap.currentObservationIndex(), uint256(0)); 180 | 181 | assertEq(_getTwap(45), uint256(0)); 182 | } 183 | } 184 | 185 | contract CumulativeTwapCalculateTwapTest is CumulativeTwapCalculateTwapBase { 186 | function setUp() public virtual override { 187 | CumulativeTwapCalculateTwapBase.setUp(); 188 | 189 | // timestamp(_BEGIN_TIME + 0) : 400 190 | // timestamp(_BEGIN_TIME + 15) : 405 191 | // timestamp(_BEGIN_TIME + 30) : 410 192 | // now = _BEGIN_TIME + 45 193 | 194 | _updatePrice(400, true); 195 | _updatePrice(405, true); 196 | _updatePrice(410, true); 197 | } 198 | 199 | function test_calculateTwap_when_interval_is_0() public { 200 | assertEq(_getTwap(0), 0); 201 | } 202 | 203 | function test_calculateTwap_when_given_a_valid_interval() public { 204 | // (410*15+405*15+400*5)/35=406.4 205 | assertEq(_getTwap(35), 406); // case 3: in the mid 206 | // (410*15+405*15)/30=407.5 207 | assertEq(_getTwap(30), 407); // case 1: left bound 208 | 209 | _updatePrice(415, false); 210 | // (410*15+405*15)/30=407.5 211 | assertEq(_getTwap(30), 407); // case 2: right bound 212 | } 213 | 214 | function test_calculateTwap_when_given_a_valid_interval_and_hasnt_beenn_updated_for_a_while() public { 215 | uint256 t = block.timestamp + 10; 216 | vm.warp(t); 217 | vm.roll(block.number + 1); 218 | // (410*25+405*5)/30=409.1 219 | assertEq(_testCumulativeTwap.calculateTwap(30, 415, t), 409); 220 | 221 | // (415*5+410*20)/25=411 222 | assertEq(_testCumulativeTwap.calculateTwap(25, 415, t - 5), 411); 223 | } 224 | 225 | function test_calculateTwap_when_given_a_interval_less_than_latest_observation() public { 226 | // (410*14)/14=410 227 | assertEq(_getTwap(14), 410); 228 | } 229 | 230 | function test_calculateTwap_when_given_interval_exceeds_observations() public { 231 | assertEq(_getTwap(46), 0); 232 | } 233 | 234 | function test_calculateTwap_when_valid_timestamp_and_price() public { 235 | uint256 interval = 30; 236 | uint256 t1 = 1000; 237 | uint256 p1 = 100 * 1e8; 238 | assertEq(_testCumulativeTwap.update(p1, t1), true); 239 | 240 | uint256 t2 = t1 + interval; 241 | vm.warp(t2); 242 | assertEq(_testCumulativeTwap.calculateTwap(interval, p1, t1), p1); 243 | } 244 | 245 | function test_revert_calculateTwap_when_same_timestamp_and_different_price() public { 246 | uint256 interval = 30; 247 | uint256 t1 = 1000; 248 | uint256 p1 = 100 * 1e8; 249 | assertEq(_testCumulativeTwap.update(p1, t1), true); 250 | 251 | uint256 t2 = t1 + interval; 252 | uint256 p2 = 120 * 1e8; 253 | vm.warp(t2); 254 | vm.expectRevert(bytes("CT_IPWCT")); 255 | _testCumulativeTwap.calculateTwap(interval, p2, t1); 256 | } 257 | } 258 | 259 | contract CumulativeTwapRingBufferTest is CumulativeTwapCalculateTwapBase { 260 | function setUp() public virtual override { 261 | CumulativeTwapCalculateTwapBase.setUp(); 262 | 263 | // fill up observations[] excludes the last one 264 | for (uint256 i = 0; i < observationLength - 1; i++) { 265 | _updatePrice(_BEGIN_PRICE + i, true); 266 | } 267 | } 268 | 269 | function test_calculateTwap_when_index_hasnt_get_rotated() public { 270 | // last filled up index 271 | assertEq(_testCumulativeTwap.currentObservationIndex(), observationLength - 2); 272 | (uint256 pricePrev, uint256 priceCumulativePrev, uint256 _) = 273 | _testCumulativeTwap.observations(observationLength - 3); 274 | _isObservationEqualTo( 275 | observationLength - 2, 276 | 2198, // _BEGIN_PRICE + observationLength - 2 = 400 + 1798 277 | priceCumulativePrev + pricePrev * 15, // 1797's cumulative price + 1797 * 15 278 | 26971 // _BEGIN_TIME + (observationLength - 2) * 15 = 1 + 1798 * 15 279 | ); 280 | 281 | // last observation hasn't been filled up yet 282 | _isObservationEqualTo(observationLength - 1, 0, 0, 0); 283 | 284 | // (2196 * 15 + 2197 * 15 + 2198 * 15) / 45 = 2197 285 | assertEq(_getTwap(45), 2197); 286 | } 287 | 288 | function test_calculateTwap_when_index_has_rotated_to_0() public { 289 | _updatePrice(_BEGIN_PRICE + observationLength - 1, true); // currentObservationIndex=1799, price=400+1799 290 | _updatePrice(_BEGIN_PRICE + observationLength, true); // currentObservationIndex=0, price=400+1800 291 | 292 | assertEq(_testCumulativeTwap.currentObservationIndex(), uint256(0)); 293 | 294 | // (2200 * 15 + 2199 * 15 + 2198 * 15) / 45 = 2199 295 | assertEq(_getTwap(45), 2199); 296 | } 297 | 298 | function test_calculateTwap_when_index_has_rotated_to_9() public { 299 | _updatePrice(_BEGIN_PRICE + observationLength - 1, true); // currentObservationIndex=1799, price=400+1799 300 | for (uint256 i; i < 10; i++) { 301 | _updatePrice(_BEGIN_PRICE + observationLength + i, true); 302 | } 303 | 304 | assertEq(_testCumulativeTwap.currentObservationIndex(), uint256(9)); 305 | 306 | // (2207 * 15 + 2208 * 15 + 2209 * 15) / 45 = 2208 307 | assertEq(_getTwap(45), 2208); 308 | } 309 | 310 | function test_calculateTwap_when_twap_interval_is_exact_the_maximum_limitation() public { 311 | _updatePrice(_BEGIN_PRICE + observationLength - 1, true); // currentObservationIndex=1799, price=400+1799 312 | _updatePrice(_BEGIN_PRICE + observationLength, false); // currentObservationIndex=0, price=400+1800 313 | 314 | assertEq(_testCumulativeTwap.currentObservationIndex(), uint256(0)); 315 | 316 | // (((401 + 2199) / 2) * (26986-1) + 2200 * 1) / 26986 = 1300.0333506263 317 | assertEq(_getTwap(1799 * 15), 1300); 318 | } 319 | 320 | function test_calculateTwap_should_return_0_when_twap_interval_exceeds_maximum_limitation() public { 321 | _updatePrice(_BEGIN_PRICE + observationLength - 1, true); // currentObservationIndex=1799, price=400+1799 322 | _updatePrice(_BEGIN_PRICE + observationLength, false); // currentObservationIndex=0, price=400+1800 323 | 324 | assertEq(_testCumulativeTwap.currentObservationIndex(), uint256(0)); 325 | 326 | assertEq(_getTwap(1799 * 15 + 1), uint256(0)); 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /test/foundry/PriceFeedDispatcher.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.7.6; 2 | 3 | import "forge-std/Test.sol"; 4 | import { IUniswapV3PriceFeed } from "../../contracts/interface/IUniswapV3PriceFeed.sol"; 5 | import { UniswapV3Pool } from "@uniswap/v3-core/contracts/UniswapV3Pool.sol"; 6 | import { UniswapV3PriceFeed } from "../../contracts/UniswapV3PriceFeed.sol"; 7 | import { TestAggregatorV3 } from "../../contracts/test/TestAggregatorV3.sol"; 8 | import { ChainlinkPriceFeedV3 } from "../../contracts/ChainlinkPriceFeedV3.sol"; 9 | import { PriceFeedDispatcher } from "../../contracts/PriceFeedDispatcher.sol"; 10 | import { IPriceFeedDispatcherEvent } from "../../contracts/interface/IPriceFeedDispatcher.sol"; 11 | 12 | contract PriceFeedDispatcherMocked is PriceFeedDispatcher { 13 | constructor(address chainlinkPriceFeedV3) PriceFeedDispatcher(chainlinkPriceFeedV3) {} 14 | 15 | function setPriceFeedStatus(Status status) external { 16 | _status = status; 17 | } 18 | 19 | function getStatus() external view returns (Status) { 20 | return _status; 21 | } 22 | } 23 | 24 | contract PriceFeedDispatcherSetup is Test { 25 | UniswapV3PriceFeed internal _uniswapV3PriceFeed; 26 | ChainlinkPriceFeedV3 internal _chainlinkPriceFeed; 27 | PriceFeedDispatcherMocked internal _priceFeedDispatcher; 28 | PriceFeedDispatcherMocked internal _priceFeedDispatcherWithUniswapV3PriceFeedUninitialized; 29 | 30 | function setUp() public virtual { 31 | _uniswapV3PriceFeed = _create_uniswapV3PriceFeed(); 32 | _chainlinkPriceFeed = _create_ChainlinkPriceFeedV3(); 33 | } 34 | 35 | function _create_uniswapV3PriceFeed() internal returns (UniswapV3PriceFeed) { 36 | TestAggregatorV3 aggregator = new TestAggregatorV3(); 37 | // UniswapV3PriceFeed needs only a contract address 38 | return new UniswapV3PriceFeed(address(aggregator)); 39 | } 40 | 41 | function _create_ChainlinkPriceFeedV3() internal returns (ChainlinkPriceFeedV3) { 42 | TestAggregatorV3 aggregator = new TestAggregatorV3(); 43 | vm.mockCall(address(aggregator), abi.encodeWithSelector(aggregator.decimals.selector), abi.encode(8)); 44 | 45 | return new ChainlinkPriceFeedV3(aggregator, 1, 1); 46 | } 47 | 48 | function _create_PriceFeedDispatcher() internal returns (PriceFeedDispatcherMocked) { 49 | return new PriceFeedDispatcherMocked(address(_chainlinkPriceFeed)); 50 | } 51 | 52 | function _create_PriceFeedDispatcher_and_setUniswapV3PriceFeed() internal returns (PriceFeedDispatcherMocked) { 53 | PriceFeedDispatcherMocked priceFeedDispatcher = _create_PriceFeedDispatcher(); 54 | priceFeedDispatcher.setUniswapV3PriceFeed(address(_uniswapV3PriceFeed)); 55 | return priceFeedDispatcher; 56 | } 57 | } 58 | 59 | contract PriceFeedDispatcherConstructorAndSetterTest is IPriceFeedDispatcherEvent, PriceFeedDispatcherSetup { 60 | function test_PFD_CNC() public { 61 | vm.expectRevert(bytes("PFD_CNC")); 62 | _priceFeedDispatcher = new PriceFeedDispatcherMocked(address(0)); 63 | 64 | vm.expectRevert(bytes("PFD_CNC")); 65 | _priceFeedDispatcher = new PriceFeedDispatcherMocked(makeAddr("HA")); 66 | } 67 | 68 | function test_PFD_UCAU() public { 69 | _priceFeedDispatcher = _create_PriceFeedDispatcher(); 70 | 71 | vm.expectRevert(bytes("PFD_UCAU")); 72 | _priceFeedDispatcher.setUniswapV3PriceFeed(address(0)); 73 | 74 | vm.expectRevert(bytes("PFD_UCAU")); 75 | _priceFeedDispatcher.setUniswapV3PriceFeed(makeAddr("HA")); 76 | } 77 | 78 | function test_cannot_setUniswapV3PriceFeed_by_non_owner() public { 79 | _priceFeedDispatcher = _create_PriceFeedDispatcher(); 80 | 81 | vm.expectRevert(bytes("Ownable: caller is not the owner")); 82 | vm.prank(makeAddr("HA")); 83 | _priceFeedDispatcher.setUniswapV3PriceFeed(address(_uniswapV3PriceFeed)); 84 | } 85 | 86 | function test_setUniswapV3PriceFeed_should_emit_event() public { 87 | _priceFeedDispatcher = _create_PriceFeedDispatcher(); 88 | 89 | vm.expectEmit(false, false, false, true, address(_priceFeedDispatcher)); 90 | emit UniswapV3PriceFeedUpdated(address(_uniswapV3PriceFeed)); 91 | _priceFeedDispatcher.setUniswapV3PriceFeed(address(_uniswapV3PriceFeed)); 92 | } 93 | } 94 | 95 | contract PriceFeedDispatcherTest is IPriceFeedDispatcherEvent, PriceFeedDispatcherSetup { 96 | address public nonOwnerAddress = makeAddr("nonOwnerAddress"); 97 | uint256 internal _chainlinkPrice = 100 * 1e18; 98 | uint256 internal _uniswapPrice = 50 * 1e18; 99 | 100 | function setUp() public virtual override { 101 | PriceFeedDispatcherSetup.setUp(); 102 | _priceFeedDispatcher = _create_PriceFeedDispatcher_and_setUniswapV3PriceFeed(); 103 | _priceFeedDispatcherWithUniswapV3PriceFeedUninitialized = _create_PriceFeedDispatcher(); 104 | 105 | vm.mockCall( 106 | address(_uniswapV3PriceFeed), 107 | abi.encodeWithSelector(_uniswapV3PriceFeed.getPrice.selector), 108 | abi.encode(50 * 1e8) 109 | ); 110 | 111 | vm.mockCall( 112 | address(_uniswapV3PriceFeed), 113 | abi.encodeWithSelector(_uniswapV3PriceFeed.decimals.selector), 114 | abi.encode(8) 115 | ); 116 | 117 | vm.mockCall( 118 | address(_chainlinkPriceFeed), 119 | abi.encodeWithSelector(_chainlinkPriceFeed.getPrice.selector), 120 | abi.encode(100 * 1e8) 121 | ); 122 | } 123 | 124 | function test_dispatchPrice_not_isToUseUniswapV3PriceFeed() public { 125 | assertEq(uint256(_priceFeedDispatcher.getStatus()), uint256(Status.Chainlink)); 126 | assertEq(_priceFeedDispatcher.isToUseUniswapV3PriceFeed(), false); 127 | _dispatchPrice_and_assertEq_getDispatchedPrice(_chainlinkPrice); 128 | } 129 | 130 | function test_dispatchPrice_isToUseUniswapV3PriceFeed_when__status_is_already_UniswapV3() public { 131 | _priceFeedDispatcher.setPriceFeedStatus(Status.UniswapV3); 132 | 133 | _dispatchPrice_and_assertEq_getDispatchedPrice(_uniswapPrice); 134 | assertEq(uint256(_priceFeedDispatcher.getStatus()), uint256(Status.UniswapV3)); 135 | } 136 | 137 | function test_dispatchPrice_isToUseUniswapV3PriceFeed_when_different__chainlinkPriceFeed_isTimedOut() public { 138 | vm.mockCall( 139 | address(_chainlinkPriceFeed), 140 | abi.encodeWithSelector(_chainlinkPriceFeed.isTimedOut.selector), 141 | abi.encode(true) 142 | ); 143 | assertEq(_priceFeedDispatcher.isToUseUniswapV3PriceFeed(), true); 144 | 145 | _expect_emit_event_from_PriceFeedDispatcher(); 146 | emit StatusUpdated(Status.UniswapV3); 147 | _dispatchPrice_and_assertEq_getDispatchedPrice(_uniswapPrice); 148 | assertEq(uint256(_priceFeedDispatcher.getStatus()), uint256(Status.UniswapV3)); 149 | assertEq(_priceFeedDispatcher.isToUseUniswapV3PriceFeed(), true); 150 | 151 | // similar to the above case, if the status is already UniswapV3, then even if ChainlinkPriceFeed !isTimedOut, 152 | // isToUseUniswapV3PriceFeed() will still be true 153 | vm.mockCall( 154 | address(_chainlinkPriceFeed), 155 | abi.encodeWithSelector(_chainlinkPriceFeed.isTimedOut.selector), 156 | abi.encode(false) 157 | ); 158 | assertEq(_priceFeedDispatcher.isToUseUniswapV3PriceFeed(), true); 159 | } 160 | 161 | function test_dispatchPrice_not_isToUseUniswapV3PriceFeed_when__uniswapV3PriceFeed_uninitialized() public { 162 | _priceFeedDispatcherWithUniswapV3PriceFeedUninitialized.dispatchPrice(0); 163 | assertEq(_priceFeedDispatcherWithUniswapV3PriceFeedUninitialized.getDispatchedPrice(0), _chainlinkPrice); 164 | assertEq( 165 | uint256(_priceFeedDispatcherWithUniswapV3PriceFeedUninitialized.getStatus()), 166 | uint256(Status.Chainlink) 167 | ); 168 | assertEq(_priceFeedDispatcherWithUniswapV3PriceFeedUninitialized.isToUseUniswapV3PriceFeed(), false); 169 | 170 | vm.mockCall( 171 | address(_chainlinkPriceFeed), 172 | abi.encodeWithSelector(_chainlinkPriceFeed.isTimedOut.selector), 173 | abi.encode(true) 174 | ); 175 | assertEq(_priceFeedDispatcherWithUniswapV3PriceFeedUninitialized.getDispatchedPrice(0), _chainlinkPrice); 176 | assertEq(_priceFeedDispatcherWithUniswapV3PriceFeedUninitialized.isToUseUniswapV3PriceFeed(), false); 177 | } 178 | 179 | function _dispatchPrice_and_assertEq_getDispatchedPrice(uint256 price) internal { 180 | _priceFeedDispatcher.dispatchPrice(0); 181 | assertEq(_priceFeedDispatcher.getDispatchedPrice(0), price); 182 | assertEq(_priceFeedDispatcher.getPrice(0), price); 183 | } 184 | 185 | function _expect_emit_event_from_PriceFeedDispatcher() internal { 186 | vm.expectEmit(false, false, false, true, address(_priceFeedDispatcher)); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /test/foundry/Setup.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.7.6; 2 | 3 | import "forge-std/Test.sol"; 4 | import { TestAggregatorV3 } from "../../contracts/test/TestAggregatorV3.sol"; 5 | import { ChainlinkPriceFeedV3 } from "../../contracts/ChainlinkPriceFeedV3.sol"; 6 | 7 | contract AggregatorV3Broken is TestAggregatorV3 { 8 | function latestRoundData() 9 | external 10 | view 11 | override 12 | returns ( 13 | uint80 roundId, 14 | int256 answer, 15 | uint256 startedAt, 16 | uint256 updatedAt, 17 | uint80 answeredInRound 18 | ) 19 | { 20 | revert(); 21 | } 22 | 23 | function decimals() external view override returns (uint8) { 24 | revert(); 25 | } 26 | } 27 | 28 | contract Setup is Test { 29 | uint256 internal _timeout = 40 * 60; // 40 mins 30 | uint80 internal _twapInterval = 30 * 60; // 30 mins 31 | 32 | TestAggregatorV3 internal _testAggregator; 33 | ChainlinkPriceFeedV3 internal _chainlinkPriceFeedV3; 34 | 35 | // for test_cachePrice_freezedReason_is_NoResponse() 36 | AggregatorV3Broken internal _aggregatorV3Broken; 37 | ChainlinkPriceFeedV3 internal _chainlinkPriceFeedV3Broken; 38 | 39 | function setUp() public virtual { 40 | _testAggregator = _create_TestAggregator(); 41 | 42 | _aggregatorV3Broken = _create_AggregatorV3Broken(); 43 | _chainlinkPriceFeedV3Broken = _create_ChainlinkPriceFeedV3(_aggregatorV3Broken); 44 | 45 | // s.t. _chainlinkPriceFeedV3Broken will revert on decimals() 46 | vm.clearMockedCalls(); 47 | } 48 | 49 | function _create_TestAggregator() internal returns (TestAggregatorV3) { 50 | TestAggregatorV3 aggregator = new TestAggregatorV3(); 51 | vm.mockCall(address(aggregator), abi.encodeWithSelector(aggregator.decimals.selector), abi.encode(8)); 52 | return aggregator; 53 | } 54 | 55 | function _create_ChainlinkPriceFeedV3(TestAggregatorV3 aggregator) internal returns (ChainlinkPriceFeedV3) { 56 | return new ChainlinkPriceFeedV3(aggregator, _timeout, _twapInterval); 57 | } 58 | 59 | function _create_AggregatorV3Broken() internal returns (AggregatorV3Broken) { 60 | AggregatorV3Broken aggregator = new AggregatorV3Broken(); 61 | vm.mockCall(address(aggregator), abi.encodeWithSelector(aggregator.decimals.selector), abi.encode(8)); 62 | return aggregator; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /test/shared/chainlink.ts: -------------------------------------------------------------------------------- 1 | // https://docs.chain.link/docs/historical-price-data/#roundid-in-proxy 2 | export function computeRoundId(phaseId: number, aggregatorRoundId: number): string { 3 | const roundId = (BigInt(phaseId) << BigInt("64")) | BigInt(aggregatorRoundId) 4 | return roundId.toString() 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "resolveJsonModule": true, 4 | "esModuleInterop": true, 5 | "outDir": "dist" 6 | }, 7 | "include": ["./scripts", "./test", "./typechain"], 8 | "files": ["./hardhat.config.ts"] 9 | } 10 | --------------------------------------------------------------------------------