├── .build ├── mainnet.json └── ropsten.json ├── .codecov.yml ├── .env.example ├── .gitattributes ├── .github ├── CODEOWNERS └── workflows │ └── solidity.yml ├── .gitignore ├── .gitmodules ├── .nvmrc ├── .solcover.js ├── .soliumrc.json ├── LICENSE ├── README.md ├── configuration ├── README.md ├── parameters-price-oracle.ts └── parameters.js ├── contracts ├── Chainlink │ └── AggregatorValidatorInterface.sol ├── Ownable.sol ├── PriceOracle │ └── PriceOracle.sol ├── Uniswap │ ├── CErc20.sol │ ├── UniswapAnchoredView.sol │ ├── UniswapConfig.sol │ └── UniswapLib.sol └── test │ ├── MockChainlinkOCRAggregator.sol │ └── UniswapV3SwapHelper.sol ├── hardhat.config.ts ├── package.json ├── poster ├── .dockerignore ├── Dockerfile ├── README.md ├── jest.config.js ├── package.json ├── src │ ├── index.ts │ ├── interfaces.ts │ ├── mainnet_uniswap_mocker.ts │ ├── post_with_retries.ts │ ├── poster.ts │ ├── prev_price.ts │ ├── sources │ │ └── coinbase.ts │ └── util.ts ├── tests │ ├── post_with_retries_test.ts │ └── poster_test.ts ├── tsconfig.json └── yarn.lock ├── scripts └── deploy-uav.js ├── sdk └── javascript │ ├── .dockerignore │ ├── Dockerfile │ ├── README.md │ ├── examples │ └── fixed.js │ ├── jest.config.js │ ├── package.json │ ├── src │ ├── cli.ts │ ├── express_endpoint.ts │ ├── index.ts │ ├── key.ts │ └── reporter.ts │ ├── tests │ ├── express_endpoint_test.ts │ └── reporter_test.ts │ ├── tsconfig.json │ └── yarn.lock ├── tasks ├── price-oracle-tasks.ts └── verify-uav.ts ├── test ├── MockChainlinkOCRAggregator.ts ├── NonReporterPrices.test.ts ├── PostRealWorldPrices.test.ts ├── PriceOracle.test.ts ├── PriceOracleConfig.test.ts ├── UniswapAnchoredView.test.ts ├── UniswapConfig.test.ts └── utils │ ├── address.ts │ ├── cTokenAddresses.ts │ ├── erc20.ts │ ├── exp.ts │ ├── expectedPricesToPush.ts │ ├── index.ts │ ├── keccak256.ts │ ├── resetFork.ts │ └── uint.ts ├── tsconfig.json └── yarn.lock /.build/mainnet.json: -------------------------------------------------------------------------------- 1 | { 2 | "AnchoredPriceView": "0xDECee5C06B8CC6c4D85dfd8518C246fd1497b2AE", 3 | "AnchoredView": "0x4345d279E3aeA247Ad7865cC95BeE3088eaBc433", 4 | "OpenOraclePriceData": "0x541e90aadfbb653843ea9b5dec43fe2cceca0dd6" 5 | } 6 | -------------------------------------------------------------------------------- /.build/ropsten.json: -------------------------------------------------------------------------------- 1 | { 2 | "OpenOraclePriceData": "0xda019b064F451657F2c117Ad79a56F7abDbF1201", 3 | "DelFiPrice": "0xA7637AD217af1DD781b7DDeD452CE339cfb4a312", 4 | "AnchoredPriceView": "0x4745ed21F933C445F6021E1949817c6C281329d2", 5 | "MockAnchorOracle": "0xF054B68676900ECB13b7107019b062662B50d3d7", 6 | "AnchoredView": "0xcaF366e452f9613Cb3fabe094619cc1e1e2aC149", 7 | "UniswapAnchoredView": "0xCCD252F17E7F69C1ce813DDE398e878A8D8A2202" 8 | } -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | require_ci_to_pass: yes 3 | 4 | coverage: 5 | precision: 2 6 | round: down 7 | range: "70...100" 8 | 9 | parsers: 10 | gcov: 11 | branch_detection: 12 | conditional: yes 13 | loop: yes 14 | method: no 15 | macro: no 16 | 17 | comment: 18 | layout: "reach,diff,flags,tree" 19 | behavior: default 20 | require_changes: no 21 | 22 | ignore: 23 | - "tests/contracts" 24 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | COMP_MULTISIG=0xbbf3f1421D886E9b2c5D716B5192aC998af2012c 2 | MAINNET_PK= # Primary key of the account to deploy from 3 | MAINNET_URL= # Mainnet http json RPC url 4 | ETHERSCAN= # Etherscan API key -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /poster/fixtures/generated/* binary 2 | *.sol linguist-language=Solidity 3 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @coburncoburn @jflatow @hayesgm @maxwolff @torreyatcitty -------------------------------------------------------------------------------- /.github/workflows/solidity.yml: -------------------------------------------------------------------------------- 1 | name: Solidity 2 | on: [push] 3 | jobs: 4 | solidity_coverage: 5 | environment: ci 6 | name: Solidity Test Coverage 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Setup node 10 | uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 11 | with: 12 | node-version: "16" 13 | - name: Checkout the repo 14 | uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 15 | - name: Yarn cache 16 | uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 17 | env: 18 | cache-name: yarn-cache 19 | with: 20 | path: | 21 | ~/.npm 22 | ~/.cache 23 | **/node_modules 24 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('yarn.lock') }} 25 | restore-keys: | 26 | ${{ runner.os }}-build-${{ env.cache-name }}- 27 | ${{ runner.os }}-build- 28 | ${{ runner.os }}- 29 | - run: yarn install --frozen-lockfile 30 | - name: Run coverage 31 | run: | 32 | yarn run compile 33 | yarn run coverage 34 | env: 35 | MAINNET_PK: ${{ secrets.MAINNET_PK }} 36 | MAINNET_URL: ${{ secrets.MAINNET_URL }} 37 | solidity_test: 38 | name: Solidity Tests 39 | environment: ci 40 | runs-on: ubuntu-latest 41 | steps: 42 | - name: Setup node 43 | uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 44 | with: 45 | node-version: "16" 46 | - name: Checkout the repo 47 | uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 48 | - name: Yarn cache 49 | uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 50 | env: 51 | cache-name: yarn-cache 52 | with: 53 | path: | 54 | ~/.npm 55 | ~/.cache 56 | **/node_modules 57 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('yarn.lock') }} 58 | restore-keys: | 59 | ${{ runner.os }}-build-${{ env.cache-name }}- 60 | ${{ runner.os }}-build- 61 | ${{ runner.os }}- 62 | - run: yarn install --frozen-lockfile 63 | - name: Run tests 64 | run: | 65 | yarn run compile 66 | yarn run test 67 | env: 68 | MAINNET_PK: ${{ secrets.MAINNET_PK }} 69 | MAINNET_URL: ${{ secrets.MAINNET_URL }} 70 | 71 | verify_proposed_uav: 72 | name: Verify Proposed UAV 73 | environment: ci 74 | runs-on: ubuntu-latest 75 | steps: 76 | - name: Setup node 77 | uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 78 | with: 79 | node-version: "16" 80 | - name: Checkout the repo 81 | uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 82 | - name: Yarn cache 83 | uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 84 | env: 85 | cache-name: yarn-cache 86 | with: 87 | path: | 88 | ~/.npm 89 | ~/.cache 90 | **/node_modules 91 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('yarn.lock') }} 92 | restore-keys: | 93 | ${{ runner.os }}-build-${{ env.cache-name }}- 94 | ${{ runner.os }}-build- 95 | ${{ runner.os }}- 96 | - run: yarn install --frozen-lockfile 97 | - name: Run tests 98 | run: | 99 | yarn compile 100 | yarn verify-uav --production 0x65c816077c29b557bee980ae3cc2dce80204a0c5 --proposed 0x50ce56A3239671Ab62f185704Caedf626352741e 101 | env: 102 | MAINNET_PK: ${{ secrets.MAINNET_PK }} 103 | MAINNET_URL: ${{ secrets.MAINNET_URL }} 104 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .dockerbuild 2 | .dockerbuild_cp 3 | *node_modules* 4 | *.tsbuilt* 5 | *.DS_Store* 6 | .build/contracts.json 7 | .build/contracts-trace.json 8 | .build/test.json 9 | .build/development.json 10 | yarn-error.log 11 | junit.xml 12 | coverage/* 13 | .saddle_history 14 | coverage.json 15 | .env 16 | 17 | # Hardhat 18 | cache 19 | artifacts 20 | etherscan/ 21 | 22 | # Typechain generated types 23 | types 24 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "compound-config"] 2 | path = compound-config 3 | url = https://github.com/compound-finance/compound-config.git 4 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16 2 | -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | skipFiles: [ 3 | "test/MockChainlinkOCRAggregator.sol", 4 | "test/UniswapV3SwapHelper.sol", 5 | "Uniswap/UniswapLib.sol", 6 | ], 7 | mocha: { 8 | grep: "@skip-on-coverage", // skip everything with this tag 9 | invert: true, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /.soliumrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solium:recommended", 3 | "plugins": [ 4 | "security" 5 | ], 6 | "rules": { 7 | "quotes": [ 8 | "error", 9 | "double" 10 | ], 11 | "indentation": [ 12 | "error", 13 | 4 14 | ] 15 | } 16 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2019 Compound Labs, Inc. https://compound.finance 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Open Oracle 2 | 3 | The Open Oracle is a standard and SDK allowing reporters to sign key-value pairs (e.g. a price feed) that interested users can post to the blockchain. The system has a built-in view system that allows clients to easily share data and build aggregates (e.g. the median price from several sources). 4 | 5 | ## Contents 6 | 7 | - [Install](#install) 8 | - [Deploy](#deploy) 9 | - [Verify](#verify) 10 | - [Manual Verification](#manual-verification) 11 | - [Transfer Ownership](#transfer-ownership) 12 | - [Common Issues](#common-issues) 13 | 14 | ## Install 15 | 16 | 1. Clone the repo 17 | 2. Install dependencies 18 | 19 | ```sh 20 | yarn install 21 | ``` 22 | 23 | 3. Copy the contents of `.env.example` to `.env` 24 | 25 | ```sh 26 | cp .env.example .env 27 | ``` 28 | 29 | 4. Edit `.env` with your values 30 | 31 | ## Compiling & Testing 32 | 33 | 1. To compile: 34 | 35 | ```sh 36 | yarn run compile 37 | ``` 38 | 39 | 2. To run tests: 40 | 41 | ```sh 42 | yarn run test 43 | ``` 44 | 45 | 3. To run test coverage: 46 | 47 | ```sh 48 | yarn run coverage 49 | ``` 50 | 51 | ## Deploy 52 | 53 | The UAV is deployed using constructor parameters defined in `./configuration/parameters.js`. If new markets need to be added, they should be added to this file first. Read more about how to add new markets in the [configuration README](./configuration/). 54 | 55 | 1. Configure constructor params in `./configuration/parameters.js` 56 | 2. Test in a local fork of mainnet: 57 | 58 | ```sh 59 | yarn deploy-test 60 | ``` 61 | 62 | 3. Deploy the UAV to mainnet: 63 | 64 | ```sh 65 | yarn deploy 66 | ``` 67 | 68 | This will output the address where the UAV was deployed. Keep this to verify on Etherscan. 69 | 70 | ## Verify 71 | 72 | Use the address from the previous step as a positional parameter in the following command: 73 | 74 | ```sh 75 | yarn verify 76 | ``` 77 | 78 | ### Manual Verification 79 | 80 | A known issue is that the Etherscan API has a limit on the amount of data it accepts. You may see an error like this: 81 | 82 | `hardhat-etherscan constructor arguments exceeds max accepted (10k chars) length` 83 | 84 | If so, it means verification must be performed through the UI manually: 85 | 86 | 1. Generate Standard JSON Input by running: 87 | 88 | ```sh 89 | yarn verify-manual 90 | ``` 91 | 92 | This will create a file in this project at `./etherscan/verify.json` 93 | 94 | 2. Navigate to the contract in your browser `https://etherscan.io/address/` 95 | 3. Click `Contract` tab, then click `Verify and Publish` 96 | 4. In the form, the address should already be filled. Fill in the following, and submit: 97 | 5. Compiler type: `Solidity (Standard-Json-Input)` 98 | 6. Compiler Version: `v0.8.7+...` 99 | 7. License: `GNU GPLv3` 100 | 8. Upload `etherscan/verify.json` and submit. 101 | 102 | ## Transfer Ownership 103 | 104 | Use the UAV address as a positional parameter in the same way as the verify step. The COMP_MULTISIG address from .env will be the new proposed owner. Run the following command: 105 | 106 | ```sh 107 | yarn transfer 108 | ``` 109 | 110 | ## Verify Proposed UAV Prices 111 | 112 | We need to verify that the newly deployed UAV is functioning correctly before a new proposal is submitted to upgrade the existing UAV. A simple 113 | way to do this is to verify that both the existing and new UAVs returns the same price when the `getUnderlyingPrice` function is called for each of the configured cTokens. This verification 114 | can be done by running the following command. 115 | 116 | ``` 117 | yarn verify-uav --production PRODUCTION_UAV_ADDR --proposed PROPOSED_UAV_ADDR 118 | 119 | e.g 120 | yarn verify-uav --production 0x65c816077c29b557bee980ae3cc2dce80204a0c5 --proposed 0x50ce56A3239671Ab62f185704Caedf626352741e 121 | ``` 122 | 123 | Sample successful verification output 124 | 125 | ``` 126 | Comparing prices for cToken cETH with address 0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5 127 | Underlying prices for cETH match. 128 | Comparing prices for cToken cDAI with address 0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643 129 | Underlying prices for cDAI match. 130 | Comparing prices for cToken cUSDC with address 0x39AA39c021dfbaE8faC545936693aC917d5E7563 131 | Underlying prices for cUSDC match. 132 | Comparing prices for cToken cUSDT with address 0xf650C3d88D12dB855b8bf7D11Be6C55A4e07dCC9 133 | Underlying prices for cUSDT match. 134 | Comparing prices for cToken cWBTC with address 0xccF4429DB6322D5C611ee964527D42E5d685DD6a 135 | Underlying prices for cWBTC match. 136 | Comparing prices for cToken cBAT with address 0x6C8c6b02E7b2BE14d4fA6022Dfd6d75921D90E4E 137 | Underlying prices for cBAT match. 138 | Comparing prices for cToken cZRX with address 0xB3319f5D18Bc0D84dD1b4825Dcde5d5f7266d407 139 | Underlying prices for cZRX match. 140 | Comparing prices for cToken cREP with address 0x158079Ee67Fce2f58472A96584A73C7Ab9AC95c1 141 | Underlying prices for cREP match. 142 | Comparing prices for cToken cDAI with address 0xF5DCe57282A584D2746FaF1593d3121Fcac444dC 143 | Underlying prices for cDAI match. 144 | Comparing prices for cToken cUNI with address 0x35A18000230DA775CAc24873d00Ff85BccdeD550 145 | Underlying prices for cUNI match. 146 | Comparing prices for cToken cCOMP with address 0x70e36f6BF80a52b3B46b3aF8e106CC0ed743E8e4 147 | Underlying prices for cCOMP match. 148 | Comparing prices for cToken cLINK with address 0xFAce851a4921ce59e912d19329929CE6da6EB0c7 149 | Underlying prices for cLINK match. 150 | Comparing prices for cToken cTUSD with address 0x12392F67bdf24faE0AF363c24aC620a2f67DAd86 151 | Underlying prices for cTUSD match. 152 | Comparing prices for cToken cAAVE with address 0xe65cdB6479BaC1e22340E4E755fAE7E509EcD06c 153 | Underlying prices for cAAVE match. 154 | Comparing prices for cToken cSUSHI with address 0x4B0181102A0112A2ef11AbEE5563bb4a3176c9d7 155 | Underlying prices for cSUSHI match. 156 | Comparing prices for cToken cMKR with address 0x95b4eF2869eBD94BEb4eEE400a99824BF5DC325b 157 | Underlying prices for cMKR match. 158 | Comparing prices for cToken cYFI with address 0x80a2AE356fc9ef4305676f7a3E2Ed04e12C33946 159 | Underlying prices for cYFI match. 160 | Comparing prices for cToken cWBTC with address 0xC11b1268C1A384e55C48c2391d8d480264A3A7F4 161 | Underlying prices for cWBTC match. 162 | Comparing prices for cToken cUSDP with address 0x041171993284df560249B57358F931D9eB7b925D 163 | Underlying prices for cUSDP match. 164 | Comparing prices for cToken cFEI with address 0x7713DD9Ca933848F6819F38B8352D9A15EA73F67 165 | Underlying prices for cFEI match. 166 | Proposed UAV at 0x50ce56A3239671Ab62f185704Caedf626352741e passed all checks with the production UAV at 0x65c816077c29b557bee980ae3cc2dce80204a0c5! 167 | ``` 168 | 169 | The script will fail if the proposed UAV contract either reverts or returns a different price from the production UAV contract. 170 | 171 | ## Common Issues 172 | 173 | ### Failure to deploy 174 | 175 | If deployment fails with an unhelpful GAS error, it usually means that something failed during the UAV's complex construction. The most common problem is incorrect uniswap config. If the `uniswapMarket` address is not a Uniswap V2 pool, construction will fail. Double check the address, and whether the pool needs to be reversed. More info on this in the [configuration README](./configuration/). 176 | 177 | ## Reporter SDK 178 | 179 | This repository contains a set of SDKs for reporters to easily sign "reporter" data in any supported languages. We currently support the following languages: 180 | 181 | - [JavaScript](./sdk/javascript/README.md) (in TypeScript) 182 | - [Elixir](./sdk/typescript/README.md) 183 | 184 | ## Poster 185 | 186 | The poster is a simple application that reads from a given feed (or set of feeds) and posts... 187 | 188 | ## Contributing 189 | 190 | Note: all code contributed to this repository must be licensed under each of 1. MIT, 2. BSD-3, and 3. GPLv3. By contributing code to this repository, you accept that your code is allowed to be released under any or all of these licenses or licenses in substantially similar form to these listed above. 191 | 192 | Please submit an issue (or create a pull request) for any issues or contributions to the project. Make sure that all test cases pass, including the integration tests in the root of this project. 193 | -------------------------------------------------------------------------------- /configuration/README.md: -------------------------------------------------------------------------------- 1 | # UAV Configuration 2 | 3 | `parameters.js` details the constructor parameters that are passed in when deploying the UAV. 4 | 5 | These fields must be known prior to deployment. 6 | 7 | - [anchorToleranceMantissa\_](#anchortolerancemantissa_) 8 | - [anchorPeriod\_](#anchorperiod_) 9 | - [configs](#configs) 10 | 11 | ## anchorToleranceMantissa\_ 12 | 13 | TYPE: `uint256` 14 | 15 | The precentage tolerance for the Uniswap anchor. 16 | 17 | For 15%, this equals: `150000000000000000` 18 | 19 | ## anchorPeriod\_ 20 | 21 | TYPE: `uint256` 22 | 23 | Anchor window size. 24 | 25 | For 30 minutes, this equals: `1800` 26 | 27 | ## configs 28 | 29 | The configs parameter is an array of `TokenConfig` structs which contains the following fields: 30 | 31 | - [cToken](#ctoken) 32 | - [underlying](#underlying) 33 | - [symbolHash](#symbolhash) 34 | - [baseUnit](#baseunit) 35 | - [priceSource](#pricesource) 36 | - [fixedPrice](#fixedprice) 37 | - [uniswapMarket](#uniswapmarket) 38 | - [reporter](#reporter) 39 | - [reporterMultiplier](#reportermultiplier) 40 | - [isUniswapReversed](#isuniswapreversed) 41 | 42 | ### cToken 43 | 44 | TYPE: `address` 45 | 46 | The address of the Compound interest baring token. For the `LINK` market configuration, this would be the address of the `cLINK` token. 47 | 48 | ### underlying 49 | 50 | TYPE: `address` 51 | 52 | The address of the underlying market token. For this `LINK` market configuration, this would be the address of the `LINK` token. 53 | 54 | ### symbolHash 55 | 56 | TYPE: `bytes32` 57 | 58 | The bytes32 hash of the underlying symbol. For the `LINK` market configuration, this would be `0x921a3539bcb764c889432630877414523e7fbca00c211bc787aeae69e2e3a779`, calculated using: 59 | 60 | ```javascript 61 | keccak256("LINK"); 62 | ``` 63 | 64 | ### baseUnit 65 | 66 | TYPE: `uint256` 67 | 68 | The number of smallest units of measurement in a single whole unit. For example: 1 ETH has 1e18 WEI, therefore the baseUnit of ETH is 1e18 (1,000,000,000,000,000,000) 69 | 70 | ### priceSource 71 | 72 | TYPE: `enum PriceSource` (defined as an integer) 73 | 74 | Options: 75 | 76 | - 0 - FIXED_ETH - The `fixedPrice` is a constant multiple of ETH price 77 | - 1 - FIXED_USD - The `fixedPrice` is a constant multiple of USD price, which is 1 78 | - 2 - REPORTER - The price is set by the `reporter` (most common. CL price feed `ValidatorProxy`s are the reporters) 79 | 80 | ### fixedPrice 81 | 82 | TYPE: `uint256` 83 | 84 | The fixed price multiple of either ETH or USD, depending on the `priceSource`. If `priceSource` is `reporter`, this is unused. 85 | 86 | ### uniswapMarket 87 | 88 | TYPE: `address` 89 | 90 | The address of the pool being used as the anchor for this market. 91 | 92 | ### reporter 93 | 94 | TYPE: `address` 95 | 96 | The address of the `ValidatorProxy` acting as the reporter 97 | 98 | ### reporterMultiplier 99 | 100 | TYPE: `uint256` 101 | 102 | Prices reported by a `ValidatorProxy` must be transformed to 6 decimals for the UAV. Chainlink USD pairs are usually 8 decimals, which is two decimals too many. This field is used to transform the price, using this equation: 103 | 104 | ```javascript 105 | price = (reportedPrice * reporterMultiplier) / 1e18; 106 | ``` 107 | 108 | Using Chainlink USD pairs as an example, and `reportedPrice = 2000000000` ($20): 109 | 110 | ```javascript 111 | 20000000 = (2000000000 * reporterMultiplier) / 1e18 112 | reporterMultiplier = 10000000000000000 113 | ``` 114 | 115 | ### isUniswapReversed 116 | 117 | TYPE: `bool` 118 | 119 | If the `uniswapMarket` pair is X / ETH, this is `false`. 120 | If the `uniswapMarket` pair is ETH / X, this is `true`. 121 | 122 | # PriceOracle Configuration 123 | 124 | [`parameters-price-oracle.js`](./parameters-price-oracle.ts) details the constructor parameters that are passed in when deploying the PriceOracle. 125 | 126 | These fields must be known prior to deployment. 127 | 128 | - [configs](#configs-1) 129 | 130 | ## configs 131 | 132 | The configs parameter is an array of `TokenConfig` structs which contains the following fields: 133 | 134 | - [cToken](#ctoken) 135 | - [underlyingAssetDecimals](#underlyingassetdecimals) 136 | - [priceFeed](#pricefeed) 137 | 138 | ### cToken 139 | 140 | TYPE: `address` 141 | 142 | The address of the Compound interest baring token. For the `LINK` market configuration, this would be the address of the `cLINK` token. 143 | 144 | ### underlyingAssetDecimals 145 | 146 | TYPE: `uint8` 147 | 148 | The decimals of the underlying asset. For `ETH`, the value would be `18`. 149 | 150 | ### priceFeed 151 | 152 | TYPE: `address` 153 | 154 | The address to the Chainlink feed used for the underlying asset's price 155 | -------------------------------------------------------------------------------- /configuration/parameters-price-oracle.ts: -------------------------------------------------------------------------------- 1 | export type TokenConfig = { 2 | // The address of the Compound Token 3 | cToken: string; 4 | // Decimals of the underlying asset 5 | underlyingAssetDecimals: string; 6 | // Address to Chainlink feed used for asset price 7 | priceFeed: string; 8 | // Fixed price for asset 9 | fixedPrice: string; 10 | }; 11 | 12 | const parameters: TokenConfig[] = [ 13 | { 14 | // "NAME": "ETH", 15 | cToken: "0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5", 16 | underlyingAssetDecimals: "18", 17 | priceFeed: "0x5f4ec3df9cbd43714fe2740f5e3616155c5b8419", 18 | fixedPrice: "0", 19 | }, 20 | { 21 | // "NAME": "DAI", 22 | cToken: "0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643", 23 | underlyingAssetDecimals: "18", 24 | priceFeed: "0xaed0c38402a5d19df6e4c03f4e2dced6e29c1ee9", 25 | fixedPrice: "0", 26 | }, 27 | { 28 | // "NAME": "USDC", 29 | cToken: "0x39AA39c021dfbaE8faC545936693aC917d5E7563", 30 | underlyingAssetDecimals: "6", 31 | priceFeed: "0x8fffffd4afb6115b954bd326cbe7b4ba576818f6", 32 | fixedPrice: "0", 33 | }, 34 | { 35 | // "NAME": "USDT", 36 | cToken: "0xf650C3d88D12dB855b8bf7D11Be6C55A4e07dCC9", 37 | underlyingAssetDecimals: "6", 38 | priceFeed: "0x3e7d1eab13ad0104d2750b8863b489d65364e32d", 39 | fixedPrice: "0", 40 | }, 41 | { 42 | // "NAME": "WBTCv2", 43 | cToken: "0xccf4429db6322d5c611ee964527d42e5d685dd6a", 44 | underlyingAssetDecimals: "8", 45 | priceFeed: "0x45939657d1CA34A8FA39A924B71D28Fe8431e581", // Custom Compound Feed 46 | fixedPrice: "0", 47 | }, 48 | { 49 | // "NAME": "WBTC", 50 | cToken: "0xC11b1268C1A384e55C48c2391d8d480264A3A7F4", 51 | underlyingAssetDecimals: "8", 52 | priceFeed: "0x45939657d1CA34A8FA39A924B71D28Fe8431e581", // Custom Compound Feed 53 | fixedPrice: "0", 54 | }, 55 | { 56 | // "NAME": "BAT", 57 | cToken: "0x6C8c6b02E7b2BE14d4fA6022Dfd6d75921D90E4E", 58 | underlyingAssetDecimals: "18", 59 | priceFeed: "0x9441D7556e7820B5ca42082cfa99487D56AcA958", 60 | fixedPrice: "0", 61 | }, 62 | { 63 | // "NAME": "ZRX", 64 | cToken: "0xB3319f5D18Bc0D84dD1b4825Dcde5d5f7266d407", 65 | underlyingAssetDecimals: "18", 66 | priceFeed: "0x2885d15b8af22648b98b122b22fdf4d2a56c6023", 67 | fixedPrice: "0", 68 | }, 69 | { 70 | // "NAME": "UNI", 71 | cToken: "0x35A18000230DA775CAc24873d00Ff85BccdeD550", 72 | underlyingAssetDecimals: "18", 73 | priceFeed: "0x553303d460ee0afb37edff9be42922d8ff63220e", 74 | fixedPrice: "0", 75 | }, 76 | { 77 | // "NAME": "COMP", 78 | cToken: "0x70e36f6BF80a52b3B46b3aF8e106CC0ed743E8e4", 79 | underlyingAssetDecimals: "18", 80 | priceFeed: "0xdbd020caef83efd542f4de03e3cf0c28a4428bd5", 81 | fixedPrice: "0", 82 | }, 83 | { 84 | // "NAME": "LINK", 85 | cToken: "0xFAce851a4921ce59e912d19329929CE6da6EB0c7", 86 | underlyingAssetDecimals: "18", 87 | priceFeed: "0x2c1d072e956affc0d435cb7ac38ef18d24d9127c", 88 | fixedPrice: "0", 89 | }, 90 | { 91 | // "NAME": "TUSD", 92 | cToken: "0x12392F67bdf24faE0AF363c24aC620a2f67DAd86", 93 | underlyingAssetDecimals: "18", 94 | priceFeed: "0xec746ecf986e2927abd291a2a1716c940100f8ba", 95 | fixedPrice: "0", 96 | }, 97 | { 98 | // "NAME": "AAVE", 99 | cToken: "0xe65cdB6479BaC1e22340E4E755fAE7E509EcD06c", 100 | underlyingAssetDecimals: "18", 101 | priceFeed: "0x547a514d5e3769680ce22b2361c10ea13619e8a9", 102 | fixedPrice: "0", 103 | }, 104 | { 105 | // "NAME": "SUSHI", 106 | cToken: "0x4B0181102A0112A2ef11AbEE5563bb4a3176c9d7", 107 | underlyingAssetDecimals: "18", 108 | priceFeed: "0xcc70f09a6cc17553b2e31954cd36e4a2d89501f7", 109 | fixedPrice: "0", 110 | }, 111 | { 112 | // "NAME": "MKR", 113 | cToken: "0x95b4eF2869eBD94BEb4eEE400a99824BF5DC325b", 114 | underlyingAssetDecimals: "18", 115 | priceFeed: "0xec1d1b3b0443256cc3860e24a46f108e699484aa", 116 | fixedPrice: "0", 117 | }, 118 | { 119 | // "NAME": "YFI", 120 | cToken: "0x80a2AE356fc9ef4305676f7a3E2Ed04e12C33946", 121 | underlyingAssetDecimals: "18", 122 | priceFeed: "0xa027702dbb89fbd58938e4324ac03b58d812b0e1", 123 | fixedPrice: "0", 124 | }, 125 | { 126 | // "NAME": "USDP", 127 | cToken: "0x041171993284df560249B57358F931D9eB7b925D", 128 | underlyingAssetDecimals: "18", 129 | priceFeed: "0x09023c0da49aaf8fc3fa3adf34c6a7016d38d5e3", 130 | fixedPrice: "0", 131 | }, 132 | { 133 | // "NAME": "REP", 134 | // Deprecated market 135 | cToken: "0x158079Ee67Fce2f58472A96584A73C7Ab9AC95c1", 136 | underlyingAssetDecimals: "18", 137 | priceFeed: "0x0000000000000000000000000000000000000000", 138 | fixedPrice: "6433680000000000000", 139 | }, 140 | { 141 | // "NAME": "FEI", 142 | // Deprecated market 143 | cToken: "0x7713DD9Ca933848F6819F38B8352D9A15EA73F67", 144 | underlyingAssetDecimals: "18", 145 | priceFeed: "0x0000000000000000000000000000000000000000", 146 | fixedPrice: "1001094000000000000", 147 | }, 148 | { 149 | // "NAME": "SAI", 150 | // Deprecated market 151 | cToken: "0xF5DCe57282A584D2746FaF1593d3121Fcac444dC", 152 | underlyingAssetDecimals: "18", 153 | priceFeed: "0x0000000000000000000000000000000000000000", 154 | fixedPrice: "15544520000000000000", 155 | }, 156 | ]; 157 | 158 | export default parameters; 159 | -------------------------------------------------------------------------------- /configuration/parameters.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | "150000000000000000", 3 | 1800, 4 | [ 5 | { 6 | // "NAME": "ETH", 7 | cToken: "0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5", 8 | underlying: "0x0000000000000000000000000000000000000000", 9 | symbolHash: 10 | "0xaaaebeba3810b1e6b70781f14b2d72c1cb89c0b2b320c43bb67ff79f562f5ff4", 11 | baseUnit: "1000000000000000000", 12 | priceSource: "2", 13 | fixedPrice: "0", 14 | uniswapMarket: "0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8", 15 | reporter: "0x264BDDFD9D93D48d759FBDB0670bE1C6fDd50236", 16 | reporterMultiplier: "10000000000000000", 17 | isUniswapReversed: true, 18 | }, 19 | { 20 | // "NAME": "DAI", 21 | cToken: "0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643", 22 | underlying: "0x6B175474E89094C44Da98b954EedeAC495271d0F", 23 | symbolHash: 24 | "0xa5e92f3efb6826155f1f728e162af9d7cda33a574a1153b58f03ea01cc37e568", 25 | baseUnit: "1000000000000000000", 26 | priceSource: "2", 27 | fixedPrice: "0", 28 | uniswapMarket: "0xc2e9f25be6257c210d7adf0d4cd6e3e881ba25f8", 29 | reporter: "0xb2419f587f497CDd64437f1B367E2e80889631ea", 30 | reporterMultiplier: "10000000000000000", 31 | isUniswapReversed: false, 32 | }, 33 | { 34 | // "NAME": "USDC", 35 | cToken: "0x39AA39c021dfbaE8faC545936693aC917d5E7563", 36 | underlying: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", 37 | symbolHash: 38 | "0xd6aca1be9729c13d677335161321649cccae6a591554772516700f986f942eaa", 39 | baseUnit: "1000000", 40 | priceSource: "1", 41 | fixedPrice: "1000000", 42 | uniswapMarket: "0x0000000000000000000000000000000000000000", 43 | reporter: "0x0000000000000000000000000000000000000000", 44 | reporterMultiplier: "1", 45 | isUniswapReversed: false, 46 | }, 47 | { 48 | // "NAME": "USDT", 49 | cToken: "0xf650C3d88D12dB855b8bf7D11Be6C55A4e07dCC9", 50 | underlying: "0xdAC17F958D2ee523a2206206994597C13D831ec7", 51 | symbolHash: 52 | "0x8b1a1d9c2b109e527c9134b25b1a1833b16b6594f92daa9f6d9b7a6024bce9d0", 53 | baseUnit: "1000000", 54 | priceSource: "1", 55 | fixedPrice: "1000000", 56 | uniswapMarket: "0x0000000000000000000000000000000000000000", 57 | reporter: "0x0000000000000000000000000000000000000000", 58 | reporterMultiplier: "1", 59 | isUniswapReversed: false, 60 | }, 61 | { 62 | // "NAME": "WBTCv2", 63 | cToken: "0xccf4429db6322d5c611ee964527d42e5d685dd6a", 64 | underlying: "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", 65 | symbolHash: 66 | "0xe98e2830be1a7e4156d656a7505e65d08c67660dc618072422e9c78053c261e9", 67 | baseUnit: "100000000", 68 | priceSource: "2", 69 | fixedPrice: "0", 70 | uniswapMarket: "0xcbcdf9626bc03e24f779434178a73a0b4bad62ed", 71 | reporter: "0x4846efc15CC725456597044e6267ad0b3B51353E", 72 | reporterMultiplier: "1000000", 73 | isUniswapReversed: false, 74 | }, 75 | { 76 | // "NAME": "BAT", 77 | cToken: "0x6C8c6b02E7b2BE14d4fA6022Dfd6d75921D90E4E", 78 | underlying: "0x0D8775F648430679A709E98d2b0Cb6250d2887EF", 79 | symbolHash: 80 | "0x3ec6762bdf44eb044276fec7d12c1bb640cb139cfd533f93eeebba5414f5db55", 81 | baseUnit: "1000000000000000000", 82 | priceSource: "2", 83 | fixedPrice: "0", 84 | uniswapMarket: "0xae614a7a56cb79c04df2aeba6f5dab80a39ca78e", 85 | reporter: "0xeBa6F33730B9751a8BA0b18d9C256093E82f6bC2", 86 | reporterMultiplier: "10000000000000000", 87 | isUniswapReversed: false, 88 | }, 89 | { 90 | // "NAME": "ZRX", 91 | cToken: "0xB3319f5D18Bc0D84dD1b4825Dcde5d5f7266d407", 92 | underlying: "0xE41d2489571d322189246DaFA5ebDe1F4699F498", 93 | symbolHash: 94 | "0xb8612e326dd19fc983e73ae3bc23fa1c78a3e01478574fa7ceb5b57e589dcebd", 95 | baseUnit: "1000000000000000000", 96 | priceSource: "2", 97 | fixedPrice: "0", 98 | uniswapMarket: "0x14424eeecbff345b38187d0b8b749e56faa68539", 99 | reporter: "0x5c5db112c98dbe5977A4c37AD33F8a4c9ebd5575", 100 | reporterMultiplier: "10000000000000000", 101 | isUniswapReversed: true, 102 | }, 103 | { 104 | // "NAME": "REP", 105 | // Warning: as of 2021-09-13, this has very low liquidity ($33.6k) 106 | cToken: "0x158079Ee67Fce2f58472A96584A73C7Ab9AC95c1", 107 | underlying: "0x1985365e9f78359a9B6AD760e32412f4a445E862", 108 | symbolHash: 109 | "0x91a08135082b0a28b4ad8ecc7749a009e0408743a9d1cdf34dd6a58d60ee9504", 110 | baseUnit: "1000000000000000000", 111 | priceSource: "2", 112 | fixedPrice: "0", 113 | uniswapMarket: "0xb055103b7633b61518cd806d95beeb2d4cd217e7", 114 | reporter: "0x90655316479383795416B615B61282C72D8382C1", 115 | reporterMultiplier: "10000000000000000", 116 | isUniswapReversed: false, 117 | }, 118 | { 119 | // "NAME": "SAI", 120 | cToken: "0xF5DCe57282A584D2746FaF1593d3121Fcac444dC", 121 | underlying: "0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359", 122 | symbolHash: 123 | "0x4dcbfd8d7239a822743634e138b90febafc5720cec2dbdc6a0e5a2118ba2c532", 124 | baseUnit: "1000000000000000000", 125 | priceSource: "0", 126 | fixedPrice: "5285000000000000", 127 | uniswapMarket: "0x0000000000000000000000000000000000000000", 128 | reporter: "0x0000000000000000000000000000000000000000", 129 | reporterMultiplier: "1", 130 | isUniswapReversed: false, 131 | }, 132 | { 133 | // "NAME": "UNI", 134 | cToken: "0x35A18000230DA775CAc24873d00Ff85BccdeD550", 135 | underlying: "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984", 136 | symbolHash: 137 | "0xfba01d52a7cd84480d0573725899486a0b5e55c20ff45d6628874349375d1650", 138 | baseUnit: "1000000000000000000", 139 | priceSource: "2", 140 | fixedPrice: "0", 141 | uniswapMarket: "0x1d42064fc4beb5f8aaf85f4617ae8b3b5b8bd801", 142 | reporter: "0x70f4D236FD678c9DB41a52d28f90E299676d9D90", 143 | reporterMultiplier: "10000000000000000", 144 | isUniswapReversed: false, 145 | }, 146 | { 147 | // "NAME": "COMP", 148 | cToken: "0x70e36f6BF80a52b3B46b3aF8e106CC0ed743E8e4", 149 | underlying: "0xc00e94Cb662C3520282E6f5717214004A7f26888", 150 | symbolHash: 151 | "0xb6dbcaeee318e11fe1e87d4af04bdd7b4d6a3f13307225dc7ee72f7c085ab454", 152 | baseUnit: "1000000000000000000", 153 | priceSource: "2", 154 | fixedPrice: "0", 155 | uniswapMarket: "0xea4ba4ce14fdd287f380b55419b1c5b6c3f22ab6", 156 | reporter: "0xE270B8E9d7a7d2A7eE35a45E43d17D56b3e272b1", 157 | reporterMultiplier: "10000000000000000", 158 | isUniswapReversed: false, 159 | }, 160 | { 161 | // "NAME": "LINK", 162 | cToken: "0xFAce851a4921ce59e912d19329929CE6da6EB0c7", 163 | underlying: "0x514910771AF9Ca656af840dff83E8264EcF986CA", 164 | symbolHash: 165 | "0x921a3539bcb764c889432630877414523e7fbca00c211bc787aeae69e2e3a779", 166 | baseUnit: "1000000000000000000", 167 | priceSource: "2", 168 | fixedPrice: "0", 169 | uniswapMarket: "0xa6cc3c2531fdaa6ae1a3ca84c2855806728693e8", 170 | reporter: "0xBcFd9b1a97cCD0a3942f0408350cdc281cDCa1B1", 171 | reporterMultiplier: "10000000000000000", 172 | isUniswapReversed: false, 173 | }, 174 | { 175 | // "NAME": "TUSD", 176 | cToken: "0x12392F67bdf24faE0AF363c24aC620a2f67DAd86", 177 | underlying: "0x0000000000085d4780B73119b644AE5ecd22b376", 178 | symbolHash: 179 | "0xa1b8d8f7e538bb573797c963eeeed40d0bcb9f28c56104417d0da1b372ae3051", 180 | baseUnit: "1000000000000000000", 181 | priceSource: "1", 182 | fixedPrice: "1000000", 183 | uniswapMarket: "0x0000000000000000000000000000000000000000", 184 | reporter: "0x0000000000000000000000000000000000000000", 185 | reporterMultiplier: "1", 186 | isUniswapReversed: false, 187 | }, 188 | { 189 | // "NAME": "AAVE", 190 | cToken: "0xe65cdB6479BaC1e22340E4E755fAE7E509EcD06c", 191 | underlying: "0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9", 192 | symbolHash: 193 | "0xde46fbfa339d54cd65b79d8320a7a53c78177565c2aaf4c8b13eed7865e7cfc8", 194 | baseUnit: "1000000000000000000", 195 | priceSource: "2", 196 | fixedPrice: "0", 197 | uniswapMarket: "0x5ab53ee1d50eef2c1dd3d5402789cd27bb52c1bb", 198 | reporter: "0x0238247E71AD0aB272203Af13bAEa72e99EE7c3c", 199 | reporterMultiplier: "10000000000000000", 200 | isUniswapReversed: false, 201 | }, 202 | { 203 | // "NAME": "SUSHI", 204 | cToken: "0x4B0181102A0112A2ef11AbEE5563bb4a3176c9d7", 205 | underlying: "0x6B3595068778DD592e39A122f4f5a5cF09C90fE2", 206 | symbolHash: 207 | "0xbbf304add43db0a05d104474683215530b076be1dfdf72a4d53a1e443d8e4c21", 208 | baseUnit: "1000000000000000000", 209 | priceSource: "2", 210 | fixedPrice: "0", 211 | uniswapMarket: "0x73a6a761fe483ba19debb8f56ac5bbf14c0cdad1", 212 | reporter: "0x1A6aA40170118bAf36BAc82214DC5681Af69b0cF", 213 | reporterMultiplier: "10000000000000000", 214 | isUniswapReversed: false, 215 | }, 216 | { 217 | // "NAME": "MKR", 218 | cToken: "0x95b4eF2869eBD94BEb4eEE400a99824BF5DC325b", 219 | underlying: "0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2", 220 | symbolHash: 221 | "0xec76ec3a7e5f010a9229e69fa1945af6f0c6cc5b0a625bf03bd6381222192020", 222 | baseUnit: "1000000000000000000", 223 | priceSource: "2", 224 | fixedPrice: "0", 225 | uniswapMarket: "0xe8c6c9227491c0a8156a0106a0204d881bb7e531", 226 | reporter: "0xbA895504a8E286691E7dacFb47ae8A3A737e2Ce1", 227 | reporterMultiplier: "10000000000000000", 228 | isUniswapReversed: false, 229 | }, 230 | { 231 | // "NAME": "YFI", 232 | cToken: "0x80a2AE356fc9ef4305676f7a3E2Ed04e12C33946", 233 | underlying: "0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e", 234 | symbolHash: 235 | "0xec34391362c28ee226b3b8624a699ee507a40fa771fd01d38b03ac7b70998bbe", 236 | baseUnit: "1000000000000000000", 237 | priceSource: "2", 238 | fixedPrice: "0", 239 | uniswapMarket: "0x04916039b1f59d9745bf6e0a21f191d1e0a84287", 240 | reporter: "0xBa4319741782151D2B1df4799d757892EFda4165", 241 | reporterMultiplier: "10000000000000000", 242 | isUniswapReversed: false, 243 | }, 244 | { 245 | // "NAME": "WBTC", 246 | cToken: "0xC11b1268C1A384e55C48c2391d8d480264A3A7F4", 247 | underlying: "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", 248 | symbolHash: 249 | "0xe98e2830be1a7e4156d656a7505e65d08c67660dc618072422e9c78053c261e9", 250 | baseUnit: "100000000", 251 | priceSource: "2", 252 | fixedPrice: "0", 253 | uniswapMarket: "0xcbcdf9626bc03e24f779434178a73a0b4bad62ed", 254 | reporter: "0x4846efc15CC725456597044e6267ad0b3B51353E", 255 | reporterMultiplier: "1000000", 256 | isUniswapReversed: false, 257 | }, 258 | { 259 | // "NAME": "USDP", 260 | cToken: "0x041171993284df560249B57358F931D9eB7b925D", 261 | underlying: "0x8E870D67F660D95d5be530380D0eC0bd388289E1", 262 | symbolHash: 263 | "0xe6ce7ecb96a43fc15fb4020f93c37885612803dd74366bb6815e4f607ac3ca20", 264 | baseUnit: "1000000000000000000", 265 | priceSource: "1", 266 | fixedPrice: "1000000", 267 | uniswapMarket: "0x0000000000000000000000000000000000000000", 268 | reporter: "0x0000000000000000000000000000000000000000", 269 | reporterMultiplier: "1", 270 | isUniswapReversed: false, 271 | }, 272 | { 273 | // "NAME": "FEI", 274 | cToken: "0x7713DD9Ca933848F6819F38B8352D9A15EA73F67", 275 | underlying: "0x956F47F50A910163D8BF957Cf5846D573E7f87CA", 276 | symbolHash: 277 | "0x58c46f3a00a69ae5a5ce163895c14f8f5b7791333af9fe6e7a73618cb5460913", 278 | baseUnit: "1000000000000000000", 279 | priceSource: "2", 280 | fixedPrice: "0", 281 | uniswapMarket: "0x2028D7Ef0223C45caDBF05E13F1823c1228012BF", 282 | reporter: "0xDe2Fa230d4C05ec0337D7b4fc10e16f5663044B0", 283 | reporterMultiplier: "10000000000000000", 284 | isUniswapReversed: false, 285 | }, 286 | { 287 | // "NAME": "MATIC", 288 | cToken: "0x944dd1c7ce133b75880cee913d513f8c07312393", 289 | underlying: "0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0", 290 | symbolHash: 291 | "0xa6a7de01e8b7ba6a4a61c782a73188d808fc1f3cf5743fadb68a02ed884b594f", 292 | baseUnit: "1000000000000000000", 293 | priceSource: "2", 294 | fixedPrice: "0", 295 | uniswapMarket: "0x290A6a7460B308ee3F19023D2D00dE604bcf5B42", 296 | reporter: "0x44750a79ae69D5E9bC1651E099DFFE1fb8611AbA", 297 | reporterMultiplier: "10000000000000000", 298 | isUniswapReversed: false, 299 | }, 300 | ], 301 | ]; 302 | -------------------------------------------------------------------------------- /contracts/Chainlink/AggregatorValidatorInterface.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.7; 3 | 4 | interface AggregatorValidatorInterface { 5 | function validate(uint256 previousRoundId, 6 | int256 previousAnswer, 7 | uint256 currentRoundId, 8 | int256 currentAnswer) external returns (bool); 9 | } -------------------------------------------------------------------------------- /contracts/Ownable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.7; 3 | 4 | /** 5 | * @notice A contract with helpers for safe contract ownership. 6 | */ 7 | contract Ownable { 8 | 9 | address private ownerAddr; 10 | address private pendingOwnerAddr; 11 | 12 | event OwnershipTransferRequested(address indexed from, address indexed to); 13 | event OwnershipTransferred(address indexed from, address indexed to); 14 | 15 | constructor() { 16 | ownerAddr = msg.sender; 17 | } 18 | 19 | /** 20 | * @notice Allows an owner to begin transferring ownership to a new address, 21 | * pending. 22 | */ 23 | function transferOwnership(address to) external onlyOwner() { 24 | require(to != msg.sender, "Cannot transfer to self"); 25 | 26 | pendingOwnerAddr = to; 27 | 28 | emit OwnershipTransferRequested(ownerAddr, to); 29 | } 30 | 31 | /** 32 | * @notice Allows an ownership transfer to be completed by the recipient. 33 | */ 34 | function acceptOwnership() external { 35 | require(msg.sender == pendingOwnerAddr, "Must be proposed owner"); 36 | 37 | address oldOwner = ownerAddr; 38 | ownerAddr = msg.sender; 39 | pendingOwnerAddr = address(0); 40 | 41 | emit OwnershipTransferred(oldOwner, msg.sender); 42 | } 43 | 44 | /** 45 | * @notice Get the current owner 46 | */ 47 | function owner() public view returns (address) { 48 | return ownerAddr; 49 | } 50 | 51 | /** 52 | * @notice Reverts if called by anyone other than the contract owner. 53 | */ 54 | modifier onlyOwner() { 55 | require(msg.sender == ownerAddr, "Only callable by owner"); 56 | _; 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /contracts/Uniswap/CErc20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity =0.8.7; 4 | 5 | interface CErc20 { 6 | function underlying() external view returns (address); 7 | } 8 | -------------------------------------------------------------------------------- /contracts/Uniswap/UniswapLib.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity =0.8.7; 4 | 5 | // From: https://github.com/Uniswap/uniswap-v3-core 6 | 7 | /// @title FixedPoint96 8 | /// @notice A library for handling binary fixed point numbers, see https://en.wikipedia.org/wiki/Q_(number_format) 9 | /// @dev Used in SqrtPriceMath.sol 10 | library FixedPoint96 { 11 | uint8 internal constant RESOLUTION = 96; 12 | uint256 internal constant Q96 = 0x1000000000000000000000000; 13 | } 14 | 15 | /// @title Contains 512-bit math functions 16 | /// @notice Facilitates multiplication and division that can have overflow of an intermediate value without any loss of precision 17 | /// @dev Handles "phantom overflow" i.e., allows multiplication and division where an intermediate value overflows 256 bits 18 | library FullMath { 19 | /// @notice Calculates floor(a×b÷denominator) with full precision. Throws if result overflows a uint256 or denominator == 0 20 | /// @param a The multiplicand 21 | /// @param b The multiplier 22 | /// @param denominator The divisor 23 | /// @return result The 256-bit result 24 | /// @dev Credit to Remco Bloemen under MIT license https://xn--2-umb.com/21/muldiv 25 | function mulDiv( 26 | uint256 a, 27 | uint256 b, 28 | uint256 denominator 29 | ) internal pure returns (uint256 result) { 30 | // 512-bit multiply [prod1 prod0] = a * b 31 | // Compute the product mod 2**256 and mod 2**256 - 1 32 | // then use the Chinese Remainder Theorem to reconstruct 33 | // the 512 bit result. The result is stored in two 256 34 | // variables such that product = prod1 * 2**256 + prod0 35 | uint256 prod0; // Least significant 256 bits of the product 36 | uint256 prod1; // Most significant 256 bits of the product 37 | assembly { 38 | let mm := mulmod(a, b, not(0)) 39 | prod0 := mul(a, b) 40 | prod1 := sub(sub(mm, prod0), lt(mm, prod0)) 41 | } 42 | 43 | // Handle non-overflow cases, 256 by 256 division 44 | if (prod1 == 0) { 45 | require(denominator > 0); 46 | assembly { 47 | result := div(prod0, denominator) 48 | } 49 | return result; 50 | } 51 | 52 | // Make sure the result is less than 2**256. 53 | // Also prevents denominator == 0 54 | require(denominator > prod1); 55 | 56 | /////////////////////////////////////////////// 57 | // 512 by 256 division. 58 | /////////////////////////////////////////////// 59 | 60 | // Make division exact by subtracting the remainder from [prod1 prod0] 61 | // Compute remainder using mulmod 62 | uint256 remainder; 63 | assembly { 64 | remainder := mulmod(a, b, denominator) 65 | } 66 | // Subtract 256 bit number from 512 bit number 67 | assembly { 68 | prod1 := sub(prod1, gt(remainder, prod0)) 69 | prod0 := sub(prod0, remainder) 70 | } 71 | 72 | // Factor powers of two out of denominator 73 | // Compute largest power of two divisor of denominator. 74 | // Always >= 1. 75 | uint256 twos = denominator & (~denominator + 1); 76 | // Divide denominator by power of two 77 | assembly { 78 | denominator := div(denominator, twos) 79 | } 80 | 81 | // Divide [prod1 prod0] by the factors of two 82 | assembly { 83 | prod0 := div(prod0, twos) 84 | } 85 | // Shift in bits from prod1 into prod0. For this we need 86 | // to flip `twos` such that it is 2**256 / twos. 87 | // If twos is zero, then it becomes one 88 | assembly { 89 | twos := add(div(sub(0, twos), twos), 1) 90 | } 91 | prod0 |= prod1 * twos; 92 | 93 | // Invert denominator mod 2**256 94 | // Now that denominator is an odd number, it has an inverse 95 | // modulo 2**256 such that denominator * inv = 1 mod 2**256. 96 | // Compute the inverse by starting with a seed that is correct 97 | // correct for four bits. That is, denominator * inv = 1 mod 2**4 98 | uint256 inv = (3 * denominator) ^ 2; 99 | // Now use Newton-Raphson iteration to improve the precision. 100 | // Thanks to Hensel's lifting lemma, this also works in modular 101 | // arithmetic, doubling the correct bits in each step. 102 | inv *= 2 - denominator * inv; // inverse mod 2**8 103 | inv *= 2 - denominator * inv; // inverse mod 2**16 104 | inv *= 2 - denominator * inv; // inverse mod 2**32 105 | inv *= 2 - denominator * inv; // inverse mod 2**64 106 | inv *= 2 - denominator * inv; // inverse mod 2**128 107 | inv *= 2 - denominator * inv; // inverse mod 2**256 108 | 109 | // Because the division is now exact we can divide by multiplying 110 | // with the modular inverse of denominator. This will give us the 111 | // correct result modulo 2**256. Since the precoditions guarantee 112 | // that the outcome is less than 2**256, this is the final result. 113 | // We don't need to compute the high bits of the result and prod1 114 | // is no longer required. 115 | result = prod0 * inv; 116 | return result; 117 | } 118 | } 119 | 120 | /// @title Math library for computing sqrt prices from ticks and vice versa 121 | /// @notice Computes sqrt price for ticks of size 1.0001, i.e. sqrt(1.0001^tick) as fixed point Q64.96 numbers. Supports 122 | /// prices between 2**-128 and 2**128 123 | library TickMath { 124 | /// @dev The maximum tick that may be passed to #getSqrtRatioAtTick computed from log base 1.0001 of 2**128 125 | int24 internal constant MIN_TICK = -887272; 126 | int24 internal constant MAX_TICK = -MIN_TICK; 127 | 128 | /// @dev The minimum value that can be returned from #getSqrtRatioAtTick. Equivalent to getSqrtRatioAtTick(MIN_TICK) 129 | uint160 internal constant MIN_SQRT_RATIO = 4295128739; 130 | /// @dev The maximum value that can be returned from #getSqrtRatioAtTick. Equivalent to getSqrtRatioAtTick(MAX_TICK) 131 | uint160 internal constant MAX_SQRT_RATIO = 132 | 1461446703485210103287273052203988822378723970342; 133 | 134 | /// @notice Calculates sqrt(1.0001^tick) * 2^96 135 | /// @dev Throws if |tick| > max tick 136 | /// @param tick The input tick for the above formula 137 | /// @return sqrtPriceX96 A Fixed point Q64.96 number representing the sqrt of the ratio of the two assets (token1/token0) 138 | /// at the given tick 139 | function getSqrtRatioAtTick(int24 tick) 140 | internal 141 | pure 142 | returns (uint160 sqrtPriceX96) 143 | { 144 | uint256 absTick = tick < 0 145 | ? uint256(-int256(tick)) 146 | : uint256(int256(tick)); 147 | require(absTick <= uint256(uint24(MAX_TICK)), "T"); 148 | 149 | uint256 ratio = absTick & 0x1 != 0 150 | ? 0xfffcb933bd6fad37aa2d162d1a594001 151 | : 0x100000000000000000000000000000000; 152 | if (absTick & 0x2 != 0) 153 | ratio = (ratio * 0xfff97272373d413259a46990580e213a) >> 128; 154 | if (absTick & 0x4 != 0) 155 | ratio = (ratio * 0xfff2e50f5f656932ef12357cf3c7fdcc) >> 128; 156 | if (absTick & 0x8 != 0) 157 | ratio = (ratio * 0xffe5caca7e10e4e61c3624eaa0941cd0) >> 128; 158 | if (absTick & 0x10 != 0) 159 | ratio = (ratio * 0xffcb9843d60f6159c9db58835c926644) >> 128; 160 | if (absTick & 0x20 != 0) 161 | ratio = (ratio * 0xff973b41fa98c081472e6896dfb254c0) >> 128; 162 | if (absTick & 0x40 != 0) 163 | ratio = (ratio * 0xff2ea16466c96a3843ec78b326b52861) >> 128; 164 | if (absTick & 0x80 != 0) 165 | ratio = (ratio * 0xfe5dee046a99a2a811c461f1969c3053) >> 128; 166 | if (absTick & 0x100 != 0) 167 | ratio = (ratio * 0xfcbe86c7900a88aedcffc83b479aa3a4) >> 128; 168 | if (absTick & 0x200 != 0) 169 | ratio = (ratio * 0xf987a7253ac413176f2b074cf7815e54) >> 128; 170 | if (absTick & 0x400 != 0) 171 | ratio = (ratio * 0xf3392b0822b70005940c7a398e4b70f3) >> 128; 172 | if (absTick & 0x800 != 0) 173 | ratio = (ratio * 0xe7159475a2c29b7443b29c7fa6e889d9) >> 128; 174 | if (absTick & 0x1000 != 0) 175 | ratio = (ratio * 0xd097f3bdfd2022b8845ad8f792aa5825) >> 128; 176 | if (absTick & 0x2000 != 0) 177 | ratio = (ratio * 0xa9f746462d870fdf8a65dc1f90e061e5) >> 128; 178 | if (absTick & 0x4000 != 0) 179 | ratio = (ratio * 0x70d869a156d2a1b890bb3df62baf32f7) >> 128; 180 | if (absTick & 0x8000 != 0) 181 | ratio = (ratio * 0x31be135f97d08fd981231505542fcfa6) >> 128; 182 | if (absTick & 0x10000 != 0) 183 | ratio = (ratio * 0x9aa508b5b7a84e1c677de54f3e99bc9) >> 128; 184 | if (absTick & 0x20000 != 0) 185 | ratio = (ratio * 0x5d6af8dedb81196699c329225ee604) >> 128; 186 | if (absTick & 0x40000 != 0) 187 | ratio = (ratio * 0x2216e584f5fa1ea926041bedfe98) >> 128; 188 | if (absTick & 0x80000 != 0) 189 | ratio = (ratio * 0x48a170391f7dc42444e8fa2) >> 128; 190 | 191 | if (tick > 0) ratio = type(uint256).max / ratio; 192 | 193 | // this divides by 1<<32 rounding up to go from a Q128.128 to a Q128.96. 194 | // we then downcast because we know the result always fits within 160 bits due to our tick input constraint 195 | // we round up in the division so getTickAtSqrtRatio of the output price is always consistent 196 | sqrtPriceX96 = uint160( 197 | (ratio >> 32) + (ratio % (1 << 32) == 0 ? 0 : 1) 198 | ); 199 | } 200 | } 201 | 202 | interface IUniswapV3Pool { 203 | /// @notice The first of the two tokens of the pool, sorted by address 204 | /// @return The token contract address 205 | function token0() external view returns (address); 206 | 207 | /// @notice The second of the two tokens of the pool, sorted by address 208 | /// @return The token contract address 209 | function token1() external view returns (address); 210 | 211 | /// @notice Returns the cumulative tick and liquidity as of each timestamp `secondsAgo` from the current block timestamp 212 | /// @dev To get a time weighted average tick or liquidity-in-range, you must call this with two values, one representing 213 | /// the beginning of the period and another for the end of the period. E.g., to get the last hour time-weighted average tick, 214 | /// you must call it with secondsAgos = [3600, 0]. 215 | /// @dev The time weighted average tick represents the geometric time weighted average price of the pool, in 216 | /// log base sqrt(1.0001) of token1 / token0. The TickMath library can be used to go from a tick value to a ratio. 217 | /// @param secondsAgos From how long ago each cumulative tick and liquidity value should be returned 218 | /// @return tickCumulatives Cumulative tick values as of each `secondsAgos` from the current block timestamp 219 | /// @return secondsPerLiquidityCumulativeX128s Cumulative seconds per liquidity-in-range value as of each `secondsAgos` from the current block 220 | /// timestamp 221 | function observe(uint32[] calldata secondsAgos) 222 | external 223 | view 224 | returns ( 225 | int56[] memory tickCumulatives, 226 | uint160[] memory secondsPerLiquidityCumulativeX128s 227 | ); 228 | 229 | /// @notice Swap token0 for token1, or token1 for token0 230 | /// @dev The caller of this method receives a callback in the form of IUniswapV3SwapCallback#uniswapV3SwapCallback 231 | /// @param recipient The address to receive the output of the swap 232 | /// @param zeroForOne The direction of the swap, true for token0 to token1, false for token1 to token0 233 | /// @param amountSpecified The amount of the swap, which implicitly configures the swap as exact input (positive), or exact output (negative) 234 | /// @param sqrtPriceLimitX96 The Q64.96 sqrt price limit. If zero for one, the price cannot be less than this 235 | /// value after the swap. If one for zero, the price cannot be greater than this value after the swap 236 | /// @param data Any data to be passed through to the callback 237 | /// @return amount0 The delta of the balance of token0 of the pool, exact when negative, minimum when positive 238 | /// @return amount1 The delta of the balance of token1 of the pool, exact when negative, minimum when positive 239 | function swap( 240 | address recipient, 241 | bool zeroForOne, 242 | int256 amountSpecified, 243 | uint160 sqrtPriceLimitX96, 244 | bytes calldata data 245 | ) external returns (int256 amount0, int256 amount1); 246 | } 247 | 248 | /// @title Callback for IUniswapV3PoolActions#swap 249 | /// @notice Any contract that calls IUniswapV3PoolActions#swap must implement this interface 250 | interface IUniswapV3SwapCallback { 251 | /// @notice Called to `msg.sender` after executing a swap via IUniswapV3Pool#swap. 252 | /// @dev In the implementation you must pay the pool tokens owed for the swap. 253 | /// The caller of this method must be checked to be a UniswapV3Pool deployed by the canonical UniswapV3Factory. 254 | /// amount0Delta and amount1Delta can both be 0 if no tokens were swapped. 255 | /// @param amount0Delta The amount of token0 that was sent (negative) or must be received (positive) by the pool by 256 | /// the end of the swap. If positive, the callback must send that amount of token0 to the pool. 257 | /// @param amount1Delta The amount of token1 that was sent (negative) or must be received (positive) by the pool by 258 | /// the end of the swap. If positive, the callback must send that amount of token1 to the pool. 259 | /// @param data Any data passed through by the caller via the IUniswapV3PoolActions#swap call 260 | function uniswapV3SwapCallback( 261 | int256 amount0Delta, 262 | int256 amount1Delta, 263 | bytes calldata data 264 | ) external; 265 | } 266 | 267 | interface IERC20 { 268 | function transferFrom( 269 | address from, 270 | address to, 271 | uint256 amount 272 | ) external returns (bool success); 273 | } 274 | -------------------------------------------------------------------------------- /contracts/test/MockChainlinkOCRAggregator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity ^0.8.7; 4 | 5 | import "../Chainlink/AggregatorValidatorInterface.sol"; 6 | 7 | contract MockChainlinkOCRAggregator { 8 | AggregatorValidatorInterface public uniswapAnchoredView; 9 | 10 | function setUniswapAnchoredView(address addr) public { 11 | uniswapAnchoredView = AggregatorValidatorInterface(addr); 12 | } 13 | 14 | function validate(int256 latestPrice) public { 15 | uniswapAnchoredView.validate(0, 0, 0, latestPrice); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /contracts/test/UniswapV3SwapHelper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.7; 3 | 4 | import "../Uniswap/UniswapLib.sol"; 5 | 6 | contract UniswapV3SwapHelper is IUniswapV3SwapCallback { 7 | function performSwap( 8 | address pool, 9 | bool zeroForOne, 10 | int256 amountSpecified, 11 | uint160 sqrtPriceLimitX96 12 | ) external returns (int256 amount0Delta, int256 amount1Delta) { 13 | (amount0Delta, amount1Delta) = IUniswapV3Pool(pool).swap( 14 | msg.sender, 15 | zeroForOne, 16 | amountSpecified, 17 | sqrtPriceLimitX96, 18 | abi.encode(msg.sender) 19 | ); 20 | } 21 | 22 | function uniswapV3SwapCallback( 23 | int256 amount0Delta, 24 | int256 amount1Delta, 25 | bytes calldata data 26 | ) external override { 27 | // Caller will be a V3 pool 28 | IUniswapV3Pool pool = IUniswapV3Pool(msg.sender); 29 | address sender = abi.decode(data, (address)); 30 | 31 | if (amount0Delta > 0) { 32 | IERC20(pool.token0()).transferFrom( 33 | sender, 34 | msg.sender, 35 | uint256(amount0Delta) 36 | ); 37 | } else { 38 | IERC20(pool.token1()).transferFrom( 39 | sender, 40 | msg.sender, 41 | uint256(amount1Delta) 42 | ); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | import "@typechain/hardhat"; 4 | import "@nomiclabs/hardhat-waffle"; 5 | import "@nomiclabs/hardhat-etherscan"; 6 | import "hardhat-contract-sizer"; 7 | import "solidity-coverage"; 8 | import { task, HardhatUserConfig } from "hardhat/config"; 9 | import verifyProposedUAVAction from "./tasks/verify-uav"; 10 | import { compareUavAndPriceOracle } from "./tasks/price-oracle-tasks"; 11 | import { deployPriceOracle } from "./tasks/price-oracle-tasks"; 12 | require("dotenv").config(); 13 | 14 | const MAINNET_URL = process.env.MAINNET_URL!; 15 | const MAINNET_PK = process.env.MAINNET_PK!; 16 | const ETHERSCAN = process.env.ETHERSCAN!; 17 | const COMP_MULTISIG = process.env.COMP_MULTISIG!; 18 | 19 | // You need to export an object to set up your config 20 | // Go to https://hardhat.org/config/ to learn more 21 | 22 | function ensureDirectoryExistence(filePath: string) { 23 | var dirname = path.dirname(filePath); 24 | if (fs.existsSync(dirname)) { 25 | return true; 26 | } 27 | ensureDirectoryExistence(dirname); 28 | fs.mkdirSync(dirname); 29 | return true; 30 | } 31 | 32 | task( 33 | "manual-verify", 34 | "Generate the input JSON required to verify the UAV through the Etherscan UI" 35 | ).setAction(async (args, hre) => { 36 | const buildInfoPaths = await hre.artifacts.getBuildInfoPaths(); 37 | const buildInfo = require(buildInfoPaths[0]); 38 | const localOutputPath = "/etherscan/verify.json"; 39 | const fullOutputPath = hre.config.paths.root + localOutputPath; 40 | const outputData = JSON.stringify(buildInfo.input, null, 2); 41 | ensureDirectoryExistence(fullOutputPath); 42 | fs.writeFileSync(fullOutputPath, outputData); 43 | console.log("Etherscan verification JSON written to " + localOutputPath); 44 | }); 45 | 46 | task("transfer-ownership", "Transfer ownership of the UAV to the COMP multisig") 47 | .addParam("contract", "Deployed UAV address") 48 | .setAction(async (args, hre) => { 49 | const Contract = await hre.ethers.getContractFactory( 50 | "contracts/Ownable.sol:Ownable" 51 | ); 52 | const contract = Contract.attach(args.contract); 53 | await contract.transferOwnership(COMP_MULTISIG); 54 | }); 55 | 56 | task( 57 | "verify-proposed-uav", 58 | "Verifies that the proposed UAV returns the correct prices for all cTokens" 59 | ) 60 | .addParam("proposed", "Proposed UAV address") 61 | .addParam("production", "Production UAV address") 62 | .setAction(verifyProposedUAVAction); 63 | 64 | task( 65 | "deploy-price-oracle", 66 | "Deploy the PriceOracle contract to the specified network" 67 | ).setAction(deployPriceOracle); 68 | 69 | task( 70 | "verify-proposed-price-oracle", 71 | "Reports the proposed PriceOracle and UAV prices for all cTokens for verification" 72 | ) 73 | .addParam("proposed", "Proposed PriceOracle address") 74 | .addParam("production", "Production UAV address") 75 | .setAction(compareUavAndPriceOracle); 76 | 77 | const hardhatUserConfig: HardhatUserConfig = { 78 | networks: { 79 | hardhat: { 80 | forking: { 81 | url: MAINNET_URL, 82 | blockNumber: 13152450, 83 | }, 84 | accounts: { 85 | mnemonic: "test test test test test test test test test test test test", 86 | accountsBalance: "1000000009583538498497992", 87 | }, 88 | }, 89 | mainnet: { 90 | url: MAINNET_URL, 91 | accounts: [MAINNET_PK], 92 | }, 93 | }, 94 | solidity: { 95 | version: "0.8.7", 96 | settings: { 97 | optimizer: { 98 | enabled: true, 99 | runs: 200, 100 | }, 101 | outputSelection: { 102 | "*": { 103 | "*": ["storageLayout"], 104 | }, 105 | }, 106 | }, 107 | }, 108 | etherscan: { 109 | apiKey: ETHERSCAN, 110 | }, 111 | typechain: { 112 | outDir: "types", 113 | target: "ethers-v5", 114 | }, 115 | contractSizer: { 116 | alphaSort: true, 117 | runOnCompile: true, 118 | disambiguatePaths: false, 119 | }, 120 | mocha: { 121 | timeout: 60000, 122 | }, 123 | }; 124 | 125 | module.exports = hardhatUserConfig; 126 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "compound-open-oracle", 3 | "version": "1.1.1", 4 | "description": "The Compound Open Oracle", 5 | "main": "index.js", 6 | "repository": "https://github.com/compound-finance/open-oracle", 7 | "author": "Compound Labs, Inc.", 8 | "license": "MIT", 9 | "dependencies": {}, 10 | "devDependencies": { 11 | "@chainlink/contracts": "0.8.0", 12 | "@defi-wonderland/smock": "^2.0.3", 13 | "@nomiclabs/ethereumjs-vm": "^4.2.2", 14 | "@nomiclabs/hardhat-ethers": "^2.0.2", 15 | "@nomiclabs/hardhat-etherscan": "^2.1.6", 16 | "@nomiclabs/hardhat-waffle": "^2.0.1", 17 | "@openzeppelin/contracts": "4.9.0", 18 | "@typechain/ethers-v5": "^7.0.1", 19 | "@typechain/hardhat": "^2.3.0", 20 | "@types/chai": "^4.2.21", 21 | "@types/mocha": "^9.0.0", 22 | "@types/node": "^16.7.10", 23 | "@uniswap/v3-core": "^1.0.0", 24 | "@uniswap/v3-sdk": "^3.3.2", 25 | "chai": "^4.3.4", 26 | "dotenv": "^10.0.0", 27 | "ethereum-waffle": "^3.4.0", 28 | "ethers": "^5.4.6", 29 | "hardhat": "^2.6.2", 30 | "hardhat-contract-sizer": "^2.0.3", 31 | "istanbul": "^0.4.5", 32 | "jest": "^24.9.0", 33 | "jest-cli": "^24.9.0", 34 | "jest-junit": "^10.0.0", 35 | "jsbi": "^3.2.1", 36 | "prettier": "^2.3.2", 37 | "solidity-coverage": "^0.7.18", 38 | "ts-node": "^10.2.1", 39 | "typechain": "^5.1.2", 40 | "typechain-common-abi": "^0.1.2", 41 | "typescript": "^4.4.2" 42 | }, 43 | "scripts": { 44 | "test": "yarn hardhat test", 45 | "coverage": "yarn hardhat coverage --network hardhat", 46 | "compile": "yarn hardhat compile", 47 | "deploy": "yarn hardhat run ./scripts/deploy-uav.js --network mainnet", 48 | "deploy-test": "yarn hardhat run ./scripts/deploy-uav.js --network hardhat", 49 | "deploy-price-oracle": "yarn hardhat deploy-price-oracle --network mainnet", 50 | "verify-uav": "yarn hardhat verify-proposed-uav --network mainnet", 51 | "verify-price-oracle": "yarn hardhat verify-proposed-price-oracle --network mainnet", 52 | "verify": "yarn hardhat verify --network mainnet --constructor-args", 53 | "verify-manual": "yarn hardhat manual-verify", 54 | "transfer": "yarn hardhat transfer-ownership --network mainnet --contract", 55 | "size-contracts": "yarn hardhat size-contracts" 56 | }, 57 | "resolutions": { 58 | "**/ganache-core": "https://github.com/compound-finance/ganache-core.git#compound", 59 | "scrypt.js": "https://registry.npmjs.org/@compound-finance/ethereumjs-wallet/-/ethereumjs-wallet-0.6.3.tgz", 60 | "**/sha3": "^2.1.2" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /poster/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .tsbuilt 3 | -------------------------------------------------------------------------------- /poster/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:13.12.0-alpine3.11 2 | 3 | RUN apk update && apk add --no-cache --virtual .gyp \ 4 | python \ 5 | make \ 6 | g++ \ 7 | yarn \ 8 | nodejs \ 9 | git 10 | 11 | WORKDIR /open-oracle-poster 12 | RUN yarn global add node-gyp npx 13 | ADD yarn.lock package.json /open-oracle-poster/ 14 | ADD . /open-oracle-poster 15 | RUN yarn install --frozen-lockfile 16 | 17 | CMD yarn start 18 | -------------------------------------------------------------------------------- /poster/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## The Open Price Feed Poster 3 | 4 | The poster is a simple application to pull prices from a set of source feeds and post them to the blockchain. The posters main job is to make sure that the requested source data is posted to the Ethereum blockchain, and thus is concerned about nonces, gas prices, etc. 5 | 6 | ## Installation 7 | 8 | The Open Price Feed poster can be run as a standalone process or as a module for configuration. To install as a global process: 9 | 10 | ```sh 11 | yarn global add open-oracle-poster 12 | # npm install -g open-oracle-poster 13 | ``` 14 | 15 | Or, if you plan on customizing the poster, you can install in a project: 16 | 17 | ```sh 18 | yarn add open-oracle-poster 19 | # npm install open-oracle-poster 20 | ``` 21 | 22 | ## Command-Line Interface 23 | 24 | The poster is a simple CLI to post prices. The following options are available: 25 | 26 | | Option | Description | 27 | | ------ | ----------- | 28 | | `--sources`, `-s` | sources to pull price messages from, a list of https endpoints created by open oracle reporters serving open oracle payloads as json. For complex sources, such as Coinbase, this can be JSON-encoded. Note: specify multiple times to specify multiple sources. | 29 | | `--poster-key`, `-k` | Private key holding enough gas to post (try: `file:` or `env:`) | 30 | | `--view-function`, `-f` | Function signature for the view (e.g. postPrices(bytes[],bytes[])) | 31 | | `--web3-provider` | Web3 provider | 32 | | `--view-address` | Address of open oracle view to post through | 33 | | `--timeout`, `-t` | how many seconds to wait before retrying with more gas, defaults to 180 | 34 | | `--asset`, `-a` | List of assets to post prices for. Pass multiple times to specify multiple assets. | 35 | 36 | ### Sources 37 | 38 | A source can simply be a URL, e.g. `http://localhost:3000/prices.json` or you can pass a JSON-encoded structure for complex sources. Specifically, for the Coinbase API, you can use the following structure: 39 | 40 | ```json 41 | "{\"source\": \"coinbase\", \"endpoint\": \"https://api.pro.coinbase.com/oracle\", \"api_key_id\": \"\", \"api_secret\": \"\", \"api_passphrase\": \"\"}" 42 | ``` 43 | 44 | ### Examples 45 | 46 | To run as standalone from this project's root, simply invoke the start script. 47 | 48 | ```sh 49 | yarn run start --view-address=0xViewAddress --poster-key=0xWalletWithGas --sources=http://localhost:3000/prices.json 50 | ``` 51 | 52 | Here's an example for the Kovan testnet: 53 | 54 | ```sh 55 | yarn prepare && yarn run start --web3-provider=https://kovan-eth.compound.finance/ --view-address=0x60F1FFB2FE2bFE6CFFA0A66e258B623f06E1949F --poster-key="$(cat ~/.ethereum/kovan)" --sources="{\"source\": \"coinbase\", \"endpoint\": \"https://api.pro.coinbase.com/oracle\", \"api_key_id\": \"$COINBASE_API_KEY\", \"api_secret\": \"$COINBASE_API_SECRET\", \"api_passphrase\": \"$COINBASE_API_PASSPHRASE\"}" 56 | ``` 57 | 58 | ## Running in JavaScript 59 | 60 | You can include the Open Price Feed poster in an app for configuration: 61 | 62 | ```js 63 | import poster from 'open-oracle-poster'; 64 | import Web3 from 'web3'; 65 | 66 | // sample arguments, fill these in with real data :) 67 | let sources: string[] = /* [list of sources, possibly JSON-encoded] */; 68 | let posterKey: string = /* ...a key to a wallet holding eth for gas */; 69 | let viewAddress: string = /* "0xDelfiPriceView" */; 70 | let viewFunction: string = 'postPrices(bytes[],bytes[],string[])' /* ...view function signature */; 71 | let provider = new Web3(); 72 | 73 | await poster.main(sources, posterKey, viewAddress, viewFunction, provider); 74 | ``` 75 | 76 | ## Testing 77 | 78 | To run tests, simply run: 79 | 80 | ```bash 81 | yarn test 82 | ``` 83 | 84 | To run a single test run: 85 | 86 | ``` 87 | yarn test tests/poster_test.ts 88 | ``` 89 | 90 | ## Contributing 91 | 92 | For all contributions, please open an issue or pull request to discuss. Ensure that all test cases are passing and that top-level (integration) tests also pass (see the `open-oracle` root project). See top-level README for license notice and contribution agreement. 93 | -------------------------------------------------------------------------------- /poster/jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after `n` failures 9 | // bail: 0, 10 | 11 | // Respect "browser" field in package.json when resolving modules 12 | // browser: false, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "/private/var/folders/jz/z56b1n2902584b4zplqztm3m0000gn/T/jest_dx", 16 | 17 | // Automatically clear mock calls and instances between every test 18 | // clearMocks: false, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | // collectCoverage: false, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | // collectCoverageFrom: null, 25 | 26 | // The directory where Jest should output its coverage files 27 | // coverageDirectory: "coverage", 28 | 29 | // An array of regexp pattern strings used to skip coverage collection 30 | // coveragePathIgnorePatterns: [ 31 | // "/node_modules/" 32 | // ], 33 | 34 | // A list of reporter names that Jest uses when writing coverage reports 35 | // coverageReporters: [ 36 | // "json", 37 | // "text", 38 | // "lcov", 39 | // "clover" 40 | // ], 41 | 42 | // An object that configures minimum threshold enforcement for coverage results 43 | // coverageThreshold: null, 44 | 45 | // A path to a custom dependency extractor 46 | // dependencyExtractor: null, 47 | 48 | // Make calling deprecated APIs throw helpful error messages 49 | // errorOnDeprecated: false, 50 | 51 | // Force coverage collection from ignored files using an array of glob patterns 52 | // forceCoverageMatch: [], 53 | 54 | // A path to a module which exports an async function that is triggered once before all test suites 55 | // globalSetup: './.tsbuilt/test.js', 56 | 57 | // A path to a module which exports an async function that is triggered once after all test suites 58 | // globalTeardown: null, 59 | 60 | // A set of global variables that need to be available in all test environments 61 | // globals: { 62 | // 'ts-jest': { 63 | // diagnostics: { 64 | // warnOnly: true 65 | // } 66 | // } 67 | // }, 68 | 69 | // An array of directory names to be searched recursively up from the requiring module's location 70 | // moduleDirectories: [ 71 | // "node_modules" 72 | // ], 73 | 74 | // An array of file extensions your modules use 75 | // moduleFileExtensions: [ 76 | // "js", 77 | // "json", 78 | // "jsx", 79 | // "ts", 80 | // "tsx", 81 | // "node" 82 | // ], 83 | 84 | // A map from regular expressions to module names that allow to stub out resources with a single module 85 | // moduleNameMapper: {}, 86 | 87 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 88 | // modulePathIgnorePatterns: [], 89 | 90 | // Activates notifications for test results 91 | // notify: false, 92 | 93 | // An enum that specifies notification mode. Requires { notify: true } 94 | // notifyMode: "failure-change", 95 | 96 | // A preset that is used as a base for Jest's configuration 97 | preset: 'ts-jest', 98 | 99 | // Run tests from one or more projects 100 | // projects: null, 101 | 102 | // Use this configuration option to add custom reporters to Jest 103 | // reporters: undefined, 104 | 105 | // Automatically reset mock state between every test 106 | // resetMocks: false, 107 | 108 | // Reset the module registry before running each individual test 109 | // resetModules: false, 110 | 111 | // A path to a custom resolver 112 | // resolver: null, 113 | 114 | // Automatically restore mock state between every test 115 | // restoreMocks: false, 116 | 117 | // The root directory that Jest should scan for tests and modules within 118 | // rootDir: null, 119 | 120 | // A list of paths to directories that Jest should use to search for files in 121 | // roots: [ 122 | // "" 123 | // ], 124 | 125 | // Allows you to use a custom runner instead of Jest's default test runner 126 | // runner: "jest-runner", 127 | 128 | // The paths to modules that run some code to configure or set up the testing environment before each test 129 | // setupFiles: [], 130 | 131 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 132 | // setupFilesAfterEnv: [] 133 | 134 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 135 | // snapshotSerializers: [], 136 | 137 | // The test environment that will be used for testing 138 | testEnvironment: "node", 139 | 140 | // Options that will be passed to the testEnvironment 141 | // testEnvironmentOptions: {}, 142 | 143 | // Adds a location field to test results 144 | // testLocationInResults: false, 145 | 146 | // The glob patterns Jest uses to detect test files 147 | testMatch: [ 148 | "**/tests/**/*.[jt]s?(x)" 149 | ], 150 | 151 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 152 | // testPathIgnorePatterns: [ 153 | // "/node_modules/" 154 | // ], 155 | 156 | // The regexp pattern or array of patterns that Jest uses to detect test files 157 | // testRegex: [], 158 | 159 | // This option allows the use of a custom results processor 160 | // testResultsProcessor: null, 161 | 162 | // This option allows use of a custom test runner 163 | // testRunner: "jasmine2", 164 | 165 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 166 | // testURL: "http://localhost", 167 | 168 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 169 | // timers: "real", 170 | 171 | // A map from regular expressions to paths to transformers 172 | "transform": { 173 | "^.+\\.tsx?$": "ts-jest" 174 | }, 175 | 176 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 177 | // transformIgnorePatterns: [ 178 | // "/node_modules/" 179 | // ], 180 | 181 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 182 | // unmockedModulePathPatterns: undefined, 183 | 184 | // Indicates whether each individual test should be reported during the run 185 | verbose: true, 186 | 187 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 188 | // watchPathIgnorePatterns: [], 189 | 190 | // Whether to use watchman for file crawling 191 | // watchman: true, 192 | }; 193 | -------------------------------------------------------------------------------- /poster/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "open-oracle-poster", 3 | "version": "1.1.2", 4 | "description": "A Customizable Poster for the Compound Open Price Feed", 5 | "main": ".tsbuilt/poster.js", 6 | "bin": ".tsbuilt/index.js", 7 | "repository": "https://compound.finance/open-oracle", 8 | "author": "Compound Labs, Inc.", 9 | "license": "MIT", 10 | "scripts": { 11 | "prepare": "npx tsc", 12 | "start": "node .tsbuilt/index.js", 13 | "test": "npx jest" 14 | }, 15 | "devDependencies": { 16 | "@types/jest": "^24.0.23", 17 | "@types/node": "^12.12.14", 18 | "jest": "^24.9.0", 19 | "jest-junit": "^10.0.0", 20 | "ts-jest": "^24.2.0", 21 | "typescript": "^3.7.3" 22 | }, 23 | "dependencies": { 24 | "bignumber.js": "^9.0.0", 25 | "ganache-core": "github:compound-finance/ganache-core.git#compound", 26 | "node-fetch": "^2.6.0", 27 | "web3": "1.2.9", 28 | "yargs": "^15.0.2" 29 | }, 30 | "resolutions": { 31 | "scrypt.js": "https://registry.npmjs.org/@compound-finance/ethereumjs-wallet/-/ethereumjs-wallet-0.6.3.tgz", 32 | "**/sha3": "^2.1.2" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /poster/src/index.ts: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | import { main } from './poster'; 3 | import Web3 from 'web3'; 4 | import yargs from 'yargs'; 5 | 6 | async function run() { 7 | const parsed = yargs 8 | .env('POSTER') 9 | .option('sources', {alias: 's', description: 'Sources to pull price messages from, a list of https endpoints created by open oracle reporters serving open oracle payloads as json', type: 'string'}) 10 | .option('poster-key', {alias: 'k', description: 'Private key holding enough gas to post (try: `file: or env:)`', type: 'string'}) 11 | .option('view-address', {alias: 'v', description: 'Address of open oracle view to post through', type: 'string'}) 12 | .option('view-function', {alias: 'f', description: 'Function signature for the view', type: 'string', default: 'postPrices(bytes[],bytes[],string[])'}) 13 | .option('web3-provider', {description: 'Web 3 provider', type: 'string', default: 'http://127.0.0.1:8545'}) 14 | .option('timeout', {alias: 't', description: 'how many seconds to wait before retrying with more gas', type: 'number', default: 180}) 15 | .option('gas-limit', {alias: 'g', description: 'how much gas to send', type: 'number', default: 4000000}) 16 | .option('gas-price', {alias: 'gp', description: 'gas price', type: 'number'}) 17 | .option('asset', {alias: 'a', description: 'A list of supported token names for posting prices', type: 'array', default: ['BTC', 'ETH', 'DAI', 'REP', 'ZRX', 'BAT', 'KNC', 'LINK', 'COMP']}) 18 | .option('price-deltas', {alias: 'd', description: 'the min required difference between new and previous asset price for the update on blockchain', type: 'string'}) 19 | .option('testnet-world', {alias: 'tw', description: 'An option to use mocked uniswap token pairs with data from mainnet', type: 'boolean', default: false}) 20 | .option('testnet-uniswap-pairs', {alias: 'tup', description: 'A list of uniswap testnet pairs for all assets', type: 'string'}) 21 | .option('mainnet-uniswap-pairs', {alias: 'mup', description: 'A list of uniswap mainnet pairs for all assets', type: 'string'}) 22 | 23 | .help() 24 | .alias('help', 'h') 25 | .demandOption(['poster-key', 'sources', 'view-function', 'web3-provider', 'view-address', 'price-deltas'], 'Provide all the arguments') 26 | .argv; 27 | 28 | const sources = Array.isArray(parsed['sources']) ? parsed['sources'] : [ parsed['sources'] ]; 29 | const poster_key = parsed['poster-key']; 30 | const view_address = parsed['view-address']; 31 | const view_function = parsed['view-function']; 32 | const web3_provider = parsed['web3-provider']; 33 | const timeout = parsed['timeout']; 34 | const gas_limit = parsed['gas-limit']; 35 | const gas_price = parsed['gas-price']; 36 | const price_deltas = JSON.parse(parsed['price-deltas']); 37 | const assets = parsed['asset']; 38 | 39 | // check that price deltas are set up for all assets 40 | assets.forEach(asset => { 41 | if (price_deltas[asset] == undefined) { 42 | throw new TypeError(`For each asset price delta should be provided, ${asset} asset is not properly configured`) 43 | } 44 | }); 45 | 46 | console.log(`Posting with price deltas = `, price_deltas); 47 | 48 | // parameters only for testnets that mock uniswap mainnet 49 | const mocked_world = parsed['testnet-world']; 50 | const testnet_pairs = JSON.parse(parsed['testnet-uniswap-pairs'] || '{}'); 51 | const mainnet_pairs = JSON.parse(parsed['mainnet-uniswap-pairs'] || '{}'); 52 | console.log(`Configuring using testnet and mainnet uniswap pairs:`, testnet_pairs, mainnet_pairs); 53 | const pairs = {testnet: {}, mainnet: {}}; 54 | if (mocked_world) { 55 | assets.forEach(asset => { 56 | if (!testnet_pairs[asset] || !mainnet_pairs[asset]) { 57 | throw new TypeError(`For each asset mainnet and testnet pairs should be provided, ${asset} asset is not properly configured`) 58 | } 59 | pairs['testnet'][asset] = testnet_pairs[asset]; 60 | pairs['mainnet'][asset] = mainnet_pairs[asset]; 61 | }); 62 | } 63 | 64 | // posting promise will reject and retry once with higher gas after this timeout 65 | const web3 = await new Web3(web3_provider); 66 | web3.eth.transactionPollingTimeout = timeout; 67 | 68 | // TODO: We should probably just have a `network` var here, or 69 | // pass this in as an option. 70 | if (web3_provider.match(/.*:8545$/)) { 71 | // confirm immediately in dev 72 | web3.eth.transactionConfirmationBlocks = 1 73 | } else { 74 | web3.eth.transactionConfirmationBlocks = 10; 75 | } 76 | 77 | await main(sources, poster_key, view_address, view_function, gas_limit, gas_price, price_deltas, assets, mocked_world, pairs, web3); 78 | 79 | let success_log = { 80 | message: "Price Feed Poster run completed successfully", 81 | metric_name: 'PriceFeed-PosterHealth', 82 | labels: { 83 | price_feed_poster_healthy: 1 84 | } 85 | }; 86 | 87 | process.stderr.write(JSON.stringify(success_log) + "\n", () => { 88 | process.exit(0); 89 | }); 90 | } 91 | 92 | run().catch((e) => { 93 | console.error(`Error encountered: ${e}`); 94 | console.error(e); 95 | console.log(e.stack) 96 | console.log 97 | 98 | let error_log = { 99 | message: "Price run failed", 100 | metric_name: 'PriceFeed-PosterHealth', 101 | labels: { 102 | price_feed_poster_healthy: 0, 103 | error: e.toString() 104 | } 105 | }; 106 | 107 | process.stderr.write(JSON.stringify(error_log) + "\n", () => { 108 | process.exit(1); 109 | }) 110 | }); 111 | -------------------------------------------------------------------------------- /poster/src/interfaces.ts: -------------------------------------------------------------------------------- 1 | /* --- Open Price Feed Interfaces --- 2 | * 3 | * In the view, there is a function that will write message values to the 4 | * the Open Price Feed data contract if the posted values are more valid 5 | * ( e.g. later timestamp ) than what already is in the data contract. 6 | * 7 | * The view will also write to its own storage cache an aggregated value 8 | * based on the state of data contract. 9 | * 10 | * A payload for an open price feed view comprises two fields: 11 | * 1. ABI-encoded values to be written to the open price feed data contract 12 | * 2. The attestor's signature on a hash of that message 13 | */ 14 | 15 | interface OpenPriceFeedPayload { 16 | // ABI-encoded values to be written to the open oracle data contract. 17 | messages: string[] 18 | // The signature of the attestor to these values. The values in `message` 19 | // will be stored in a mapping under this signer's public address. 20 | signatures: string[] 21 | prices: {[symbol: string]: string } 22 | }; 23 | 24 | interface DecodedMessage { 25 | dataType: string 26 | timestamp: number 27 | symbol: string 28 | price: number 29 | } 30 | 31 | interface OpenPriceFeedItem { 32 | message: string 33 | signature: string 34 | dataType: string 35 | timestamp: number 36 | symbol: string 37 | price: number 38 | source: string 39 | prev: number 40 | }; 41 | -------------------------------------------------------------------------------- /poster/src/mainnet_uniswap_mocker.ts: -------------------------------------------------------------------------------- 1 | 2 | import Web3 from 'web3'; 3 | import { read, readMany, encode } from './util'; 4 | import { postWithRetries } from './post_with_retries'; 5 | 6 | const mainnetWeb3 = new Web3(new Web3.providers.HttpProvider('https://mainnet-eth.compound.finance/')); 7 | 8 | async function getReserves(pair: string) { 9 | return await readMany( 10 | pair, 11 | "getReserves()", 12 | [], 13 | ["uint112","uint112","uint32"], 14 | mainnetWeb3 15 | ); 16 | } 17 | 18 | async function getCumulativePrices(pair: string) { 19 | const price0 = await read( 20 | pair, 21 | "price0CumulativeLast()", 22 | [], 23 | "uint256", 24 | mainnetWeb3 25 | ); 26 | const price1 = await read( 27 | pair, 28 | "price1CumulativeLast()", 29 | [], 30 | "uint256", 31 | mainnetWeb3 32 | ); 33 | 34 | return [price0, price1]; 35 | } 36 | 37 | function buildTrxData(reserve0, reserve1, blockTimestampLast, price0, price1, functionSig){ 38 | return encode( 39 | functionSig, 40 | [reserve0, reserve1, blockTimestampLast, price0, price1] 41 | ); 42 | } 43 | 44 | async function mockUniswapTokenPair(symbol: string, senderKey: string, pairs, gas: number, gasPrice: number, web3: Web3) { 45 | const testnetPair = pairs.testnet[symbol]; 46 | const mainnetPair = pairs.mainnet[symbol]; 47 | const reserves = await getReserves(mainnetPair); 48 | const cumulatives = await getCumulativePrices(mainnetPair); 49 | 50 | const reserve0 = reserves[0]; 51 | const reserve1 = reserves[1]; 52 | const blockTimestampLast = reserves[2]; 53 | const cumulativePrice0 = cumulatives[0]; 54 | const cumulativePrice1 = cumulatives[1]; 55 | 56 | console.log(`Mocking uniswap token pair for ${symbol} with results--> ${reserve0} ${reserve1} ${blockTimestampLast} ${cumulativePrice0} ${cumulativePrice1}`); 57 | 58 | const functionSig = "update(uint112,uint112,uint32,uint256,uint256)"; 59 | const trxData = buildTrxData(reserve0, reserve1, blockTimestampLast, cumulativePrice0, cumulativePrice1, functionSig); 60 | const trx = { 61 | data: trxData, 62 | to: testnetPair, 63 | gasPrice: gasPrice, 64 | gas: gas 65 | }; 66 | 67 | return await postWithRetries(trx, senderKey, web3); 68 | } 69 | 70 | export async function mockUniswapTokenPairs(assets: string[], senderKey: string, pairs, gas: number, gasPrice: number, web3: Web3) { 71 | for (const asset of assets) { 72 | await mockUniswapTokenPair(asset.toUpperCase(), senderKey, pairs, gas, gasPrice, web3); 73 | } 74 | } -------------------------------------------------------------------------------- /poster/src/post_with_retries.ts: -------------------------------------------------------------------------------- 1 | import Web3 from 'web3'; 2 | import Utils from 'web3-utils'; 3 | import { TransactionConfig, TransactionReceipt } from 'web3-core'; 4 | 5 | function ensureHex(val: string, type: string): string { 6 | if (Utils.isHexStrict(val)) { 7 | return val; 8 | } 9 | 10 | const val0x = `0x${val}`; 11 | if (Utils.isHexStrict(val0x)) { 12 | return val0x; 13 | } 14 | 15 | throw new Error(`Invalid hex for ${type}: got \`${val}\``); 16 | } 17 | 18 | function isUnderpriced(e) { 19 | return e.message === 'Returned error: replacement transaction underpriced'; 20 | } 21 | 22 | function isTimeout(e) { 23 | return /Error: Timeout exceeded during the transaction confirmation process. Be aware the transaction could still get confirmed!/.test(e.error); 24 | } 25 | 26 | function maybeIsOutOfGas(e) { 27 | return e.message && e.message.includes('Transaction has been reverted by the EVM'); 28 | } 29 | 30 | const SLEEP_DURATION = 3000; // 3s 31 | const RETRIES = 3; 32 | const GAS_PRICE_ADJUSTMENT = 1.2; // Increase gas price by this percentage each retry 33 | const GAS_ADJUSTMENT = 1.5; // Increase gas limit by this percentage each retry 34 | 35 | async function postWithRetries(transaction: TransactionConfig, signerKey: string, web3: Web3, retries: number = RETRIES, attempt: number = 0) { 36 | console.log(`Running Open Price Feed Poster${attempt > 0 ? ` [attempt ${attempt}]` : ''}...`); 37 | 38 | signerKey = ensureHex(signerKey, 'private key'); 39 | 40 | let pubKey = web3.eth.accounts.privateKeyToAccount(signerKey) 41 | 42 | console.log(`Posting from account: ${pubKey.address}`); 43 | 44 | let nonce = await web3.eth.getTransactionCount(pubKey.address) 45 | transaction.nonce = nonce 46 | 47 | try { 48 | return await signAndSend(transaction, signerKey, web3); 49 | } catch (e) { 50 | console.debug({transaction}); 51 | console.warn('Failed to post Open Price Feed:'); 52 | console.warn(e); 53 | 54 | // Try more gas and higher gas price, reverse engineering geth/parity errors is error-prone 55 | transaction = { 56 | ...transaction, 57 | gas: Math.floor(Number(transaction.gas) * GAS_ADJUSTMENT), 58 | gasPrice: Math.floor(Number(transaction.gasPrice) * GAS_PRICE_ADJUSTMENT) 59 | }; 60 | 61 | if (retries > 0) { 62 | // Sleep for some time before retrying 63 | await (new Promise(okay => setTimeout(okay, SLEEP_DURATION))); 64 | 65 | return postWithRetries(transaction, signerKey, web3, retries - 1, attempt + 1); 66 | } else { 67 | throw new Error(`Failed to run Open Price Feed poster after ${attempt} attempt(s): error=\`${e.toString()}\``); 68 | } 69 | } 70 | } 71 | 72 | async function signAndSend(transaction: TransactionConfig, signerKey: string, web3: Web3): Promise { 73 | let signedTransaction = 74 | await web3.eth.accounts.signTransaction(transaction, signerKey); 75 | 76 | return web3.eth.sendSignedTransaction(signedTransaction.rawTransaction || ''); 77 | } 78 | 79 | export { 80 | postWithRetries, 81 | signAndSend 82 | } 83 | -------------------------------------------------------------------------------- /poster/src/poster.ts: -------------------------------------------------------------------------------- 1 | import { postWithRetries } from './post_with_retries'; 2 | import fetch from 'node-fetch'; 3 | import Web3 from 'web3'; 4 | import { TransactionConfig } from 'web3-core'; 5 | import { 6 | getDataAddress, 7 | getPreviousPrice, 8 | getSourceAddress 9 | } from './prev_price'; 10 | import { BigNumber as BN } from 'bignumber.js'; 11 | import { CoinbaseConfig, readCoinbasePayload } from './sources/coinbase'; 12 | import { decodeMessage, encode, zip } from './util'; 13 | import { mockUniswapTokenPairs } from './mainnet_uniswap_mocker'; 14 | 15 | const GAS_PRICE_API = 'https://api.compound.finance/api/gas_prices/get_gas_price'; 16 | const DEFAULT_GAS_PRICE = 3_000_000_000; // use 3 gwei if api is unreachable for some reason 17 | 18 | export async function main( 19 | sources: string[], 20 | senderKey: string, 21 | viewAddress: string, 22 | functionSig: string, 23 | gas: number, 24 | gasPrice: number | undefined, 25 | deltas, 26 | assets: string[], 27 | mocked_world: boolean, 28 | pairs, 29 | web3: Web3) { 30 | 31 | const payloads = await fetchPayloads(sources); 32 | const feedItems = await filterPayloads(payloads, viewAddress, assets, deltas, web3); 33 | 34 | if (feedItems.length > 0) { 35 | // If gas price was not defined, fetch average one from Compound API 36 | if (!gasPrice) { 37 | gasPrice = await fetchGasPrice(); 38 | } 39 | 40 | // mock uniswap mainnet pairs price 41 | if (mocked_world) { 42 | // Mock only pairs that will be updated 43 | const updateAssets = feedItems.map(item => item.symbol) 44 | await mockUniswapTokenPairs(updateAssets, senderKey, pairs, gas, gasPrice, web3); 45 | } 46 | 47 | const trxData = buildTrxData(feedItems, functionSig); 48 | const gasEstimate = await web3.eth.estimateGas({data: trxData, to: viewAddress}); 49 | // Make gas estimate safer by 50% adjustment 50 | const gastEstimateAdjusted = Math.floor(gasEstimate * 1.5); 51 | const trx = { 52 | data: trxData, 53 | to: viewAddress, 54 | gasPrice: gasPrice, 55 | gas: gastEstimateAdjusted 56 | }; 57 | 58 | console.log(`Posting...`); 59 | console.log(feedItems); 60 | 61 | return await postWithRetries(trx, senderKey, web3); 62 | } 63 | } 64 | 65 | export async function filterPayloads( 66 | payloads: OpenPriceFeedPayload[], 67 | viewAddress: string, 68 | supportedAssets: string[], 69 | deltas, 70 | web3: Web3): Promise { 71 | 72 | const dataAddress = await getDataAddress(viewAddress, web3); 73 | 74 | let filteredFeedItems = await Promise.all(payloads.map(async payload => { 75 | return await Promise.all(zip(payload.messages, payload.signatures).map(([message, signature]) => { 76 | const { 77 | dataType, 78 | timestamp, 79 | symbol, 80 | price 81 | } = decodeMessage(message, web3) 82 | 83 | return { 84 | message, 85 | signature, 86 | dataType, 87 | timestamp, 88 | symbol: symbol.toUpperCase(), 89 | price: Number(price) 90 | }; 91 | }).filter(({message, signature, symbol}) => { 92 | return supportedAssets.includes(symbol.toUpperCase()); 93 | }).map(async (feedItem) => { 94 | const source = await getSourceAddress(dataAddress, feedItem.message, feedItem.signature, web3); 95 | const prev = await getPreviousPrice(source, feedItem.symbol, dataAddress, web3); 96 | 97 | return { 98 | ...feedItem, 99 | source, 100 | prev: Number(prev) / 1e6 101 | }; 102 | })).then((feedItems) => { 103 | return feedItems.filter(({message, signature, symbol, price, prev}) => { 104 | return !inDeltaRange(deltas[symbol], price, prev); 105 | }); 106 | }); 107 | })); 108 | 109 | let feedItems = filteredFeedItems.flat(); 110 | 111 | feedItems 112 | .forEach(({source, symbol, price, prev}) => { 113 | console.log(`Setting Price: source=${source}, symbol=${symbol}, price=${price}, prev_price=${prev}`); 114 | }); 115 | 116 | return feedItems; 117 | } 118 | 119 | // Checks if new price is less than delta percent different form the old price 120 | // Note TODO: price here is uh... a number that needs to be scaled by 1e6? 121 | export function inDeltaRange(delta: number, price: number, prevPrice: number) { 122 | // Always update prices if delta is set to 0 or delta is not within expected range [0..100]% 123 | if (delta <= 0 || delta > 100) { 124 | return false 125 | }; 126 | 127 | const minDifference = new BN(prevPrice).multipliedBy(delta).dividedBy(100); 128 | const difference = new BN(prevPrice).minus(new BN(price)).abs(); 129 | 130 | return difference.isLessThanOrEqualTo(minDifference); 131 | } 132 | 133 | export async function fetchPayloads(sources: string[], fetchFn=fetch): Promise { 134 | function parse(json): object { 135 | let result; 136 | try { 137 | result = JSON.parse(json); 138 | } catch (e) { 139 | console.error(`Error parsing source input: ${json}`); 140 | throw e; 141 | } 142 | if (!result['source']) { 143 | throw new Error(`Source must include \`source\` field for ${json}`); 144 | } 145 | return result; 146 | } 147 | 148 | return await Promise.all(sources.map(async (sourceRaw) => { 149 | let source = sourceRaw.includes('{') ? parse(sourceRaw) : sourceRaw; 150 | let response; 151 | 152 | try { 153 | if (typeof(source) === 'string') { 154 | response = await fetchFn(source); 155 | } else if (source['source'] === 'coinbase') { 156 | response = await readCoinbasePayload(source, fetchFn); 157 | } 158 | 159 | return await response.json(); 160 | } catch (e) { 161 | // This is now just for some extra debugging messages 162 | console.error(`Error Fetching Payload for ${JSON.stringify(source)}`); 163 | if (response) { 164 | console.debug({response}); 165 | } 166 | console.error(e); 167 | throw e; 168 | } 169 | })); 170 | } 171 | 172 | export async function fetchGasPrice(fetchFn = fetch): Promise { 173 | try { 174 | let response = await fetchFn(GAS_PRICE_API); 175 | let prices = await response.json(); 176 | return Number(prices["average"]["value"]); 177 | } catch (e) { 178 | console.warn(`Failed to fetch gas price`, e); 179 | return DEFAULT_GAS_PRICE; 180 | } 181 | } 182 | 183 | export function buildTrxData(feedItems: OpenPriceFeedItem[], functionSig: string): string { 184 | const messages = feedItems.map(({message}) => message); 185 | const signatures = feedItems.map(({signature}) => signature); 186 | const symbols = [...new Set(feedItems.map(({symbol}) => symbol.toUpperCase()))]; 187 | 188 | return encode( 189 | functionSig, 190 | [messages, signatures, symbols] 191 | ); 192 | } 193 | -------------------------------------------------------------------------------- /poster/src/prev_price.ts: -------------------------------------------------------------------------------- 1 | import Web3 from 'web3'; 2 | import { read } from './util'; 3 | 4 | export async function getPreviousPrice(sourceAddress: string, asset: string, dataAddress: string, web3: Web3) { 5 | return await read( 6 | dataAddress, 7 | 'getPrice(address,string)', 8 | [sourceAddress, asset.toUpperCase()], 9 | 'uint256', 10 | web3 11 | ); 12 | } 13 | 14 | export async function getDataAddress(viewAddress: string, web3: Web3) { 15 | return await read( 16 | viewAddress, 17 | 'priceData()', 18 | [], 19 | 'address', 20 | web3 21 | ); 22 | } 23 | 24 | export async function getSourceAddress(dataAddress: string, message: string, signature: string, web3 : Web3) { 25 | return await read( 26 | dataAddress, 27 | 'source(bytes,bytes)', 28 | [message, signature], 29 | 'address', 30 | web3 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /poster/src/sources/coinbase.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto'; 2 | 3 | export interface CoinbaseConfig { 4 | source: string 5 | endpoint: string 6 | api_key_id: string 7 | api_secret: string 8 | api_passphrase: string 9 | } 10 | 11 | export async function readCoinbasePayload(config: CoinbaseConfig, fetchFn) { 12 | let timestamp = Date.now() / 1000; 13 | let method = 'GET'; 14 | 15 | // create the prehash string by concatenating required parts 16 | let what = timestamp + method + "/oracle"; 17 | 18 | // decode the base64 secret 19 | let key = Buffer.from(config.api_secret, 'base64'); 20 | 21 | // create a sha256 hmac with the secret 22 | let hmac = crypto.createHmac('sha256', key); 23 | 24 | // sign the require message with the hmac 25 | // and finally base64 encode the result 26 | let signature = hmac.update(what).digest('base64'); 27 | let headers = { 28 | 'CB-ACCESS-KEY': config.api_key_id, 29 | 'CB-ACCESS-SIGN': signature, 30 | 'CB-ACCESS-TIMESTAMP': timestamp, 31 | 'CB-ACCESS-PASSPHRASE': config.api_passphrase, 32 | 'Content-Type': 'application/json' 33 | }; 34 | 35 | return await fetchFn(config.endpoint, { 36 | headers: headers 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /poster/src/util.ts: -------------------------------------------------------------------------------- 1 | import AbiCoder from 'web3-eth-abi'; 2 | import Web3 from 'web3'; 3 | import { TransactionConfig } from 'web3-core'; 4 | 5 | export function decodeMessage(message: string, web3: Web3): DecodedMessage { 6 | // TODO: Consider using `decode` from `reporter.ts` 7 | let { 8 | '0': dataType, 9 | '1': timestamp, 10 | '2': symbol, 11 | '3': price 12 | } = web3.eth.abi.decodeParameters(['string', 'uint64', 'string', 'uint64'], message); 13 | 14 | return { 15 | dataType, 16 | timestamp, 17 | symbol, 18 | price: price / 1e6 19 | }; 20 | } 21 | 22 | function encodeFull(sig: string, args: any[]): [string[], string] { 23 | const types = findTypes(sig); 24 | 25 | const callData = 26 | (AbiCoder).encodeFunctionSignature(sig) + 27 | (AbiCoder).encodeParameters(types, args).slice(2); 28 | 29 | return [types, callData]; 30 | } 31 | 32 | export function encode(sig: string, args: any[]): string { 33 | let [types, callData] = encodeFull(sig, args); 34 | 35 | return callData; 36 | } 37 | 38 | export async function read(address: string, sig: string, args: any[], returns: string, web3: Web3): Promise { 39 | let [types, callData] = encodeFull(sig, args); 40 | 41 | const call = { 42 | data: callData, 43 | // Price open oracle data 44 | to: address 45 | }; 46 | 47 | try { 48 | const result = await web3.eth.call(call); 49 | 50 | return (AbiCoder).decodeParameter(returns, result); 51 | } catch (e) { 52 | console.error(`Error reading ${sig}:${args} at ${address}: ${e.toString()}`); 53 | throw e; 54 | } 55 | } 56 | 57 | export async function readMany(address: string, sig: string, args: any[], returns: string[], web3: Web3): Promise { 58 | let [_, callData] = encodeFull(sig, args); 59 | 60 | const call = { 61 | data: callData, 62 | // Price open oracle data 63 | to: address 64 | }; 65 | 66 | try { 67 | const result = await web3.eth.call(call); 68 | 69 | return (AbiCoder).decodeParameters(returns, result); 70 | } catch (e) { 71 | console.error(`Error reading ${sig}:${args} at ${address}: ${e.toString()}`); 72 | throw e; 73 | } 74 | } 75 | 76 | // TODO: Swap with ether's own implementation of this 77 | // e.g. findTypes("postPrices(bytes[],bytes[],string[])")-> ["bytes[]","bytes[]","string[]"] 78 | export function findTypes(functionSig: string): string[] { 79 | // this unexported function from ethereumjs-abi is copy pasted from source 80 | // see https://github.com/ethereumjs/ethereumjs-abi/blob/master/lib/index.js#L81 81 | let parseSignature = function (sig) { 82 | var tmp = /^(\w+)\((.*)\)$/.exec(sig) || []; 83 | 84 | if (tmp.length !== 3) { 85 | throw new Error('Invalid method signature') 86 | } 87 | 88 | var args = /^(.+)\):\((.+)$/.exec(tmp[2]) 89 | 90 | if (args !== null && args.length === 3) { 91 | return { 92 | method: tmp[1], 93 | args: args[1].split(','), 94 | retargs: args[2].split(',') 95 | } 96 | } else { 97 | var params = tmp[2].split(',') 98 | if (params.length === 1 && params[0] === '') { 99 | // Special-case (possibly naive) fixup for functions that take no arguments. 100 | // TODO: special cases are always bad, but this makes the function return 101 | // match what the calling functions expect 102 | params = [] 103 | } 104 | return { 105 | method: tmp[1], 106 | args: params 107 | } 108 | } 109 | } 110 | 111 | return parseSignature(functionSig).args; 112 | } 113 | 114 | export function zip(arr1: T[], arr2: U[]): [T, U][] { 115 | return arr1.map((k, i) => [k, arr2[i]]) 116 | } 117 | 118 | export async function asyncFilter(arr: T[], f: ((T) => Promise)): Promise { 119 | let tests: boolean[] = await Promise.all(arr.map(f)); 120 | 121 | return tests.reduce((acc, el, i) => { 122 | if (el) { 123 | return [...acc, arr[i]]; 124 | } else { 125 | return acc; 126 | } 127 | }, []); 128 | } 129 | 130 | export async function allSuccesses(promises: Promise[]): Promise { 131 | let settled = await Promise.allSettled(promises); 132 | 133 | return settled 134 | .filter((promise) => promise.status === 'fulfilled') 135 | .map((promise => (>promise).value)); 136 | } 137 | -------------------------------------------------------------------------------- /poster/tests/post_with_retries_test.ts: -------------------------------------------------------------------------------- 1 | import Ganache from 'ganache-core'; 2 | import Web3 from 'web3'; 3 | import { PromiEvent } from 'web3-core'; 4 | import { TransactionReceipt } from 'web3-eth'; 5 | import { postWithRetries, signAndSend } from '../src/post_with_retries'; 6 | 7 | describe('posting', () => { 8 | test('signs and sends', async () => { 9 | const web3 = new Web3(Ganache.provider()); 10 | web3.eth.transactionConfirmationBlocks = 1; 11 | 12 | (web3.eth).sendSignedTransaction = (signedTransactionData: string, callback?: ((error: Error, hash: string) => void) | undefined): PromiEvent => { 13 | // TODO: Parse signed transaction data to ensure matches 14 | expect(signedTransactionData).toEqual("0xf901eb8083989680830186a0944fe3dd76d873caf6cbf56e442b2c808d3984df1d80b90184a59d56ad000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020177ee777e72b8c042e05ef41d1db0f17f1fcb0e8150b37cfad6993e4373bdf1000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000006004a78a7b3013f6939da19eac6fd1ad5c5a20c41bcc5d828557442aad6f07598d029ae684620bec13e13d018cba0da5096626e83cfd4d5356d808d7437a0a5076000000000000000000000000000000000000000000000000000000000000001c820a95a0122a39912374aaf6d64f0ea8d4de552237af6deda0038cc3ebf4e2f8b36daa2fa0408d15da1b8fa27a5f73b3c06a561eecf6e4e667d410c6f973da6a54abb78f4b"); 15 | 16 | return Promise.resolve({ 17 | "blockHash": "0x4dd036433dde8a2e04bf49f50f7543de20e8a66566bad42359fae6ec2a816ea3", 18 | "blockNumber": 1, 19 | "contractAddress": null, 20 | "cumulativeGasUsed": 29528, 21 | "from": "0x864f667F63B8650e10A0E52910f01198dAb19d69", 22 | "gas": undefined, 23 | "gasUsed": 29528, 24 | "logs": [], 25 | "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 26 | "nonce": undefined, 27 | "r": "0x13315249d203595fae8d58dfbad79460c5a24f395e7317b72409f0cb770caee6", 28 | "s": "0x470d1bb57af3ecbd20ac13fc22dca5f023f7cc5533a17dffd03959c8f585b545", 29 | "status": true, 30 | "to": "0x4fe3dd76d873CAF6Cbf56E442B2C808D3984df1D", 31 | "transactionHash": "0xc693cb5a9f8bea2fac46d003c5ebe63102704c66658b532e76a3f9a2f1272f8b", 32 | "transactionIndex": 0, 33 | "v": "0x9e" 34 | }); 35 | } 36 | 37 | let senderKey = "0x620622d7fbe43a4dccd3aef6ef90d20728508c563719380f289cf3f9460d0510"; 38 | 39 | // sends the transaction 40 | const data = "0xa59d56ad000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020177ee777e72b8c042e05ef41d1db0f17f1fcb0e8150b37cfad6993e4373bdf1000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000006004a78a7b3013f6939da19eac6fd1ad5c5a20c41bcc5d828557442aad6f07598d029ae684620bec13e13d018cba0da5096626e83cfd4d5356d808d7437a0a5076000000000000000000000000000000000000000000000000000000000000001c"; 41 | 42 | let receipt = await signAndSend({ 43 | data: data, 44 | to: "0x4fe3dd76d873CAF6Cbf56E442B2C808D3984df1D", 45 | gasPrice: 10_000_000, 46 | gas: 100_000 47 | }, senderKey, web3); 48 | 49 | expect(receipt).toEqual( 50 | { 51 | "blockHash": "0x4dd036433dde8a2e04bf49f50f7543de20e8a66566bad42359fae6ec2a816ea3", 52 | "blockNumber": 1, 53 | "contractAddress": null, 54 | "cumulativeGasUsed": 29528, 55 | "from": "0x864f667F63B8650e10A0E52910f01198dAb19d69", 56 | "gas": undefined, 57 | "gasUsed": 29528, 58 | "logs": [], 59 | "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 60 | "nonce": undefined, 61 | "r": "0x13315249d203595fae8d58dfbad79460c5a24f395e7317b72409f0cb770caee6", 62 | "s": "0x470d1bb57af3ecbd20ac13fc22dca5f023f7cc5533a17dffd03959c8f585b545", 63 | "status": true, 64 | "to": "0x4fe3dd76d873CAF6Cbf56E442B2C808D3984df1D", 65 | "transactionHash": "0xc693cb5a9f8bea2fac46d003c5ebe63102704c66658b532e76a3f9a2f1272f8b", 66 | "transactionIndex": 0, 67 | "v": "0x9e" 68 | } 69 | ); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /poster/tests/poster_test.ts: -------------------------------------------------------------------------------- 1 | import Web3 from 'web3'; 2 | import { 3 | buildTrxData, 4 | fetchGasPrice, 5 | fetchPayloads, 6 | inDeltaRange, 7 | filterPayloads 8 | } from '../src/poster'; 9 | import * as prevPrice from '../src/prev_price'; 10 | import * as util from '../src/util'; 11 | 12 | const endpointResponses = { 13 | "http://localhost:3000": { 14 | "messages": ["0xmessage"], 15 | "prices": { 16 | "eth": 260, 17 | "zrx": 0.58, 18 | }, 19 | "signatures": ["0xsignature"], 20 | }, 21 | "http://localhost:3000/prices.json": { 22 | "messages": ["0xmessage"], 23 | "prices": { 24 | "eth": 250, 25 | "zrx": 1.58, 26 | }, 27 | "signatures": ["0xsignature"], 28 | } 29 | } 30 | 31 | const gasResponses = { 32 | "https://api.compound.finance/api/gas_prices/get_gas_price": { 33 | "average": { 34 | "value": "2600000000" 35 | }, 36 | "fast": { 37 | "value": "9000000000" 38 | }, 39 | "fastest": { 40 | "value": "21000000000" 41 | }, 42 | "safe_low": { 43 | "value": "1000000000" 44 | } 45 | } 46 | }; 47 | 48 | const mockFetch = (responses) => { 49 | return async (url) => { 50 | const response = responses[url]; 51 | if (response === undefined) { 52 | throw new Error(`Mock Fetch: Unknown URL \`${url}\``); 53 | } 54 | 55 | return { 56 | text: () => JSON.stringify(response), 57 | json: () => response 58 | }; 59 | }; 60 | }; 61 | 62 | describe('loading poster arguments from environment and https', () => { 63 | test('fetchGasPrice', async () => { 64 | let gasPrice = await fetchGasPrice(mockFetch(gasResponses)); 65 | expect(gasPrice).toEqual(2600000000); 66 | }); 67 | 68 | test('fetchPayloads', async () => { 69 | // hits the http endpoints, encodes a transaction 70 | let payloads = await fetchPayloads(["http://localhost:3000", "http://localhost:3000/prices.json"], mockFetch(endpointResponses)); 71 | 72 | expect(payloads).toEqual([ 73 | { 74 | "messages": ["0xmessage"], 75 | "prices": { 76 | "eth": 260, 77 | "zrx": 0.58, 78 | }, 79 | "signatures": ["0xsignature"], 80 | }, 81 | { 82 | "messages": ["0xmessage"], 83 | "prices": { 84 | "eth": 250, 85 | "zrx": 1.58, 86 | }, 87 | "signatures": ["0xsignature"], 88 | }]); 89 | }); 90 | }); 91 | 92 | describe('building a function call', () => { 93 | test('findTypes', () => { 94 | let typeString = "writePrices(bytes[],bytes[],string[])"; 95 | expect(util.findTypes(typeString)).toEqual(["bytes[]", "bytes[]", "string[]"]); 96 | }); 97 | 98 | test('buildTrxData', () => { 99 | let feedItems = [{ 100 | message: '0x177ee777e72b8c042e05ef41d1db0f17f1fcb0e8150b37cfad6993e4373bdf10', 101 | signature: '0x04a78a7b3013f6939da19eac6fd1ad5c5a20c41bcc5d828557442aad6f07598d029ae684620bec13e13d018cba0da5096626e83cfd4d5356d808d7437a0a5076000000000000000000000000000000000000000000000000000000000000001c', 102 | price: 250.0, 103 | symbol: 'eth' 104 | }]; 105 | let messages = feedItems.map(({message}) => message); 106 | let signatures = feedItems.map(({signature}) => signature); 107 | 108 | let data = buildTrxData(feedItems, "writePrices(bytes[],bytes[],string[])"); 109 | 110 | let assumedAbi = { 111 | "constant": false, 112 | "inputs": [ 113 | { 114 | "name": "anything", 115 | "type": "bytes[]" 116 | }, 117 | { 118 | "name": "whatever", 119 | "type": "bytes[]" 120 | }, 121 | { 122 | "name": "whatnot", 123 | "type": "string[]" 124 | }, 125 | ], 126 | "name": "writePrices", 127 | "outputs": [], 128 | "payable": false, 129 | "stateMutability": "nonpayable", 130 | "type": "function" 131 | }; 132 | 133 | // @ts-ignore-start 134 | let officialWeb3Encoding = new Web3().eth.abi.encodeFunctionCall(assumedAbi, [messages, signatures, ['ETH']]); 135 | // @ts-ignore-end 136 | 137 | expect(data).toEqual(officialWeb3Encoding); 138 | }); 139 | }); 140 | 141 | describe('checking that numbers are within the specified delta range', () => { 142 | test('inDeltaRange', () => { 143 | expect(inDeltaRange(0, 9687.654999, 9696.640000)).toEqual(false); 144 | expect(inDeltaRange(0.01, 9687.654999, 9696.640000)).toEqual(false); 145 | expect(inDeltaRange(0.1, 9687.654999, 9696.640000)).toEqual(true); 146 | expect(inDeltaRange(5, 9687.654999, 9696.640000)).toEqual(true); 147 | 148 | expect(inDeltaRange(0, 1, 1)).toEqual(false); 149 | expect(inDeltaRange(-1, 1, 1)).toEqual(false); 150 | expect(inDeltaRange(101, 1, 1)).toEqual(false); 151 | expect(inDeltaRange(0.01, 1, 1)).toEqual(true); 152 | expect(inDeltaRange(5, 1, 1)).toEqual(true); 153 | expect(inDeltaRange(100, 1, 1)).toEqual(true); 154 | }) 155 | }) 156 | 157 | describe('filtering payloads', () => { 158 | function mockPrevPrices(prevPrices={}) { 159 | async function mockPreviousPrice(_sourceAddress, asset, _dataAddress, _web3) { 160 | return prevPrices[asset]; 161 | } 162 | 163 | const getSourceAddressSpy = jest.spyOn(prevPrice, 'getSourceAddress'); 164 | getSourceAddressSpy.mockImplementation(() => Promise.resolve("")); 165 | const getDataAddressSpy = jest.spyOn(prevPrice, 'getDataAddress'); 166 | getDataAddressSpy.mockImplementation(() => Promise.resolve("")); 167 | const getPreviousPriceSpy = jest.spyOn(prevPrice, 'getPreviousPrice'); 168 | getPreviousPriceSpy.mockImplementation(mockPreviousPrice); 169 | }; 170 | 171 | function mockMessages(messages: {[message: string]: DecodedMessage}) { 172 | const decodeMessageSpy = jest.spyOn(util, 'decodeMessage'); 173 | 174 | decodeMessageSpy.mockImplementation((message, web3) => { 175 | return messages[message]; 176 | }); 177 | }; 178 | 179 | function transformPayloads(payloads) { 180 | return Object.fromEntries(payloads.map((payload) => { 181 | return util.zip(Object.entries(payload.prices), payload.messages).map(([[symbol, price], message]) => { 182 | return [message, { 183 | dataType: 'type', 184 | timestamp: 0, 185 | symbol, 186 | price 187 | }]; 188 | }) 189 | }).flat()); 190 | } 191 | 192 | test('Filtering payloads, BAT price is more than delta % different', async () => { 193 | const payloads = [ 194 | { 195 | timestamp: '1593209100', 196 | messages: ['0x1', '0x2', '0x3', '0x4', '0x5', '0x6', '0x7', '0x8', '0x9'], 197 | signatures: ['0x1', '0x2', '0x3', '0x4', '0x5', '0x6', '0x7', '0x8', '0x9'], 198 | prices: { 199 | BTC: '9192.23', 200 | ETH: '230.585', 201 | XTZ: '2.5029500000000002', 202 | DAI: '1.0035515', 203 | REP: '16.83', 204 | ZRX: '0.3573955', 205 | BAT: '0.26466', 206 | KNC: '1.16535', 207 | LINK: '4.70819' 208 | } 209 | } 210 | ]; 211 | mockMessages(transformPayloads(payloads)); 212 | mockPrevPrices({ 'BTC': 9149090000, 'ETH': 229435000, 'DAI': 1003372, 'REP': 16884999, 'ZRX': 357704, 'BAT': 260992, 'KNC': 1156300, 'LINK': 4704680 }); 213 | 214 | const feedItems = await filterPayloads(payloads, '0x0', ['BTC', 'ETH', 'DAI', 'REP', 'ZRX', 'BAT', 'KNC', 'LINK', 'COMP'], {BTC: 1, ETH: 1, DAI: 1, REP: 1, ZRX: 1, BAT: 1, KNC: 1, LINK: 1, COMP: 1}, new Web3()); 215 | expect(feedItems).toEqual([ 216 | { 217 | dataType: "type", 218 | message: "0x7", 219 | prev: 0.260992, 220 | price: 0.26466, 221 | signature: "0x7", 222 | source: "", 223 | symbol: "BAT", 224 | timestamp: 0, 225 | } 226 | ]); 227 | }) 228 | 229 | test('Filtering payloads, ETH, BTC and ZRX prices are more than delta % different, ZRX, XTZ are not supported', async () => { 230 | mockPrevPrices({ 'BTC': 10000000000, 'ETH': 1000000000, 'ZRX': 1011000, 'REP': 16000000, 'DAI': 1000000, 'BAT': 1000000, 'KNC': 2000000, 'LINK': 5000000 }); 231 | 232 | const payloads = [ 233 | { 234 | timestamp: '1593209100', 235 | messages: ['0x1', '0x2', '0x3', '0x4', '0x5', '0x6', '0x7', '0x8', '0x9'], 236 | signatures: ['0x1', '0x2', '0x3', '0x4', '0x5', '0x6', '0x7', '0x8', '0x9'], 237 | prices: { 238 | BTC: '10101', 239 | ETH: '1011', 240 | XTZ: '10', 241 | DAI: '1', 242 | REP: '16', 243 | ZRX: '1.011', 244 | BAT: '1', 245 | KNC: '2', 246 | LINK: '5' 247 | } 248 | } 249 | ]; 250 | mockMessages(transformPayloads(payloads)); 251 | 252 | const feedItems = await filterPayloads(payloads, '0x0', ['BTC', 'ETH', 'DAI', 'REP', 'BAT', 'KNC', 'LINK', 'COMP'], {BTC: 1, ETH: 1, DAI: 1, REP: 1, ZRX: 1, BAT: 1, KNC: 1, LINK: 1, COMP: 1}, new Web3()); 253 | expect(feedItems).toEqual([ 254 | { 255 | message: '0x1', 256 | signature: '0x1', 257 | dataType: 'type', 258 | timestamp: 0, 259 | symbol: 'BTC', 260 | price: 10101, 261 | source: '', 262 | prev: 10000 263 | }, 264 | { 265 | message: '0x2', 266 | signature: '0x2', 267 | dataType: 'type', 268 | timestamp: 0, 269 | symbol: 'ETH', 270 | price: 1011, 271 | source: '', 272 | prev: 1000 273 | } 274 | ]); 275 | }) 276 | 277 | test('Filtering payloads, ETH, BTC and ZRX prices are more than delta % different, no assets are supported', async () => { 278 | mockPrevPrices({ 'BTC': 10000000000, 'ETH': 1000000000, 'ZRX': 1011000, 'REP': 16000000, 'DAI': 1000000, 'BAT': 1000000, 'KNC': 2000000, 'LINK': 5000000 }); 279 | 280 | const payloads = [ 281 | { 282 | timestamp: '1593209100', 283 | messages: ['0x1', '0x2', '0x3', '0x4', '0x5', '0x6', '0x7', '0x8', '0x9'], 284 | signatures: ['0x1', '0x2', '0x3', '0x4', '0x5', '0x6', '0x7', '0x8', '0x9'], 285 | prices: { 286 | BTC: '10101', 287 | ETH: '1011', 288 | XTZ: '10', 289 | DAI: '1', 290 | REP: '16', 291 | ZRX: '1.011', 292 | BAT: '1', 293 | KNC: '2', 294 | LINK: '5' 295 | } 296 | } 297 | ] 298 | mockMessages(transformPayloads(payloads)); 299 | 300 | const feedItems = await filterPayloads(payloads, '0x0', [], {}, new Web3()); 301 | expect(feedItems).toEqual([]); 302 | }) 303 | 304 | test('Filtering payloads, delta is 0% percent, all supported prices should be updated', async () => { 305 | mockPrevPrices({ 'BTC': 10000000000, 'ETH': 1000000000, 'ZRX': 1011000, 'REP': 16000000, 'DAI': 1000000, 'BAT': 1000000, 'KNC': 2000000, 'LINK': 5000000 }); 306 | 307 | const payloads = [ 308 | { 309 | timestamp: '1593209100', 310 | messages: ['0x1', '0x2', '0x3', '0x4', '0x5', '0x6', '0x7', '0x8', '0x9'], 311 | signatures: ['0x1', '0x2', '0x3', '0x4', '0x5', '0x6', '0x7', '0x8', '0x9'], 312 | prices: { 313 | BTC: '10101', 314 | ETH: '1011', 315 | XTZ: '10', 316 | DAI: '1', 317 | REP: '16', 318 | ZRX: '1.011', 319 | BAT: '1', 320 | KNC: '2', 321 | LINK: '5' 322 | } 323 | } 324 | ]; 325 | mockMessages(transformPayloads(payloads)); 326 | 327 | const feedItems = await filterPayloads(payloads, '0x0', ['BTC', 'ETH', 'DAI', 'REP', 'ZRX', 'BAT', 'KNC', 'LINK', 'COMP'], {BTC: 0, ETH: 0, DAI: 0, REP: 0, ZRX: 0, BAT: 0, KNC: 0, LINK: 0, COMP: 0}, new Web3()); 328 | expect(feedItems).toEqual([ 329 | { 330 | message: '0x1', 331 | signature: '0x1', 332 | dataType: 'type', 333 | timestamp: 0, 334 | symbol: 'BTC', 335 | price: 10101, 336 | source: '', 337 | prev: 10000 338 | }, 339 | { 340 | message: '0x2', 341 | signature: '0x2', 342 | dataType: 'type', 343 | timestamp: 0, 344 | symbol: 'ETH', 345 | price: 1011, 346 | source: '', 347 | prev: 1000 348 | }, 349 | { 350 | message: '0x4', 351 | signature: '0x4', 352 | dataType: 'type', 353 | timestamp: 0, 354 | symbol: 'DAI', 355 | price: 1, 356 | source: '', 357 | prev: 1 358 | }, 359 | { 360 | message: '0x5', 361 | signature: '0x5', 362 | dataType: 'type', 363 | timestamp: 0, 364 | symbol: 'REP', 365 | price: 16, 366 | source: '', 367 | prev: 16 368 | }, 369 | { 370 | message: '0x6', 371 | signature: '0x6', 372 | dataType: 'type', 373 | timestamp: 0, 374 | symbol: 'ZRX', 375 | price: 1.011, 376 | source: '', 377 | prev: 1.011 378 | }, 379 | { 380 | message: '0x7', 381 | signature: '0x7', 382 | dataType: 'type', 383 | timestamp: 0, 384 | symbol: 'BAT', 385 | price: 1, 386 | source: '', 387 | prev: 1 388 | }, 389 | { 390 | message: '0x8', 391 | signature: '0x8', 392 | dataType: 'type', 393 | timestamp: 0, 394 | symbol: 'KNC', 395 | price: 2, 396 | source: '', 397 | prev: 2 398 | }, 399 | { 400 | message: '0x9', 401 | signature: '0x9', 402 | dataType: 'type', 403 | timestamp: 0, 404 | symbol: 'LINK', 405 | price: 5, 406 | source: '', 407 | prev: 5 408 | } 409 | ]); 410 | }) 411 | }); 412 | -------------------------------------------------------------------------------- /poster/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es2018", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ 5 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | "lib": ["esnext"], /* Specify library files to be included in the compilation. */ 7 | // "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 11 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 12 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 13 | // "outFile": "./", /* Concatenate and emit output to single file. */ 14 | "outDir": "./.tsbuilt", /* Redirect output structure to the directory. */ 15 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 16 | // "composite": true, /* Enable project compilation */ 17 | // "removeComments": true, /* Do not emit comments to output. */ 18 | // "noEmit": true, /* Do not emit outputs. */ 19 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 20 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 21 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 22 | 23 | /* Strict Type-Checking Options */ 24 | "strict": true, /* Enable all strict type-checking options. */ 25 | "noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */ 26 | // "strictNullChecks": true, /* Enable strict null checks. */ 27 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 28 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 29 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 30 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 31 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 32 | 33 | /* Additional Checks */ 34 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 35 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 36 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 37 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 38 | 39 | /* Module Resolution Options */ 40 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 41 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 42 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 43 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 44 | // "typeRoots": [], /* List of folders to include type definitions from. */ 45 | // "types": [], /* Type declaration files to be included in compilation. */ 46 | "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 47 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 48 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 49 | 50 | /* Source Map Options */ 51 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 52 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 53 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 54 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 55 | 56 | /* Experimental Options */ 57 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 58 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 59 | }, 60 | "include": [ 61 | "./src/**/*" 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /scripts/deploy-uav.js: -------------------------------------------------------------------------------- 1 | const hre = require("hardhat"); 2 | const params = require("../configuration/parameters"); 3 | 4 | async function main() { 5 | const UAV = await hre.ethers.getContractFactory("UniswapAnchoredView"); 6 | const uav = await UAV.deploy(params[0], params[1], params[2], {gasLimit: 9000000}) 7 | await uav.deployed(); 8 | console.log("UAV deployed to: ", uav.address); 9 | } 10 | 11 | main() 12 | .then(() => process.exit(0)) 13 | .catch(error => { 14 | console.error(error); 15 | process.exit(1); 16 | }); -------------------------------------------------------------------------------- /sdk/javascript/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .tsbuilt 3 | -------------------------------------------------------------------------------- /sdk/javascript/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:13.6.0-alpine3.10 2 | 3 | WORKDIR /open-oracle-reporter 4 | ADD package.json /open-oracle-reporter/package.json 5 | RUN yarn install --ignore-scripts 6 | 7 | ADD . /open-oracle-reporter 8 | RUN yarn prepare 9 | 10 | CMD yarn start 11 | -------------------------------------------------------------------------------- /sdk/javascript/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## The Open Oracle Reporter 3 | 4 | The Open Oracle Reporter makes it easy to add a price feed to your application web server. 5 | 6 | ## Installation 7 | 8 | To add the Open Oracle Reporter to your application, run: 9 | 10 | ``` 11 | yarn add open-oracle-reporter 12 | ``` 13 | 14 | ## Running Stand-alone 15 | 16 | You can run this reporter as a stand-alone by providing a simple JavaScript function that will pull the data for the reporter. 17 | 18 | ```bash 19 | yarn global add open-oracle-reporter 20 | 21 | open-oracle-reporter \ 22 | --port 3000 \ 23 | --private_key file:./private_key \ 24 | --script ./fetchPrices.js \ 25 | --kind prices \ 26 | --path /prices.json \ 27 | ``` 28 | 29 | Or to quickly test using yarn: 30 | 31 | ```bash 32 | yarn run start \ 33 | --private_key 0x177ee777e72b8c042e05ef41d1db0f17f1fcb0e8150b37cfad6993e4373bdf10 \ 34 | --script examples/fixed.js 35 | ``` 36 | 37 | ## Usage 38 | 39 | Once you've installed the Open Oracle SDK, you can sign a Open Oracle feed as follows: 40 | 41 | ```typescript 42 | import {encode, sign} from 'open-oracle-reporter'; 43 | 44 | let encoded = encode('prices', Math.floor(+new Date / 1000), {'eth': 260.0, 'zrx': 0.58}); 45 | let signature = sign(encoded, '0x...'); 46 | ``` 47 | 48 | Or sign with a remote call: 49 | 50 | ```typescript 51 | import {signWith} from 'open-oracle-reporter'; 52 | 53 | let encoded = encode('prices', Math.floor(+new Date / 1000), {'eth': 260.0, 'zrx': 0.58}); 54 | let signature = signWith(encoded, '0x...', signer); 55 | ``` 56 | 57 | For example, in an express app: 58 | 59 | ```typescript 60 | // See above for signing data 61 | 62 | express.get('/prices.json', async (req, res) => { 63 | res.json({ 64 | encoded: encoded, 65 | signature: signature 66 | }); 67 | }); 68 | ``` 69 | 70 | You may also use the open oracle express adapter: 71 | 72 | ```typescript 73 | import {endpoint} from 'open-oracle-reporter'; 74 | 75 | async function fetchPrices(now) { 76 | return [now, {'eth': 260.0, 'zrx': 0.58}]; 77 | } 78 | 79 | app.use(endpoint('0x...', fetchPrices, 'prices', '/prices.json')); 80 | ``` 81 | -------------------------------------------------------------------------------- /sdk/javascript/examples/fixed.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = async function fetchPrices(now) { 3 | return [now, {'eth': 260.0, 'zrx': 0.58}]; 4 | } 5 | -------------------------------------------------------------------------------- /sdk/javascript/jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after `n` failures 9 | // bail: 0, 10 | 11 | // Respect "browser" field in package.json when resolving modules 12 | // browser: false, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "/private/var/folders/jz/z56b1n2902584b4zplqztm3m0000gn/T/jest_dx", 16 | 17 | // Automatically clear mock calls and instances between every test 18 | // clearMocks: false, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | // collectCoverage: false, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | // collectCoverageFrom: null, 25 | 26 | // The directory where Jest should output its coverage files 27 | // coverageDirectory: "coverage", 28 | 29 | // An array of regexp pattern strings used to skip coverage collection 30 | // coveragePathIgnorePatterns: [ 31 | // "/node_modules/" 32 | // ], 33 | 34 | // A list of reporter names that Jest uses when writing coverage reports 35 | // coverageReporters: [ 36 | // "json", 37 | // "text", 38 | // "lcov", 39 | // "clover" 40 | // ], 41 | 42 | // An object that configures minimum threshold enforcement for coverage results 43 | // coverageThreshold: null, 44 | 45 | // A path to a custom dependency extractor 46 | // dependencyExtractor: null, 47 | 48 | // Make calling deprecated APIs throw helpful error messages 49 | // errorOnDeprecated: false, 50 | 51 | // Force coverage collection from ignored files using an array of glob patterns 52 | // forceCoverageMatch: [], 53 | 54 | // A path to a module which exports an async function that is triggered once before all test suites 55 | // globalSetup: './.tsbuilt/test.js', 56 | 57 | // A path to a module which exports an async function that is triggered once after all test suites 58 | // globalTeardown: null, 59 | 60 | // A set of global variables that need to be available in all test environments 61 | // globals: {}, 62 | 63 | // An array of directory names to be searched recursively up from the requiring module's location 64 | // moduleDirectories: [ 65 | // "node_modules" 66 | // ], 67 | 68 | // An array of file extensions your modules use 69 | // moduleFileExtensions: [ 70 | // "js", 71 | // "json", 72 | // "jsx", 73 | // "ts", 74 | // "tsx", 75 | // "node" 76 | // ], 77 | 78 | // A map from regular expressions to module names that allow to stub out resources with a single module 79 | // moduleNameMapper: {}, 80 | 81 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 82 | // modulePathIgnorePatterns: [], 83 | 84 | // Activates notifications for test results 85 | // notify: false, 86 | 87 | // An enum that specifies notification mode. Requires { notify: true } 88 | // notifyMode: "failure-change", 89 | 90 | // A preset that is used as a base for Jest's configuration 91 | preset: 'ts-jest', 92 | 93 | // Run tests from one or more projects 94 | // projects: null, 95 | 96 | // Use this configuration option to add custom reporters to Jest 97 | // reporters: undefined, 98 | 99 | // Automatically reset mock state between every test 100 | // resetMocks: false, 101 | 102 | // Reset the module registry before running each individual test 103 | // resetModules: false, 104 | 105 | // A path to a custom resolver 106 | // resolver: null, 107 | 108 | // Automatically restore mock state between every test 109 | // restoreMocks: false, 110 | 111 | // The root directory that Jest should scan for tests and modules within 112 | // rootDir: null, 113 | 114 | // A list of paths to directories that Jest should use to search for files in 115 | // roots: [ 116 | // "" 117 | // ], 118 | 119 | // Allows you to use a custom runner instead of Jest's default test runner 120 | // runner: "jest-runner", 121 | 122 | // The paths to modules that run some code to configure or set up the testing environment before each test 123 | // setupFiles: [], 124 | 125 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 126 | // setupFilesAfterEnv: [] 127 | 128 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 129 | // snapshotSerializers: [], 130 | 131 | // The test environment that will be used for testing 132 | testEnvironment: "node", 133 | 134 | // Options that will be passed to the testEnvironment 135 | // testEnvironmentOptions: {}, 136 | 137 | // Adds a location field to test results 138 | // testLocationInResults: false, 139 | 140 | // The glob patterns Jest uses to detect test files 141 | testMatch: [ 142 | "**/tests/**/*.[jt]s?(x)" 143 | ], 144 | 145 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 146 | // testPathIgnorePatterns: [ 147 | // "/node_modules/" 148 | // ], 149 | 150 | // The regexp pattern or array of patterns that Jest uses to detect test files 151 | // testRegex: [], 152 | 153 | // This option allows the use of a custom results processor 154 | // testResultsProcessor: null, 155 | 156 | // This option allows use of a custom test runner 157 | // testRunner: "jasmine2", 158 | 159 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 160 | // testURL: "http://localhost", 161 | 162 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 163 | // timers: "real", 164 | 165 | // A map from regular expressions to paths to transformers 166 | // transform: null, 167 | 168 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 169 | // transformIgnorePatterns: [ 170 | // "/node_modules/" 171 | // ], 172 | 173 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 174 | // unmockedModulePathPatterns: undefined, 175 | 176 | // Indicates whether each individual test should be reported during the run 177 | verbose: true, 178 | 179 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 180 | // watchPathIgnorePatterns: [], 181 | 182 | // Whether to use watchman for file crawling 183 | // watchman: true, 184 | }; 185 | -------------------------------------------------------------------------------- /sdk/javascript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "open-oracle-reporter", 3 | "version": "1.0.2", 4 | "description": "The Open Oracle Reporter", 5 | "main": ".tsbuilt/index.js", 6 | "types": ".tsbuilt/index.js", 7 | "repository": "https://github.com/compound-finance/open-oracle", 8 | "author": "Compound Labs, Inc.", 9 | "license": "MIT", 10 | "scripts": { 11 | "start": "npx tsc --skipLibCheck && node ./.tsbuilt/cli.js", 12 | "prepare": "npx tsc --skipLibCheck", 13 | "test": "jest" 14 | }, 15 | "dependencies": { 16 | "web3": "^1.2.4" 17 | }, 18 | "devDependencies": { 19 | "@types/jest": "^24.0.23", 20 | "jest": "^24.9.0", 21 | "jest-junit": "^10.0.0", 22 | "node-fetch": "^2.6.0", 23 | "ts-jest": "^24.2.0", 24 | "typescript": "^3.7.3" 25 | }, 26 | "optionalDependencies": { 27 | "@types/express": "^4.17.2", 28 | "express": "^4.17.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /sdk/javascript/src/cli.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import {Filters, endpoint} from './express_endpoint'; 3 | import yargs from 'yargs'; 4 | import {loadKey} from './key'; 5 | import * as fs from 'fs'; 6 | import * as Path from 'path'; 7 | 8 | const argv = yargs 9 | .option('port', {alias: 'p', description: 'Port to listen on', type: 'number', default: 3000}) 10 | .option('private_key', {alias: 'k', description: 'Private key (try: `file: or env:`', type: 'string'}) 11 | .option('script', {alias: 's', description: 'Script for data', type: 'string'}) 12 | .option('filter', {alias: 'f', description: 'Filter for data', type: 'string', default: 'symbols'}) 13 | .option('kind', {alias: 'K', description: 'Kind of data to encode', type: 'string', default: 'prices'}) 14 | .option('path', {alias: 'u', description: 'Path for endpoint', type: 'string', default: '/prices.json'}) 15 | .help() 16 | .alias('help', 'h') 17 | .demandOption(['private_key', 'script'], 'Please provide both run and path arguments to work with this tool') 18 | .argv; 19 | 20 | // Create a new express application instance 21 | const app: express.Application = express(); 22 | 23 | function fetchEnv(name: string): string { 24 | let res = process.env[name]; 25 | if (res) { 26 | return res; 27 | } 28 | throw `Cannot find env var "${name}"`; 29 | } 30 | 31 | async function start( 32 | port: number, 33 | privateKey: string, 34 | script: string, 35 | filter: string, 36 | kind: string, 37 | path: string 38 | ) { 39 | const fn: any = await import(Path.join(process.cwd(), script)); 40 | app.use(endpoint(privateKey, fn.default, Filters[filter], kind, path)); 41 | app.listen(port, function () { 42 | console.log(`Reporter listening on port ${port}. Try running "curl http://localhost:${port}${path}"`); 43 | }); 44 | } 45 | 46 | start(argv.port, argv.private_key, argv.script, argv.filter, argv.kind, argv.path); 47 | -------------------------------------------------------------------------------- /sdk/javascript/src/express_endpoint.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import {encode, sign} from './reporter'; 3 | 4 | type UnixT = number; 5 | type Pairs = object | [any, any][]; 6 | type Query = any; 7 | 8 | function flatStringList(param: string | string[]): string[] { 9 | if (Array.isArray(param)) 10 | return param.reduce((a: string[], s) => a.concat(flatStringList(s)), []); 11 | return param.split(','); 12 | } 13 | 14 | function upper(str: string): string { 15 | return str.toUpperCase(); 16 | } 17 | 18 | export const Filters = { 19 | symbols: (pairs: Pairs, query: Query): Pairs => { 20 | if (query.symbols) { 21 | const symbols = new Set(flatStringList(query.symbols).map(upper)); 22 | if (Array.isArray(pairs)) 23 | return pairs.filter(([k, _]) => symbols.has(upper(k))); 24 | return Object.entries(pairs).reduce((a, [k, v]) => { 25 | if (symbols.has(upper(k))) 26 | a[k] = v; 27 | return a; 28 | }, {}); 29 | } 30 | return pairs; 31 | } 32 | } 33 | 34 | export function endpoint( 35 | privateKey: string, 36 | getter: (now: UnixT) => Promise<[UnixT, Pairs]>, 37 | filter: (pairs: Pairs, query: Query) => Pairs = Filters.symbols, 38 | kind: string = 'prices', 39 | path: string = `/${kind}.json` 40 | ): express.Application { 41 | return express() 42 | .get(path, async (req, res) => { 43 | const [timestamp, pairs] = await getter(Math.floor(+new Date / 1000)); 44 | const filtered = filter(pairs, req.query); 45 | const signed = sign(encode(kind, timestamp, filtered), privateKey); 46 | res.json({ 47 | messages: signed.map(s => s.message), 48 | signatures: signed.map(s => s.signature), 49 | timestamp: timestamp, 50 | [kind]: filtered 51 | }); 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /sdk/javascript/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as ExpressEndpoint from './express_endpoint' 2 | import * as Reporter from './reporter' 3 | import * as KeyLoader from './key' 4 | 5 | export { 6 | ExpressEndpoint, 7 | Reporter, 8 | KeyLoader 9 | } 10 | -------------------------------------------------------------------------------- /sdk/javascript/src/key.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as fs from 'fs'; 3 | 4 | export function loadKey(key: string | undefined): string | undefined { 5 | let privateKey; 6 | 7 | if (!key) { 8 | return undefined; 9 | } 10 | 11 | let fileMatch = /file:.*/.exec(key); 12 | if (fileMatch) { 13 | return fs.readFileSync(fileMatch[1], 'utf8'); 14 | } 15 | 16 | let envMatch = /env:.*/.exec(key); 17 | if (envMatch) { 18 | return process.env[envMatch[1]]; 19 | } 20 | 21 | return key; 22 | } 23 | -------------------------------------------------------------------------------- /sdk/javascript/src/reporter.ts: -------------------------------------------------------------------------------- 1 | import Web3 from 'web3'; 2 | 3 | const web3 = new Web3(null); // This is just for encoding, etc. 4 | 5 | interface SignedMessage { 6 | hash: string, 7 | message: string, 8 | signature: string, 9 | signatory: string 10 | }; 11 | 12 | export function getKeyAndValueType(kind: string): [string, string] { 13 | switch (kind) { 14 | case 'prices': 15 | return ['symbol', 'decimal']; 16 | default: 17 | throw new Error(`Unknown kind of data "${kind}"`); 18 | } 19 | } 20 | 21 | export function fancyParameterDecoder(paramType: string): [string, (any) => any] { 22 | let actualParamType = paramType, actualParamDec = (x) => x; 23 | 24 | if (paramType == 'decimal') { 25 | actualParamType = 'uint64'; 26 | actualParamDec = (x) => x / 1e6; 27 | } 28 | 29 | if (paramType == 'symbol') { 30 | actualParamType = 'string'; 31 | actualParamDec = (x) => x; // we don't know what the original case was anymore 32 | } 33 | 34 | return [actualParamType, actualParamDec]; 35 | } 36 | 37 | export function decode(kind: string, messages: string[]): [number, any, any][] { 38 | const [keyType, valueType] = getKeyAndValueType(kind); 39 | const [kType, kDec] = fancyParameterDecoder(keyType); 40 | const [vType, vDec] = fancyParameterDecoder(valueType); 41 | return messages.map((message): [number, any, any] => { 42 | const {0: kind_, 1: timestamp, 2: key, 3: value} = web3.eth.abi.decodeParameters(['string', 'uint64', kType, vType], message); 43 | if (kind_ != kind) 44 | throw new Error(`Expected data of kind ${kind}, got ${kind_}`); 45 | return [timestamp, key, value]; 46 | }); 47 | } 48 | 49 | export function fancyParameterEncoder(paramType: string): [string, (any) => any] { 50 | let actualParamType = paramType, actualParamEnc = (x) => x; 51 | 52 | // We add a decimal type for reporter convenience. 53 | // Decimals are encoded as uints with 6 decimals of precision on-chain. 54 | if (paramType === 'decimal') { 55 | actualParamType = 'uint64'; 56 | actualParamEnc = (x) => web3.utils.toBN(1e6).muln(x).toString(); 57 | } 58 | 59 | if (paramType == 'symbol') { 60 | actualParamType = 'string'; 61 | actualParamEnc = (x) => x.toUpperCase(); 62 | } 63 | 64 | return [actualParamType, actualParamEnc]; 65 | } 66 | 67 | export function encode(kind: string, timestamp: number, pairs: [any, any][] | object): string[] { 68 | const [keyType, valueType] = getKeyAndValueType(kind); 69 | const [kType, kEnc] = fancyParameterEncoder(keyType); 70 | const [vType, vEnc] = fancyParameterEncoder(valueType); 71 | const actualPairs = Array.isArray(pairs) ? pairs : Object.entries(pairs); 72 | return actualPairs.map(([key, value]) => { 73 | return web3.eth.abi.encodeParameters(['string', 'uint64', kType, vType], [kind, timestamp, kEnc(key), vEnc(value)]); 74 | }); 75 | } 76 | 77 | export function encodeRotationMessage(rotationTarget: string) : string { 78 | return web3.eth.abi.encodeParameters(['string', 'address'], ['rotate', rotationTarget]); 79 | } 80 | 81 | export function sign(messages: string | string[], privateKey: string): SignedMessage[] { 82 | const actualMessages = Array.isArray(messages) ? messages : [messages]; 83 | return actualMessages.map((message) => { 84 | const hash = web3.utils.keccak256(message); 85 | const {r, s, v} = web3.eth.accounts.sign(hash, privateKey); 86 | const signature = web3.eth.abi.encodeParameters(['bytes32', 'bytes32', 'uint8'], [r, s, v]); 87 | const signatory = web3.eth.accounts.recover(hash, v, r, s); 88 | return {hash, message, signature, signatory}; 89 | }); 90 | } 91 | 92 | export async function signWith(messages: string | string[], signer: (string) => Promise<{r: string, s: string, v: string}>): Promise { 93 | const actualMessages = Array.isArray(messages) ? messages : [messages]; 94 | return await Promise.all(actualMessages.map(async (message) => { 95 | const hash = web3.utils.keccak256(message); 96 | const {r, s, v} = await signer(hash); 97 | const signature = web3.eth.abi.encodeParameters(['bytes32', 'bytes32', 'uint8'], [r, s, v]); 98 | const signatory = web3.eth.accounts.recover(hash, v, r, s); 99 | return {hash, message, signature, signatory}; 100 | })); 101 | } 102 | -------------------------------------------------------------------------------- /sdk/javascript/tests/express_endpoint_test.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | import {endpoint} from '../src/express_endpoint'; 3 | 4 | test('integration test', async () => { 5 | const privateKey = '0x177ee777e72b8c042e05ef41d1db0f17f1fcb0e8150b37cfad6993e4373bdf10'; 6 | const timestamp = 1563606000; 7 | 8 | async function fetchPrices(): Promise<[number, object]> { 9 | return [timestamp, {'eth': 260.0, 'zrx': 0.58}]; 10 | } 11 | 12 | const port = 10123; 13 | const app = await endpoint(privateKey, fetchPrices).listen(port); 14 | 15 | const response1 = await fetch(`http://localhost:${port}/prices.json`); 16 | expect(response1.ok).toBe(true); 17 | expect(await response1.json()).toEqual({ 18 | messages: [ 19 | "0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000005d32bbf000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000f7f49000000000000000000000000000000000000000000000000000000000000000006707269636573000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000034554480000000000000000000000000000000000000000000000000000000000", 20 | "0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000005d32bbf000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000008d9a00000000000000000000000000000000000000000000000000000000000000006707269636573000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000035a52580000000000000000000000000000000000000000000000000000000000" 21 | ], 22 | prices: { 23 | eth: 260, 24 | zrx: 0.58, 25 | }, 26 | timestamp: timestamp, 27 | signatures: [ 28 | "0x1ad78f3fadc4ff82206e9144da21a9e3c7150054af3b629c134356b78571892d7a19419a4841af321cf9d3a537995cb75378984b61461316e873979a3c071ce0000000000000000000000000000000000000000000000000000000000000001c", 29 | "0xd76c7a01c9b12bcf0e759f42d79a867ea92f1a7ad2b96c49feede44392fe45684bdd51dd3f18fd24b54d65526e36eeff0c8b0d108d2cdde35f18e4c1cd7f059f000000000000000000000000000000000000000000000000000000000000001b" 30 | ] 31 | }); 32 | 33 | const response2 = await fetch(`http://localhost:${port}/prices.json?symbols=eth,zrx`); 34 | expect(response2.ok).toBe(true); 35 | expect(await response2.json()).toEqual({ 36 | messages: [ 37 | "0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000005d32bbf000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000f7f49000000000000000000000000000000000000000000000000000000000000000006707269636573000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000034554480000000000000000000000000000000000000000000000000000000000", 38 | "0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000005d32bbf000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000008d9a00000000000000000000000000000000000000000000000000000000000000006707269636573000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000035a52580000000000000000000000000000000000000000000000000000000000" 39 | ], 40 | prices: { 41 | eth: 260, 42 | zrx: 0.58 43 | }, 44 | timestamp: timestamp, 45 | signatures: [ 46 | "0x1ad78f3fadc4ff82206e9144da21a9e3c7150054af3b629c134356b78571892d7a19419a4841af321cf9d3a537995cb75378984b61461316e873979a3c071ce0000000000000000000000000000000000000000000000000000000000000001c", 47 | "0xd76c7a01c9b12bcf0e759f42d79a867ea92f1a7ad2b96c49feede44392fe45684bdd51dd3f18fd24b54d65526e36eeff0c8b0d108d2cdde35f18e4c1cd7f059f000000000000000000000000000000000000000000000000000000000000001b" 48 | ] 49 | }); 50 | 51 | const response3 = await fetch(`http://localhost:${port}/prices.json?symbols=etH`); 52 | expect(response3.ok).toBe(true); 53 | expect(await response3.json()).toEqual({ 54 | messages: [ 55 | "0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000005d32bbf000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000f7f49000000000000000000000000000000000000000000000000000000000000000006707269636573000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000034554480000000000000000000000000000000000000000000000000000000000" 56 | ], 57 | prices: { 58 | eth: 260 59 | }, 60 | timestamp: timestamp, 61 | signatures: [ 62 | "0x1ad78f3fadc4ff82206e9144da21a9e3c7150054af3b629c134356b78571892d7a19419a4841af321cf9d3a537995cb75378984b61461316e873979a3c071ce0000000000000000000000000000000000000000000000000000000000000001c" 63 | ] 64 | }); 65 | 66 | const response4 = await fetch(`http://localhost:${port}/prices.json?symbols=ZRX`); 67 | expect(response4.ok).toBe(true); 68 | expect(await response4.json()).toEqual({ 69 | messages: [ 70 | "0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000005d32bbf000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000008d9a00000000000000000000000000000000000000000000000000000000000000006707269636573000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000035a52580000000000000000000000000000000000000000000000000000000000" 71 | ], 72 | prices: { 73 | zrx: 0.58 74 | }, 75 | timestamp: timestamp, 76 | signatures: [ 77 | "0xd76c7a01c9b12bcf0e759f42d79a867ea92f1a7ad2b96c49feede44392fe45684bdd51dd3f18fd24b54d65526e36eeff0c8b0d108d2cdde35f18e4c1cd7f059f000000000000000000000000000000000000000000000000000000000000001b" 78 | ] 79 | }); 80 | 81 | const response5 = await fetch(`http://localhost:${port}/prices.json?symbols=bat`); 82 | expect(response5.ok).toBe(true); 83 | expect(await response5.json()).toEqual({ 84 | messages: [], 85 | prices: {}, 86 | timestamp: timestamp, 87 | signatures: [] 88 | }); 89 | 90 | await new Promise(ok => app.close(ok)); 91 | }); 92 | -------------------------------------------------------------------------------- /sdk/javascript/tests/reporter_test.ts: -------------------------------------------------------------------------------- 1 | import {decode, encode, encodeRotationMessage, sign, signWith} from '../src/reporter'; 2 | import Web3 from 'web3'; 3 | import utils from 'web3-utils'; 4 | 5 | const web3 = new Web3(null); // This is just for encoding, etc. 6 | 7 | test('encode', async () => { 8 | let encoded = encode('prices', 12345678, {"eth": 5.0, "zrx": 10.0}); 9 | expect(decode('prices', encoded).map(([t, k, v]) => [t.toString(), k, v.toString()])).toEqual([ 10 | ["12345678", 'ETH', (5.0e6).toString()], 11 | ["12345678", 'ZRX', (10.0e6).toString()] 12 | ]); 13 | }); 14 | 15 | test('sign', async () => { 16 | let [{signatory, signature}] = sign('some data', '0x177ee777e72b8c042e05ef41d1db0f17f1fcb0e8150b37cfad6993e4373bdf10'); 17 | expect(signature).toEqual('0x04a78a7b3013f6939da19eac6fd1ad5c5a20c41bcc5d828557442aad6f07598d029ae684620bec13e13d018cba0da5096626e83cfd4d5356d808d7437a0a5076000000000000000000000000000000000000000000000000000000000000001c'); 18 | expect(signatory).toEqual('0x1826265c3156c3B9b9e751DC4635376F3CD6ee06'); 19 | }); 20 | 21 | test('should handle signing an empty array', async () => { 22 | const encoded = encode('prices', 12345678, []); 23 | expect(encoded).toEqual([]); 24 | const signed = sign(encoded, '0x177ee777e72b8c042e05ef41d1db0f17f1fcb0e8150b37cfad6993e4373bdf10'); 25 | expect(signed).toEqual([]); 26 | }); 27 | 28 | test('signing rotation message', async () => { 29 | const rotationTarget = '0xAbcdef0123456789000000000000000000000005' 30 | const encoded = encodeRotationMessage(rotationTarget) 31 | const [ signed ] = sign(encoded, '0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d'); 32 | 33 | const {0: _type, 1: target} = web3.eth.abi.decodeParameters(['string', 'address'], signed.message); 34 | expect(_type).toEqual('rotate') 35 | expect(target).toEqual(rotationTarget); 36 | 37 | const recoverable = utils.keccak256(encoded) 38 | const recovered = web3.eth.accounts.recover(recoverable, signed.signature) 39 | 40 | expect(signed.signatory).toEqual('0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1'); 41 | expect(recovered).toEqual(signed.signatory) 42 | }); 43 | 44 | test('signWith', async () => { 45 | let signer = async (hash) => web3.eth.accounts.sign(hash, '0x177ee777e72b8c042e05ef41d1db0f17f1fcb0e8150b37cfad6993e4373bdf10'); 46 | let [{signature}] = await signWith('some data', signer); 47 | expect(signature).toEqual('0x04a78a7b3013f6939da19eac6fd1ad5c5a20c41bcc5d828557442aad6f07598d029ae684620bec13e13d018cba0da5096626e83cfd4d5356d808d7437a0a5076000000000000000000000000000000000000000000000000000000000000001c'); 48 | }); 49 | -------------------------------------------------------------------------------- /sdk/javascript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es2018", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ 5 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | "lib": ["esnext"], /* Specify library files to be included in the compilation. */ 7 | // "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 11 | "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 12 | "sourceMap": true, /* Generates corresponding '.map' file. */ 13 | // "outFile": "./", /* Concatenate and emit output to single file. */ 14 | "outDir": "./.tsbuilt", /* Redirect output structure to the directory. */ 15 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 16 | // "composite": true, /* Enable project compilation */ 17 | // "removeComments": true, /* Do not emit comments to output. */ 18 | // "noEmit": true, /* Do not emit outputs. */ 19 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 20 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 21 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 22 | 23 | /* Strict Type-Checking Options */ 24 | "strict": true, /* Enable all strict type-checking options. */ 25 | "noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */ 26 | // "strictNullChecks": true, /* Enable strict null checks. */ 27 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 28 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 29 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 30 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 31 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 32 | 33 | /* Additional Checks */ 34 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 35 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 36 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 37 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 38 | 39 | /* Module Resolution Options */ 40 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 41 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 42 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 43 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 44 | // "typeRoots": [], /* List of folders to include type definitions from. */ 45 | // "types": [], /* Type declaration files to be included in compilation. */ 46 | "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 47 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 48 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 49 | 50 | /* Source Map Options */ 51 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 52 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 53 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 54 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 55 | 56 | /* Experimental Options */ 57 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 58 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 59 | }, 60 | "declaration": true, 61 | "include": [ 62 | "./src/**/*" 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /tasks/price-oracle-tasks.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | import { HardhatRuntimeEnvironment } from "hardhat/types"; 3 | import parametersMainnet from "../configuration/parameters-price-oracle"; 4 | 5 | // Ignore pairs that are not configured 6 | const IGNORED_PAIRS = ["cREP", "cFEI"]; 7 | 8 | // Prints the price returned by the UAV and Price Oracle if not equal. 9 | // The price returned by the new contract can differ from the UAV without the Uniswap anchor 10 | // so a manually inspection of the prices are necessary 11 | export async function compareUavAndPriceOracle( 12 | arg: { 13 | production: string; 14 | proposed: string; 15 | }, 16 | hre: HardhatRuntimeEnvironment 17 | ) { 18 | const PRODUCTION_UAV_ADDR = arg.production; 19 | const PROPOSED_PRICE_ORACLE_ADDR = arg.proposed; 20 | const RPC_URL = process.env.MAINNET_URL; 21 | 22 | const uavConfiguration = require("../configuration/parameters"); 23 | const uavABI = require("../artifacts/contracts/Uniswap/UniswapAnchoredView.sol/UniswapAnchoredView.json"); 24 | const priceOracleABI = require("../artifacts/contracts/PriceOracle/PriceOracle.sol/PriceOracle.json"); 25 | 26 | const cTokenABI = [ 27 | { 28 | constant: true, 29 | inputs: [], 30 | name: "symbol", 31 | outputs: [{ name: "", type: "string" }], 32 | payable: false, 33 | stateMutability: "view", 34 | type: "function", 35 | }, 36 | ]; 37 | 38 | const [_, __, tokenConfigs] = uavConfiguration; 39 | if (!RPC_URL) { 40 | throw new Error("RPC URL Cannot be empty"); 41 | } 42 | const provider = new hre.ethers.providers.JsonRpcProvider(RPC_URL); 43 | const prodUAV = new hre.ethers.Contract( 44 | PRODUCTION_UAV_ADDR, 45 | uavABI.abi, 46 | provider 47 | ); 48 | const proposedPriceOracle = new hre.ethers.Contract( 49 | PROPOSED_PRICE_ORACLE_ADDR, 50 | priceOracleABI.abi, 51 | provider 52 | ); 53 | 54 | for (const { cToken: cTokenAddr } of tokenConfigs) { 55 | const checksumCTokenAddr = hre.ethers.utils.getAddress(cTokenAddr); 56 | const cToken = new hre.ethers.Contract( 57 | checksumCTokenAddr, 58 | cTokenABI, 59 | provider 60 | ); 61 | const cTokenSymbol = await cToken.symbol(); 62 | 63 | if (IGNORED_PAIRS.indexOf(cTokenSymbol) !== -1) { 64 | console.log(`Skipping check for ${cTokenSymbol}`); 65 | continue; 66 | } 67 | 68 | console.log( 69 | `Comparing prices for cToken ${cTokenSymbol} with address ${checksumCTokenAddr}` 70 | ); 71 | const [prodUAVPrice, proposedPriceOraclePrice] = await Promise.all([ 72 | fetchUnderlyingPrice(prodUAV, checksumCTokenAddr, PRODUCTION_UAV_ADDR), 73 | fetchUnderlyingPrice( 74 | proposedPriceOracle, 75 | checksumCTokenAddr, 76 | PROPOSED_PRICE_ORACLE_ADDR 77 | ), 78 | ]); 79 | 80 | if (!prodUAVPrice.eq(proposedPriceOraclePrice)) { 81 | console.log( 82 | `Prod UAV price: $${prodUAVPrice}. Proposed Price Oracle price: $${proposedPriceOraclePrice}` 83 | ); 84 | continue; 85 | } 86 | console.log( 87 | `Underlying prices for ${cTokenSymbol} match: $${proposedPriceOraclePrice}` 88 | ); 89 | } 90 | } 91 | 92 | async function fetchUnderlyingPrice( 93 | contract: ethers.Contract, 94 | cTokenAddr: string, 95 | proposedUAVAddr: string 96 | ) { 97 | try { 98 | return await contract.getUnderlyingPrice(cTokenAddr); 99 | } catch (e) { 100 | const label = 101 | contract.address === proposedUAVAddr ? "PROPOSED" : "PRODUCTION"; 102 | throw new Error( 103 | `Call to getUnderlyingPrice(${cTokenAddr}) to ${label} at address ${contract.address} reverted!` 104 | ); 105 | } 106 | } 107 | 108 | export async function deployPriceOracle( 109 | _: any, 110 | hre: HardhatRuntimeEnvironment 111 | ) { 112 | const PriceOracle = await hre.ethers.getContractFactory("PriceOracle"); 113 | let parameters; 114 | if (hre.network.name === "mainnet" || hre.network.name === "hardhat") { 115 | parameters = parametersMainnet; 116 | } else { 117 | throw Error("Invalid network"); 118 | } 119 | const priceOracle = await PriceOracle.deploy(parameters, { 120 | gasLimit: 9000000, 121 | }); 122 | await priceOracle.deployed(); 123 | console.log("PriceOracle deployed to: ", priceOracle.address); 124 | } 125 | -------------------------------------------------------------------------------- /tasks/verify-uav.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | import { HardhatRuntimeEnvironment } from "hardhat/types"; 3 | 4 | interface Args { 5 | production: string; 6 | proposed: string; 7 | } 8 | 9 | // Ignore pairs that are not configured 10 | const IGNORED_PAIRS = ["cMATIC"]; 11 | 12 | // Pair uses a different aggregator in the new UAV 13 | const PAIRS_WITH_EXPECTED_PRICE_DEVIATION = ["cSUSHI"]; 14 | 15 | const MAX_DEVIATION = 0.5; //% 16 | 17 | export default async function verifyProposedUAV( 18 | arg: Args, 19 | hre: HardhatRuntimeEnvironment 20 | ) { 21 | const PRODUCTION_UAV_ADDR = arg.production; 22 | const PROPOSED_UAV_ADDR = arg.proposed; 23 | const RPC_URL = process.env.MAINNET_URL; 24 | 25 | const uavConfiguration = require("../configuration/parameters"); 26 | const uavABI = require("../artifacts/contracts/Uniswap/UniswapAnchoredView.sol/UniswapAnchoredView.json"); 27 | 28 | const cTokenABI = [ 29 | { 30 | constant: true, 31 | inputs: [], 32 | name: "symbol", 33 | outputs: [{ name: "", type: "string" }], 34 | payable: false, 35 | stateMutability: "view", 36 | type: "function", 37 | }, 38 | ]; 39 | 40 | const [_, __, tokenConfigs] = uavConfiguration; 41 | if (!RPC_URL) { 42 | throw new Error("RPC URL Cannot be empty"); 43 | } 44 | const provider = new hre.ethers.providers.JsonRpcProvider(RPC_URL); 45 | const prodUAV = new hre.ethers.Contract( 46 | PRODUCTION_UAV_ADDR, 47 | uavABI.abi, 48 | provider 49 | ); 50 | const proposedUAV = new hre.ethers.Contract( 51 | PROPOSED_UAV_ADDR, 52 | uavABI.abi, 53 | provider 54 | ); 55 | 56 | for (const { cToken: cTokenAddr } of tokenConfigs) { 57 | const checksumCTokenAddr = hre.ethers.utils.getAddress(cTokenAddr); 58 | const cToken = new hre.ethers.Contract( 59 | checksumCTokenAddr, 60 | cTokenABI, 61 | provider 62 | ); 63 | const cTokenSymbol = await cToken.symbol(); 64 | 65 | if (IGNORED_PAIRS.indexOf(cTokenSymbol) !== -1) { 66 | console.log(`Skipping check for ${cTokenSymbol}`); 67 | continue; 68 | } 69 | 70 | console.log( 71 | `Comparing prices for cToken ${cTokenSymbol} with address ${checksumCTokenAddr}` 72 | ); 73 | const [prodUAVPrice, proposedUAVPrice] = await Promise.all([ 74 | fetchUnderlyingPrice( 75 | prodUAV, 76 | checksumCTokenAddr, 77 | cTokenSymbol, 78 | PRODUCTION_UAV_ADDR 79 | ), 80 | fetchUnderlyingPrice( 81 | proposedUAV, 82 | checksumCTokenAddr, 83 | cTokenSymbol, 84 | PROPOSED_UAV_ADDR 85 | ), 86 | ]); 87 | 88 | if (!prodUAVPrice.eq(proposedUAVPrice)) { 89 | const errorMsg = `Price mismatch for ${cTokenSymbol}! Prod UAV Price: ${prodUAVPrice.toString()} Proposed UAV Price: ${proposedUAVPrice.toString()}`; 90 | if (PAIRS_WITH_EXPECTED_PRICE_DEVIATION.indexOf(cTokenSymbol) === -1) { 91 | throw new Error(errorMsg); 92 | } 93 | const prodPriceNum = parseFloat( 94 | ethers.utils.formatEther(prodUAVPrice.toString()) 95 | ); 96 | const proposedPriceNum = parseFloat( 97 | ethers.utils.formatEther(proposedUAVPrice.toString()) 98 | ); 99 | 100 | const pctDeviation = 101 | (100 * Math.abs(prodPriceNum - proposedPriceNum)) / prodPriceNum; 102 | 103 | if (pctDeviation >= MAX_DEVIATION) throw new Error(errorMsg); 104 | } 105 | console.log(`Underlying prices for ${cTokenSymbol} match.`); 106 | } 107 | 108 | console.log( 109 | `Proposed UAV at ${PROPOSED_UAV_ADDR} passed all checks with the production UAV at ${PRODUCTION_UAV_ADDR}!` 110 | ); 111 | } 112 | 113 | async function fetchUnderlyingPrice( 114 | uavContract: ethers.Contract, 115 | cTokenAddr: string, 116 | cTokenSymbol: string, 117 | proposedUAVAddr: string 118 | ) { 119 | try { 120 | return await uavContract.getUnderlyingPrice(cTokenAddr); 121 | } catch (e) { 122 | const label = 123 | uavContract.address === proposedUAVAddr ? "PROPOSED" : "PRODUCTION"; 124 | throw new Error( 125 | `Call to getUnderlyingPrice(${cTokenAddr}) for cToken ${cTokenSymbol} to ${label} UAV at address ${uavContract.address} reverted!` 126 | ); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /test/MockChainlinkOCRAggregator.ts: -------------------------------------------------------------------------------- 1 | import { smock } from "@defi-wonderland/smock"; 2 | import { BaseContract, BigNumberish } from "ethers"; 3 | import { UniswapAnchoredView, UniswapAnchoredView__factory } from "../types"; 4 | 5 | export interface MockChainlinkOCRAggregator extends BaseContract { 6 | functions: { 7 | setUniswapAnchoredView: (addr: string) => Promise; 8 | validate: (price: BigNumberish) => Promise; 9 | }; 10 | } 11 | 12 | export async function createMockReporter() { 13 | let uniswapAnchoredView: UniswapAnchoredView; 14 | 15 | /// Mock reporter that validates against a UAV 16 | const reporter = await smock.fake([ 17 | { 18 | inputs: [ 19 | { 20 | internalType: "int256", 21 | name: "price", 22 | type: "int256", 23 | }, 24 | ], 25 | name: "validate", 26 | stateMutability: "nonpayable", 27 | type: "function", 28 | }, 29 | { 30 | inputs: [ 31 | { 32 | internalType: "address", 33 | name: "addr", 34 | type: "address", 35 | }, 36 | ], 37 | name: "setUniswapAnchoredView", 38 | stateMutability: "nonpayable", 39 | type: "function", 40 | }, 41 | ]); 42 | reporter.setUniswapAnchoredView.returns((addr: string) => { 43 | uniswapAnchoredView = new UniswapAnchoredView__factory().attach(addr); 44 | }); 45 | reporter.validate.returns(async (currentPrice: number) => { 46 | await uniswapAnchoredView.validate(0, 0, 0, currentPrice); 47 | }); 48 | 49 | return reporter; 50 | } 51 | -------------------------------------------------------------------------------- /test/NonReporterPrices.test.ts: -------------------------------------------------------------------------------- 1 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; 2 | import "@nomiclabs/hardhat-waffle"; 3 | import { expect, use } from "chai"; 4 | import { ethers } from "hardhat"; 5 | import { 6 | MockChainlinkOCRAggregator__factory, 7 | UniswapAnchoredView__factory, 8 | } from "../types"; 9 | import { smock } from "@defi-wonderland/smock"; 10 | import * as UniswapV3Pool from "@uniswap/v3-core/artifacts/contracts/UniswapV3Pool.sol/UniswapV3Pool.json"; 11 | import { address, uint, keccak256 } from "./utils"; 12 | import { getTokenAddresses } from "./utils/cTokenAddresses"; 13 | 14 | // Chai matchers for mocked contracts 15 | use(smock.matchers); 16 | 17 | // BigNumber type helpers 18 | export const BigNumber = ethers.BigNumber; 19 | export type BigNumber = ReturnType; 20 | 21 | const PriceSource = { 22 | FIXED_ETH: 0, 23 | FIXED_USD: 1, 24 | REPORTER: 2, 25 | }; 26 | 27 | describe("UniswapAnchoredView", () => { 28 | let signers: SignerWithAddress[]; 29 | let deployer: SignerWithAddress; 30 | beforeEach(async () => { 31 | signers = await ethers.getSigners(); 32 | deployer = signers[0]; 33 | }); 34 | 35 | const cTokenAddresses = getTokenAddresses(["USDC", "USDT"]); 36 | 37 | it("handles fixed_usd prices", async () => { 38 | const USDC = { 39 | cToken: cTokenAddresses["USDC"].cToken, 40 | underlying: cTokenAddresses["USDC"].underlying, 41 | symbolHash: keccak256("USDC"), 42 | baseUnit: uint(1e6), 43 | priceSource: PriceSource.FIXED_USD, 44 | fixedPrice: uint(1e6), 45 | uniswapMarket: address(0), 46 | reporter: address(0), 47 | reporterMultiplier: uint(1e16), 48 | isUniswapReversed: false, 49 | }; 50 | const USDT = { 51 | cToken: cTokenAddresses["USDT"].cToken, 52 | underlying: cTokenAddresses["USDT"].underlying, 53 | symbolHash: keccak256("USDT"), 54 | baseUnit: uint(1e6), 55 | priceSource: PriceSource.FIXED_USD, 56 | fixedPrice: uint(1e6), 57 | uniswapMarket: address(0), 58 | reporter: address(0), 59 | reporterMultiplier: uint(1e16), 60 | isUniswapReversed: false, 61 | }; 62 | const oracle = await new UniswapAnchoredView__factory(deployer).deploy( 63 | 0, 64 | 60, 65 | [USDC, USDT] 66 | ); 67 | expect(await oracle.price("USDC")).to.equal(uint(1e6)); 68 | expect(await oracle.price("USDT")).to.equal(uint(1e6)); 69 | }); 70 | 71 | it("reverts fixed_eth prices if no ETH price", async () => { 72 | const SAI = { 73 | cToken: address(5), 74 | underlying: address(6), 75 | symbolHash: keccak256("SAI"), 76 | baseUnit: uint(1e18), 77 | priceSource: PriceSource.FIXED_ETH, 78 | fixedPrice: uint(5285551943761727), 79 | uniswapMarket: address(0), 80 | reporter: address(0), 81 | reporterMultiplier: uint(1e16), 82 | isUniswapReversed: false, 83 | }; 84 | const oracle = await new UniswapAnchoredView__factory(deployer).deploy( 85 | 0, 86 | 60, 87 | [SAI] 88 | ); 89 | expect(oracle.price("SAI")).to.be.revertedWith("ETH price not set"); 90 | }); 91 | 92 | it("reverts if ETH has no uniswap market", async () => { 93 | // if (!coverage) { 94 | // This test for some reason is breaking coverage in CI, skip for now 95 | const ETH = { 96 | cToken: address(5), 97 | underlying: address(6), 98 | symbolHash: keccak256("ETH"), 99 | baseUnit: uint(1e18), 100 | priceSource: PriceSource.REPORTER, 101 | fixedPrice: 0, 102 | uniswapMarket: address(0), 103 | reporter: address(1), 104 | reporterMultiplier: uint(1e16), 105 | isUniswapReversed: true, 106 | }; 107 | const SAI = { 108 | cToken: address(5), 109 | underlying: address(6), 110 | symbolHash: keccak256("SAI"), 111 | baseUnit: uint(1e18), 112 | priceSource: PriceSource.FIXED_ETH, 113 | fixedPrice: uint(5285551943761727), 114 | uniswapMarket: address(0), 115 | reporter: address(0), 116 | reporterMultiplier: uint(1e16), 117 | isUniswapReversed: false, 118 | }; 119 | expect( 120 | new UniswapAnchoredView__factory(deployer).deploy(0, 60, [ETH, SAI]) 121 | ).to.be.revertedWith("No anchor"); 122 | // } 123 | }); 124 | 125 | it("reverts if non-reporter has a uniswap market", async () => { 126 | // if (!coverage) { 127 | const ETH = { 128 | cToken: address(5), 129 | underlying: address(6), 130 | symbolHash: keccak256("ETH"), 131 | baseUnit: uint(1e18), 132 | priceSource: PriceSource.FIXED_ETH, 133 | fixedPrice: 14, 134 | uniswapMarket: address(112), 135 | reporter: address(0), 136 | reporterMultiplier: uint(1e16), 137 | isUniswapReversed: true, 138 | }; 139 | const SAI = { 140 | cToken: address(5), 141 | underlying: address(6), 142 | symbolHash: keccak256("SAI"), 143 | baseUnit: uint(1e18), 144 | priceSource: PriceSource.FIXED_ETH, 145 | fixedPrice: uint(5285551943761727), 146 | uniswapMarket: address(0), 147 | reporter: address(0), 148 | reporterMultiplier: uint(1e16), 149 | isUniswapReversed: false, 150 | }; 151 | expect( 152 | new UniswapAnchoredView__factory(deployer).deploy(0, 60, [ETH, SAI]) 153 | ).to.be.revertedWith("Doesnt need anchor"); 154 | // } 155 | }); 156 | 157 | it("handles fixed_eth prices", async () => { 158 | // if (!coverage) { 159 | // Get USDC/ETH V3 pool (highest liquidity/highest fee) from mainnet 160 | const usdc_eth_pair = await ethers.getContractAt( 161 | UniswapV3Pool.abi, 162 | "0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8" 163 | ); 164 | const ans = await usdc_eth_pair.functions.observe([3600, 0]); 165 | const tickCumulatives: BigNumber[] = ans[0]; 166 | 167 | const timeWeightedAverageTick = tickCumulatives[1] 168 | .sub(tickCumulatives[0]) 169 | .div("3600"); 170 | const inverseTwap = 1.0001 ** timeWeightedAverageTick.toNumber(); // USDC/ETH 171 | // console.log(`${inverseTwap} <- inverse TWAP (USDC/ETH) sanity check`); 172 | // ETH has 1e18 precision, USDC has 1e6 precision 173 | const twap = 1e18 / (1e6 * inverseTwap); // ETH/USDC 174 | // console.log(`${twap} <- TWAP (ETH/USDC) sanity check`); 175 | // Sanity check ~ USDC/ETH mid price at block 13152450 176 | expect(3957.1861616593173 - twap).to.be.lessThanOrEqual(Number.EPSILON); 177 | 178 | const reporter = await new MockChainlinkOCRAggregator__factory( 179 | deployer 180 | ).deploy(); 181 | 182 | const ETH = { 183 | cToken: address(5), 184 | underlying: address(6), 185 | symbolHash: keccak256("ETH"), 186 | baseUnit: uint(1e18), 187 | priceSource: PriceSource.REPORTER, 188 | fixedPrice: 0, 189 | uniswapMarket: usdc_eth_pair.address, 190 | reporter: reporter.address, 191 | reporterMultiplier: uint(1e16), 192 | isUniswapReversed: true, 193 | }; 194 | const SAI = { 195 | cToken: address(7), 196 | underlying: address(8), 197 | symbolHash: keccak256("SAI"), 198 | baseUnit: uint(1e18), 199 | priceSource: PriceSource.FIXED_ETH, 200 | fixedPrice: uint(5285551943761727), 201 | uniswapMarket: address(0), 202 | reporter: address(0), 203 | reporterMultiplier: uint(1e16), 204 | isUniswapReversed: false, 205 | }; 206 | const oracle = await new UniswapAnchoredView__factory(deployer).deploy( 207 | uint(20e16), 208 | 60, 209 | [ETH, SAI] 210 | ); 211 | await reporter.setUniswapAnchoredView(oracle.address); 212 | 213 | await ethers.provider.send("evm_increaseTime", [30 * 60]); 214 | await ethers.provider.send("evm_mine", []); 215 | 216 | // 8 decimals posted 217 | const ethPrice = 395718616161; 218 | // 6 decimals stored 219 | const expectedEthPrice = BigNumber.from(ethPrice).div(100); // enforce int division 220 | // Feed price via mock reporter -> UAV 221 | await reporter.validate(ethPrice); 222 | // console.log( 223 | // await oracle.queryFilter(oracle.filters.PriceUpdated(null, null)) 224 | // ); 225 | // const priceGuards = await oracle.queryFilter( 226 | // oracle.filters.PriceGuarded(null, null, null) 227 | // ); 228 | // const priceGuarded = priceGuards[0]; 229 | // console.log('Guarded: reported -> ' + (priceGuarded.args[1] as BigNumber).toString()); 230 | // console.log('Guarded: anchored -> ' + (priceGuarded.args[2] as BigNumber).toString()); 231 | expect(await oracle.price("ETH")).to.equal(expectedEthPrice.toNumber()); 232 | expect(await oracle.price("SAI")).to.equal(20915913); 233 | // } 234 | }); 235 | }); 236 | -------------------------------------------------------------------------------- /test/PostRealWorldPrices.test.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import { Contract } from "ethers"; 3 | import { expect } from "chai"; 4 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; 5 | import * as UniswapV3Pool from "@uniswap/v3-core/artifacts/contracts/UniswapV3Pool.sol/UniswapV3Pool.json"; 6 | import { 7 | MockChainlinkOCRAggregator__factory, 8 | MockChainlinkOCRAggregator, 9 | UniswapAnchoredView__factory, 10 | UniswapAnchoredView, 11 | UniswapV3SwapHelper__factory, 12 | UniswapV3SwapHelper, 13 | } from "../types"; 14 | import { WETH9 } from "typechain-common-abi/types/contract-types/ethers"; 15 | import { uint, keccak256, getWeth9, resetFork } from "./utils"; 16 | import { getTokenAddresses } from "./utils/cTokenAddresses"; 17 | 18 | const BigNumber = ethers.BigNumber; 19 | type BigNumber = ReturnType; 20 | 21 | // @notice UniswapAnchoredView `validate` test 22 | // Based on price data from Coingecko and Uniswap token pairs 23 | // at block 13152450 (2021-09-03 11:23:34 UTC) 24 | interface TestTokenPair { 25 | pair: Contract; 26 | reporter: MockChainlinkOCRAggregator; 27 | } 28 | 29 | type TestTokenPairs = { 30 | [key: string]: TestTokenPair; 31 | }; 32 | 33 | async function setupTokenPairs( 34 | deployer: SignerWithAddress 35 | ): Promise { 36 | // Reversed market for ETH, read value of ETH in USDC 37 | // USDC/ETH V3 pool (highest liquidity/highest fee) from mainnet 38 | const usdc_eth_pair = await ethers.getContractAt( 39 | UniswapV3Pool.abi, 40 | "0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8" 41 | ); 42 | const usdc_reporter = await new MockChainlinkOCRAggregator__factory( 43 | deployer 44 | ).deploy(); 45 | 46 | // DAI/ETH V3 pool from mainnet 47 | const dai_eth_pair = await ethers.getContractAt( 48 | UniswapV3Pool.abi, 49 | "0xc2e9f25be6257c210d7adf0d4cd6e3e881ba25f8" 50 | ); 51 | const dai_reporter = await new MockChainlinkOCRAggregator__factory( 52 | deployer 53 | ).deploy(); 54 | 55 | // REP V3 pool from mainnet 56 | const rep_eth_pair = await ethers.getContractAt( 57 | UniswapV3Pool.abi, 58 | "0xb055103b7633b61518cd806d95beeb2d4cd217e7" 59 | ); 60 | const rep_reporter = await new MockChainlinkOCRAggregator__factory( 61 | deployer 62 | ).deploy(); 63 | 64 | // Initialize BAT pair with values from mainnet 65 | const bat_eth_pair = await ethers.getContractAt( 66 | UniswapV3Pool.abi, 67 | "0xAE614a7a56cB79c04Df2aeBA6f5dAB80A39CA78E" 68 | ); 69 | const bat_reporter = await new MockChainlinkOCRAggregator__factory( 70 | deployer 71 | ).deploy(); 72 | 73 | // Initialize ZRX pair with values from mainnet 74 | // Reversed market 75 | const eth_zrx_pair = await ethers.getContractAt( 76 | UniswapV3Pool.abi, 77 | "0x14424eeecbff345b38187d0b8b749e56faa68539" 78 | ); 79 | const zrx_reporter = await new MockChainlinkOCRAggregator__factory( 80 | deployer 81 | ).deploy(); 82 | 83 | // WBTC/ETH (0.3%) V3 pool from mainnet 84 | const wbtc_eth_pair = await ethers.getContractAt( 85 | UniswapV3Pool.abi, 86 | "0xcbcdf9626bc03e24f779434178a73a0b4bad62ed" 87 | ); 88 | const wbtc_reporter = await new MockChainlinkOCRAggregator__factory( 89 | deployer 90 | ).deploy(); 91 | 92 | // Initialize COMP pair with values from mainnet 93 | const comp_eth_pair = await ethers.getContractAt( 94 | UniswapV3Pool.abi, 95 | "0xea4ba4ce14fdd287f380b55419b1c5b6c3f22ab6" 96 | ); 97 | const comp_reporter = await new MockChainlinkOCRAggregator__factory( 98 | deployer 99 | ).deploy(); 100 | 101 | // Initialize LINK pair with values from mainnet 102 | const link_eth_pair = await ethers.getContractAt( 103 | UniswapV3Pool.abi, 104 | "0xa6cc3c2531fdaa6ae1a3ca84c2855806728693e8" 105 | ); 106 | const link_reporter = await new MockChainlinkOCRAggregator__factory( 107 | deployer 108 | ).deploy(); 109 | 110 | return { 111 | ETH: { 112 | pair: usdc_eth_pair, 113 | reporter: usdc_reporter, 114 | }, 115 | DAI: { 116 | pair: dai_eth_pair, 117 | reporter: dai_reporter, 118 | }, 119 | REP: { 120 | pair: rep_eth_pair, 121 | reporter: rep_reporter, 122 | }, 123 | BAT: { 124 | pair: bat_eth_pair, 125 | reporter: bat_reporter, 126 | }, 127 | ZRX: { 128 | pair: eth_zrx_pair, 129 | reporter: zrx_reporter, 130 | }, 131 | BTC: { 132 | pair: wbtc_eth_pair, 133 | reporter: wbtc_reporter, 134 | }, 135 | COMP: { 136 | pair: comp_eth_pair, 137 | reporter: comp_reporter, 138 | }, 139 | LINK: { 140 | pair: link_eth_pair, 141 | reporter: link_reporter, 142 | }, 143 | }; 144 | } 145 | 146 | async function setupUniswapAnchoredView( 147 | pairs: TestTokenPairs, 148 | deployer: SignerWithAddress 149 | ) { 150 | const PriceSource = { 151 | FIXED_ETH: 0, 152 | FIXED_USD: 1, 153 | REPORTER: 2, 154 | }; 155 | 156 | const anchorMantissa = BigNumber.from("10").pow("17"); //1e17 equates to 10% tolerance for source price to be above or below anchor 157 | const anchorPeriod = 30 * 60; 158 | 159 | const cTokenAddresses = getTokenAddresses([ 160 | "ETH", 161 | "DAI", 162 | "REP", 163 | "BAT", 164 | "ZRX", 165 | "BTC", 166 | "COMP", 167 | "LINK", 168 | ]); 169 | 170 | const tokenConfigs = [ 171 | { 172 | cToken: cTokenAddresses["ETH"].cToken, 173 | underlying: cTokenAddresses["ETH"].underlying, 174 | symbolHash: keccak256("ETH"), 175 | baseUnit: uint(1e18), 176 | priceSource: PriceSource.REPORTER, 177 | fixedPrice: 0, 178 | uniswapMarket: pairs.ETH.pair.address, 179 | reporter: pairs.ETH.reporter.address, 180 | reporterMultiplier: uint(1e16), 181 | isUniswapReversed: true, 182 | }, 183 | { 184 | cToken: cTokenAddresses["DAI"].cToken, 185 | underlying: cTokenAddresses["DAI"].underlying, 186 | symbolHash: keccak256("DAI"), 187 | baseUnit: uint(1e18), 188 | priceSource: PriceSource.REPORTER, 189 | fixedPrice: 0, 190 | uniswapMarket: pairs.DAI.pair.address, 191 | reporter: pairs.DAI.reporter.address, 192 | reporterMultiplier: uint(1e16), 193 | isUniswapReversed: false, 194 | }, 195 | { 196 | cToken: cTokenAddresses["REP"].cToken, 197 | underlying: cTokenAddresses["REP"].underlying, 198 | symbolHash: keccak256("REP"), 199 | baseUnit: uint(1e18), 200 | priceSource: PriceSource.REPORTER, 201 | fixedPrice: 0, 202 | uniswapMarket: pairs.REP.pair.address, 203 | reporter: pairs.REP.reporter.address, 204 | reporterMultiplier: uint(1e16), 205 | isUniswapReversed: false, 206 | }, 207 | { 208 | cToken: cTokenAddresses["BAT"].cToken, 209 | underlying: cTokenAddresses["BAT"].underlying, 210 | symbolHash: keccak256("BAT"), 211 | baseUnit: uint(1e18), 212 | priceSource: PriceSource.REPORTER, 213 | fixedPrice: 0, 214 | uniswapMarket: pairs.BAT.pair.address, 215 | reporter: pairs.BAT.reporter.address, 216 | reporterMultiplier: uint(1e16), 217 | isUniswapReversed: false, 218 | }, 219 | { 220 | cToken: cTokenAddresses["ZRX"].cToken, 221 | underlying: cTokenAddresses["ZRX"].underlying, 222 | symbolHash: keccak256("ZRX"), 223 | baseUnit: uint(1e18), 224 | priceSource: PriceSource.REPORTER, 225 | fixedPrice: 0, 226 | uniswapMarket: pairs.ZRX.pair.address, 227 | reporter: pairs.ZRX.reporter.address, 228 | reporterMultiplier: uint(1e16), 229 | isUniswapReversed: true, 230 | }, 231 | { 232 | cToken: cTokenAddresses["BTC"].cToken, 233 | underlying: cTokenAddresses["BTC"].underlying, 234 | symbolHash: keccak256("BTC"), 235 | baseUnit: uint(1e8), 236 | priceSource: PriceSource.REPORTER, 237 | fixedPrice: 0, 238 | uniswapMarket: pairs.BTC.pair.address, 239 | reporter: pairs.BTC.reporter.address, 240 | reporterMultiplier: uint(1e6), 241 | isUniswapReversed: false, 242 | }, 243 | { 244 | cToken: cTokenAddresses["COMP"].cToken, 245 | underlying: cTokenAddresses["COMP"].underlying, 246 | symbolHash: keccak256("COMP"), 247 | baseUnit: uint(1e18), 248 | priceSource: PriceSource.REPORTER, 249 | fixedPrice: 0, 250 | uniswapMarket: pairs.COMP.pair.address, 251 | reporter: pairs.COMP.reporter.address, 252 | reporterMultiplier: uint(1e16), 253 | isUniswapReversed: false, 254 | }, 255 | { 256 | cToken: cTokenAddresses["LINK"].cToken, 257 | underlying: cTokenAddresses["LINK"].underlying, 258 | symbolHash: keccak256("LINK"), 259 | baseUnit: uint(1e18), 260 | priceSource: PriceSource.REPORTER, 261 | fixedPrice: 0, 262 | uniswapMarket: pairs.LINK.pair.address, 263 | reporter: pairs.LINK.reporter.address, 264 | reporterMultiplier: uint(1e16), 265 | isUniswapReversed: false, 266 | }, 267 | ]; 268 | 269 | return new UniswapAnchoredView__factory(deployer).deploy( 270 | anchorMantissa, 271 | anchorPeriod, 272 | tokenConfigs 273 | ); 274 | } 275 | 276 | async function configureReporters( 277 | uniswapAnchoredViewAddress: string, 278 | pairs: TestTokenPairs 279 | ) { 280 | for (const key of Object.keys(pairs)) { 281 | await pairs[key].reporter.setUniswapAnchoredView( 282 | uniswapAnchoredViewAddress 283 | ); 284 | } 285 | } 286 | 287 | async function setup(deployer: SignerWithAddress) { 288 | const pairs = await setupTokenPairs(deployer); 289 | const uniswapAnchoredView = await setupUniswapAnchoredView(pairs, deployer); 290 | const uniswapV3SwapHelper_ = await new UniswapV3SwapHelper__factory( 291 | deployer 292 | ).deploy(); 293 | const uniswapV3SwapHelper = await uniswapV3SwapHelper_.connect(deployer); 294 | const weth9 = await getWeth9(deployer); 295 | 296 | return { 297 | uniswapAnchoredView, 298 | pairs, 299 | uniswapV3SwapHelper, 300 | weth9, 301 | }; 302 | } 303 | 304 | describe("UniswapAnchoredView", () => { 305 | // No data for COMP from Coinbase so far, it is not added to the oracle yet 306 | const prices: Array<[string, BigNumber]> = [ 307 | ["BTC", BigNumber.from("49338784652")], 308 | ["ETH", BigNumber.from("3793300743")], 309 | ["DAI", BigNumber.from("999851")], 310 | ["REP", BigNumber.from("28877036")], 311 | ["ZRX", BigNumber.from("1119738")], 312 | ["BAT", BigNumber.from("851261")], 313 | ["LINK", BigNumber.from("30058454")], 314 | ]; 315 | let deployer: SignerWithAddress; 316 | let uniswapAnchoredView: UniswapAnchoredView; 317 | let pairs: TestTokenPairs; 318 | let uniswapV3SwapHelper: UniswapV3SwapHelper; 319 | let weth9: WETH9; 320 | beforeEach(async () => { 321 | await resetFork(); 322 | 323 | const signers = await ethers.getSigners(); 324 | deployer = signers[0]; 325 | ({ uniswapAnchoredView, pairs, uniswapV3SwapHelper, weth9 } = await setup( 326 | deployer 327 | )); 328 | }); 329 | 330 | it("basic scenario, use real world data", async () => { 331 | await configureReporters(uniswapAnchoredView.address, pairs); 332 | 333 | for (let i = 0; i < prices.length; i++) { 334 | const element = prices[i]; 335 | const reporter = pairs[element[0]].reporter; 336 | // *100 to conform to 8 decimals 337 | await reporter.validate(element[1].mul("100")); 338 | const updatedPrice = await uniswapAnchoredView.price(element[0]); 339 | expect(updatedPrice).to.equal(prices[i][1].toString()); 340 | } 341 | }); 342 | 343 | // TODO: PriceGuarded tests?! 344 | it("test price events - PriceUpdated", async () => { 345 | await configureReporters(uniswapAnchoredView.address, pairs); 346 | 347 | for (let i = 0; i < prices.length; i++) { 348 | const element = prices[i]; 349 | const reporter = pairs[element[0]].reporter; 350 | // *100 to conform to 8 decimals 351 | const validateTx = await reporter.validate(element[1].mul("100")); 352 | const events = await uniswapAnchoredView.queryFilter( 353 | uniswapAnchoredView.filters.PriceUpdated(keccak256(prices[i][0])), 354 | validateTx.blockNumber 355 | ); 356 | expect(events.length).to.equal(1); 357 | // Price was updated 358 | expect(events[0].args.price).to.equal(prices[i][1]); 359 | } 360 | }); 361 | 362 | it("test ETH (USDC/ETH) pair while token reserves change", async () => { 363 | await configureReporters(uniswapAnchoredView.address, pairs); 364 | // Report new price so the UAV TWAP is initialised, and confirm it 365 | await pairs.ETH.reporter.validate(3950e8); 366 | const ethAnchorInitial = await uniswapAnchoredView.price("ETH"); 367 | expect(ethAnchorInitial).to.equal(3950e6); 368 | 369 | // Record the ETH mid-price from the pool 370 | const ethPriceInitial = Math.ceil( 371 | 1e12 * 1.0001 ** -(await pairs.ETH.pair.slot0()).tick 372 | ); 373 | expect(ethPriceInitial).to.equal(3951); 374 | 375 | const ethToSell = BigNumber.from("20000").mul(String(1e18)); 376 | // Wrap ETH, unlimited allowance to UniswapV3SwapHelper 377 | await weth9.deposit({ value: ethToSell }); 378 | await weth9.approve( 379 | uniswapV3SwapHelper.address, 380 | BigNumber.from("2").pow("256").sub("1") 381 | ); 382 | // Simulate a swap using the helper 383 | await uniswapV3SwapHelper.performSwap( 384 | pairs.ETH.pair.address, 385 | false, // zeroForOne: false -> swap token1 (ETH) for token0 (USDC) 386 | ethToSell, 387 | BigNumber.from("1461446703485210103287273052203988822378723970342").sub( 388 | "1" 389 | ) // (MAX_SQRT_RATIO - 1) -> "no price limit" 390 | ); 391 | 392 | // Check that mid-price on the V3 pool has dumped 393 | const ethPriceAfter = Math.ceil( 394 | 1e12 * 1.0001 ** -(await pairs.ETH.pair.slot0()).tick 395 | ); 396 | expect(1 - ethPriceAfter / ethPriceInitial).to.be.greaterThan(0.1); 397 | // TWAP should not be severely affected 398 | // so try to report a price that's within anchor tolerance of TWAP. 399 | // UAV should emit PriceUpdated accordingly. 400 | const tx = await pairs.ETH.reporter.validate(3960e8); 401 | const emittedEvents = await uniswapAnchoredView.queryFilter( 402 | uniswapAnchoredView.filters.PriceUpdated(null, null), 403 | tx.blockNumber 404 | ); 405 | expect(emittedEvents.length).to.equal(1); 406 | expect(emittedEvents[0].args.symbolHash).to.equal(keccak256("ETH")); 407 | expect(emittedEvents[0].args.price).to.equal(3960e6); 408 | }); 409 | }); 410 | -------------------------------------------------------------------------------- /test/PriceOracleConfig.test.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import { expect, use } from "chai"; 3 | import { PriceOracle__factory } from "../types"; 4 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; 5 | import { smock } from "@defi-wonderland/smock"; 6 | import { TokenConfig } from "../configuration/parameters-price-oracle"; 7 | import { resetFork } from "./utils"; 8 | import { deployMockContract } from "ethereum-waffle"; 9 | import { mockAggregatorAbi } from "./PriceOracle.test"; 10 | import { BigNumber } from "ethers"; 11 | 12 | use(smock.matchers); 13 | 14 | const zeroAddress = "0x0000000000000000000000000000000000000000"; 15 | 16 | describe("PriceOracle", () => { 17 | let signers: SignerWithAddress[]; 18 | let deployer: SignerWithAddress; 19 | 20 | before(async () => { 21 | signers = await ethers.getSigners(); 22 | deployer = signers[0]; 23 | }); 24 | 25 | describe("constructor", () => { 26 | beforeEach(async () => { 27 | await resetFork(); 28 | }); 29 | 30 | it("succeeds", async () => { 31 | const configs: TokenConfig[] = [ 32 | // Price feed config 33 | { 34 | cToken: "0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5", 35 | underlyingAssetDecimals: "18", 36 | priceFeed: "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419", 37 | fixedPrice: "0", 38 | }, 39 | { 40 | cToken: "0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643", 41 | underlyingAssetDecimals: "18", 42 | priceFeed: "0xAed0c38402a5d19df6E4c03F4E2DceD6e29c1ee9", 43 | fixedPrice: "0", 44 | }, 45 | // Fixed price config 46 | { 47 | cToken: "0xF5DCe57282A584D2746FaF1593d3121Fcac444dC", 48 | underlyingAssetDecimals: "18", 49 | priceFeed: zeroAddress, 50 | fixedPrice: "15544520000000000000", 51 | }, 52 | // 0 underlying decimal 53 | { 54 | cToken: "0xccf4429db6322d5c611ee964527d42e5d685dd6a", 55 | underlyingAssetDecimals: "0", 56 | priceFeed: "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419", 57 | fixedPrice: "0", 58 | }, 59 | ]; 60 | const priceOracle = await new PriceOracle__factory(deployer).deploy( 61 | configs 62 | ); 63 | // Validate config 1 64 | const config1 = configs[0]; 65 | const returnedConfig1 = await priceOracle.getConfig(config1.cToken); 66 | expect(returnedConfig1.underlyingAssetDecimals).to.equal( 67 | Number(config1.underlyingAssetDecimals) 68 | ); 69 | expect(returnedConfig1.priceFeed).to.equal(config1.priceFeed); 70 | expect(returnedConfig1.fixedPrice).to.equal(0); 71 | 72 | // Validate config 2 73 | const config2 = configs[1]; 74 | const returnedConfig2 = await priceOracle.getConfig(config2.cToken); 75 | expect(returnedConfig2.underlyingAssetDecimals).to.equal( 76 | Number(config2.underlyingAssetDecimals) 77 | ); 78 | expect(returnedConfig2.priceFeed).to.equal(config2.priceFeed); 79 | expect(returnedConfig2.fixedPrice).to.equal(0); 80 | 81 | // Validate config 3 82 | const config3 = configs[2]; 83 | const returnedConfig3 = await priceOracle.getConfig(config3.cToken); 84 | expect(returnedConfig3.underlyingAssetDecimals).to.equal( 85 | Number(config3.underlyingAssetDecimals) 86 | ); 87 | expect(returnedConfig3.priceFeed).to.equal(config3.priceFeed); 88 | expect(returnedConfig3.fixedPrice).to.equal( 89 | BigNumber.from(config3.fixedPrice) 90 | ); 91 | 92 | // Validate config 4 93 | const config4 = configs[3]; 94 | const returnedConfig4 = await priceOracle.getConfig(config4.cToken); 95 | expect(returnedConfig4.underlyingAssetDecimals).to.equal( 96 | Number(config4.underlyingAssetDecimals) 97 | ); 98 | expect(returnedConfig4.priceFeed).to.equal(config4.priceFeed); 99 | expect(returnedConfig4.fixedPrice).to.equal(0); 100 | 101 | const invalidCToken = "0x39AA39c021dfbaE8faC545936693aC917d5E7563"; 102 | expect(priceOracle.getConfig(invalidCToken)).to.be.revertedWith( 103 | "ConfigNotFound" 104 | ); 105 | }); 106 | it("reverts if underlyingAssetDecimals is too high", async () => { 107 | const invalidConfigs: TokenConfig[] = [ 108 | { 109 | cToken: "0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5", 110 | underlyingAssetDecimals: "31", 111 | priceFeed: "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419", 112 | fixedPrice: "0", 113 | }, 114 | ]; 115 | await expect( 116 | new PriceOracle__factory(deployer).deploy(invalidConfigs) 117 | ).to.be.revertedWith("InvalidUnderlyingAssetDecimals"); 118 | }); 119 | it("reverts if repeating configs", async () => { 120 | const repeatConfigs: TokenConfig[] = [ 121 | { 122 | cToken: "0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5", 123 | underlyingAssetDecimals: "6", 124 | priceFeed: zeroAddress, 125 | fixedPrice: "1000000000000000000", 126 | }, 127 | { 128 | cToken: "0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643", 129 | underlyingAssetDecimals: "18", 130 | priceFeed: "0xAed0c38402a5d19df6E4c03F4E2DceD6e29c1ee9", 131 | fixedPrice: "0", 132 | }, 133 | { 134 | cToken: "0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5", 135 | underlyingAssetDecimals: "18", 136 | priceFeed: "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419", 137 | fixedPrice: "0", 138 | }, 139 | { 140 | cToken: "0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643", 141 | underlyingAssetDecimals: "6", 142 | priceFeed: zeroAddress, 143 | fixedPrice: "1000000000000000000", 144 | }, 145 | ]; 146 | await expect( 147 | new PriceOracle__factory(deployer).deploy(repeatConfigs) 148 | ).to.be.revertedWith("DuplicateConfig"); 149 | }); 150 | it("reverts if missing cToken", async () => { 151 | const invalidConfigs: TokenConfig[] = [ 152 | { 153 | cToken: zeroAddress, 154 | underlyingAssetDecimals: "18", 155 | priceFeed: "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419", 156 | fixedPrice: "0", 157 | }, 158 | ]; 159 | await expect( 160 | new PriceOracle__factory(deployer).deploy(invalidConfigs) 161 | ).to.be.revertedWith("MissingCTokenAddress"); 162 | }); 163 | it("reverts if feed decimals are too high", async () => { 164 | const mockedEthAggregator = await deployMockContract( 165 | deployer, 166 | mockAggregatorAbi 167 | ); 168 | await mockedEthAggregator.mock.decimals.returns(75); 169 | const invalidConfigs: TokenConfig[] = [ 170 | { 171 | cToken: "0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5", 172 | underlyingAssetDecimals: "18", 173 | priceFeed: mockedEthAggregator.address, 174 | fixedPrice: "0", 175 | }, 176 | ]; 177 | await expect( 178 | new PriceOracle__factory(deployer).deploy(invalidConfigs) 179 | ).to.be.revertedWith("FormattingDecimalsTooHigh"); 180 | }); 181 | it("reverts if missing both priceFeed and fixedPrice", async () => { 182 | const invalidConfigs: TokenConfig[] = [ 183 | { 184 | cToken: "0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5", 185 | underlyingAssetDecimals: "18", 186 | priceFeed: zeroAddress, 187 | fixedPrice: "0", 188 | }, 189 | ]; 190 | await expect( 191 | new PriceOracle__factory(deployer).deploy(invalidConfigs) 192 | ).to.be.revertedWith("InvalidPriceConfigs"); 193 | }); 194 | it("reverts if both priceFeed and fixedPrice are set", async () => { 195 | const invalidConfigs: TokenConfig[] = [ 196 | { 197 | cToken: "0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5", 198 | underlyingAssetDecimals: "18", 199 | priceFeed: "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419", 200 | fixedPrice: "1000000000000000000", 201 | }, 202 | ]; 203 | await expect( 204 | new PriceOracle__factory(deployer).deploy(invalidConfigs) 205 | ).to.be.revertedWith("InvalidPriceConfigs"); 206 | }); 207 | }); 208 | }); 209 | -------------------------------------------------------------------------------- /test/UniswapConfig.test.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import { expect, use } from "chai"; 3 | import { UniswapConfig__factory } from "../types"; 4 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; 5 | import { address, keccak256, uint } from "./utils"; 6 | import { smock } from "@defi-wonderland/smock"; 7 | import { getTokenAddresses } from "./utils/cTokenAddresses"; 8 | 9 | use(smock.matchers); 10 | 11 | // Update this to the max number of tokens supported in UniswapConfig.sol 12 | const MAX_TOKENS = 29; 13 | 14 | describe("UniswapConfig", () => { 15 | let signers: SignerWithAddress[]; 16 | let deployer: SignerWithAddress; 17 | 18 | beforeEach(async () => { 19 | signers = await ethers.getSigners(); 20 | deployer = signers[0]; 21 | }); 22 | 23 | describe("constructor", () => { 24 | it("reverts if too many configs", async () => { 25 | const symbols = Array(MAX_TOKENS + 1) 26 | .fill(0) 27 | .map((_, i) => String.fromCharCode("a".charCodeAt(0) + i)); 28 | const configs = symbols.map((symbol, i) => { 29 | return { 30 | cToken: address(i), 31 | underlying: address(i), 32 | symbolHash: keccak256(symbol), 33 | baseUnit: uint(i + 49), 34 | priceSource: 0, 35 | fixedPrice: 1, 36 | uniswapMarket: address(i + 50), 37 | reporter: address(i + 51), 38 | reporterMultiplier: uint(i + 52), 39 | isUniswapReversed: i % 2 == 0, 40 | }; 41 | }); 42 | await expect( 43 | new UniswapConfig__factory(deployer).deploy(configs) 44 | ).to.be.revertedWith("Too many"); 45 | }); 46 | }); 47 | 48 | it("basically works", async () => { 49 | const MockCTokenABI = [ 50 | { 51 | type: "function", 52 | name: "underlying", 53 | stateMutability: "view", 54 | outputs: [{ type: "address", name: "underlying" }], 55 | }, 56 | ]; 57 | const unlistedNorUnderlying = await smock.fake(MockCTokenABI); 58 | const cTokenAddresses = getTokenAddresses(["ETH", "BTC", "REP"]); 59 | const contract = await new UniswapConfig__factory(deployer).deploy([ 60 | { 61 | cToken: cTokenAddresses["ETH"].cToken, 62 | underlying: cTokenAddresses["ETH"].underlying, 63 | symbolHash: keccak256("ETH"), 64 | baseUnit: uint(1e18), 65 | priceSource: 0, 66 | fixedPrice: 0, 67 | uniswapMarket: address(6), 68 | reporter: address(8), 69 | reporterMultiplier: uint(1e16), 70 | isUniswapReversed: false, 71 | }, 72 | { 73 | cToken: cTokenAddresses["BTC"].cToken, 74 | underlying: cTokenAddresses["BTC"].underlying, 75 | symbolHash: keccak256("BTC"), 76 | baseUnit: uint(1e18), 77 | priceSource: 1, 78 | fixedPrice: 1, 79 | uniswapMarket: address(7), 80 | reporter: address(9), 81 | reporterMultiplier: uint(1e16), 82 | isUniswapReversed: true, 83 | }, 84 | { 85 | cToken: cTokenAddresses["REP"].cToken, 86 | underlying: cTokenAddresses["REP"].underlying, 87 | symbolHash: keccak256("REP"), 88 | baseUnit: uint(1e18), 89 | priceSource: 1, 90 | fixedPrice: 1, 91 | uniswapMarket: address(7), 92 | reporter: address(10), 93 | reporterMultiplier: uint(1e16), 94 | isUniswapReversed: true, 95 | }, 96 | ]); 97 | 98 | const cfg0 = await contract.getTokenConfig(0); 99 | const cfg1 = await contract.getTokenConfig(1); 100 | const cfg2 = await contract.getTokenConfig(2); 101 | const cfgETH = await contract.getTokenConfigBySymbol("ETH"); 102 | const cfgBTC = await contract.getTokenConfigBySymbol("BTC"); 103 | const cfgR8 = await contract.getTokenConfigByReporter(address(8)); 104 | const cfgR9 = await contract.getTokenConfigByReporter(address(9)); 105 | const cfgCT0 = await contract.getTokenConfigByUnderlying( 106 | cTokenAddresses["ETH"].underlying 107 | ); 108 | const cfgCT1 = await contract.getTokenConfigByUnderlying( 109 | cTokenAddresses["BTC"].underlying 110 | ); 111 | const cfgU2 = await contract.getTokenConfigByUnderlying( 112 | cTokenAddresses["REP"].underlying 113 | ); 114 | expect(cfg0).to.deep.equal(cfgETH); 115 | expect(cfgETH).to.deep.equal(cfgR8); 116 | expect(cfgR8).to.deep.equal(cfgCT0); 117 | expect(cfg1).to.deep.equal(cfgBTC); 118 | expect(cfgBTC).to.deep.equal(cfgR9); 119 | expect(cfgR9).to.deep.equal(cfgCT1); 120 | expect(cfg0).not.to.deep.equal(cfg1); 121 | expect(cfgU2).to.deep.equal(cfg2); 122 | 123 | await expect(contract.getTokenConfig(3)).to.be.revertedWith("Not found"); 124 | await expect(contract.getTokenConfigBySymbol("COMP")).to.be.revertedWith( 125 | "Not found" 126 | ); 127 | await expect( 128 | contract.getTokenConfigByReporter(address(1)) 129 | ).to.be.revertedWith("Not found"); 130 | await expect( 131 | contract.getTokenConfigByUnderlying(address(1)) 132 | ).to.be.revertedWith("revert"); // not a cToken 133 | await expect( 134 | contract.getTokenConfigByUnderlying(unlistedNorUnderlying.address) 135 | ).to.be.revertedWith("Not found"); 136 | }); 137 | 138 | it("returns configs exactly as specified", async () => { 139 | const symbols = Array(MAX_TOKENS) 140 | .fill(0) 141 | .map((_, i) => String.fromCharCode("a".charCodeAt(0) + i)); 142 | const configs = symbols.map((symbol, i) => { 143 | return { 144 | cToken: address(i), 145 | underlying: address(i), 146 | symbolHash: keccak256(symbol), 147 | baseUnit: uint(i + 49), 148 | priceSource: 0, 149 | fixedPrice: 1, 150 | uniswapMarket: address(i + 50), 151 | reporter: address(i + 51), 152 | reporterMultiplier: uint(i + 52), 153 | isUniswapReversed: i % 2 == 0, 154 | }; 155 | }); 156 | const contract = await new UniswapConfig__factory(deployer).deploy(configs); 157 | 158 | await Promise.all( 159 | configs.map(async (config, i) => { 160 | const cfgByIndex = await contract.getTokenConfig(i); 161 | const cfgBySymbol = await contract.getTokenConfigBySymbol(symbols[i]); 162 | const cfgByCReporter = await contract.getTokenConfigByReporter( 163 | address(i + 51) 164 | ); 165 | const cfgByUnderlying = await contract.getTokenConfigByUnderlying( 166 | address(i) 167 | ); 168 | const cfgTokenConfig = await contract.getTokenConfigByCToken( 169 | config.cToken 170 | ); 171 | const expectedTokenConfig = { 172 | underlying: config.underlying, 173 | symbolHash: config.symbolHash, 174 | baseUnit: config.baseUnit, 175 | priceSource: config.priceSource, 176 | fixedPrice: config.fixedPrice, 177 | uniswapMarket: config.uniswapMarket, 178 | reporter: config.reporter, 179 | reporterMultiplier: config.reporterMultiplier, 180 | isUniswapReversed: config.isUniswapReversed, 181 | }; 182 | expect(cfgTokenConfig).to.deep.equal(cfgTokenConfig); 183 | expect({ 184 | underlying: cfgByIndex.underlying, 185 | symbolHash: cfgByIndex.symbolHash, 186 | baseUnit: cfgByIndex.baseUnit, 187 | priceSource: cfgByIndex.priceSource, 188 | fixedPrice: cfgByIndex.fixedPrice.toNumber(), // enum => uint8 189 | uniswapMarket: cfgByIndex.uniswapMarket, 190 | reporter: cfgByIndex.reporter, 191 | reporterMultiplier: cfgByIndex.reporterMultiplier, 192 | isUniswapReversed: cfgByIndex.isUniswapReversed, 193 | }).to.deep.equal(expectedTokenConfig); 194 | expect(cfgByIndex).to.deep.equal(cfgBySymbol); 195 | expect(cfgBySymbol).to.deep.equal(cfgByCReporter); 196 | expect(cfgByUnderlying).to.deep.equal(cfgBySymbol); 197 | }) 198 | ); 199 | 200 | const nonConfiguredCTokenAddress = 201 | "0x0F9a74715CfDF8Aa6c9896CE40adc05215A3013d"; 202 | await expect( 203 | contract.getTokenConfigByCToken(nonConfiguredCTokenAddress) 204 | ).to.be.revertedWith("Not found"); 205 | }); 206 | 207 | it("checks gas [ @skip-on-coverage ]", async () => { 208 | const configs = Array(MAX_TOKENS) 209 | .fill(0) 210 | .map((_, i) => { 211 | const symbol = String.fromCharCode("a".charCodeAt(0) + i); 212 | return { 213 | cToken: address(i), 214 | underlying: address(i + 1), 215 | symbolHash: keccak256(symbol), 216 | baseUnit: uint(i + 49), 217 | priceSource: 0, 218 | fixedPrice: 1, 219 | uniswapMarket: address(i + 50), 220 | reporter: address(i + 51), 221 | reporterMultiplier: uint(i + 52), 222 | isUniswapReversed: i % 2 == 0, 223 | }; 224 | }); 225 | const contract = await new UniswapConfig__factory(deployer).deploy(configs); 226 | 227 | const cfg9 = await contract.getTokenConfig(9); 228 | const tx9__ = await contract.populateTransaction.getTokenConfig(9); 229 | const tx9_ = await deployer.sendTransaction(tx9__); 230 | const tx9 = await tx9_.wait(); 231 | expect(cfg9.underlying).to.equal(address(10)); 232 | expect(tx9.gasUsed).to.equal(22970); 233 | 234 | const cfg25 = await contract.getTokenConfig(24); 235 | const tx25__ = await contract.populateTransaction.getTokenConfig(24); 236 | const tx25_ = await deployer.sendTransaction(tx25__); 237 | const tx25 = await tx25_.wait(); 238 | expect(cfg25.underlying).to.equal(address(25)); 239 | expect(tx25.gasUsed).to.equal(23360); 240 | 241 | const cfgY = await contract.getTokenConfigBySymbol("y"); 242 | const txY__ = await contract.populateTransaction.getTokenConfigBySymbol( 243 | "y" 244 | ); 245 | const txY_ = await deployer.sendTransaction(txY__); 246 | const txY = await txY_.wait(); 247 | expect(cfgY.underlying).to.equal(address(25)); 248 | expect(txY.gasUsed).to.equal(25252); 249 | 250 | const cfgCT26 = await contract.getTokenConfigByUnderlying(address(25)); 251 | const txCT26__ = 252 | await contract.populateTransaction.getTokenConfigByUnderlying( 253 | address(25) 254 | ); 255 | const txCT26_ = await deployer.sendTransaction(txCT26__); 256 | const txCT26 = await txCT26_.wait(); 257 | expect(cfgCT26.underlying).to.equal(address(25)); 258 | expect(txCT26.gasUsed).to.equal(25348); 259 | 260 | const cfgR26 = await contract.getTokenConfigByReporter(address(24 + 51)); 261 | const txR26__ = await contract.populateTransaction.getTokenConfigByReporter( 262 | address(24 + 51) 263 | ); 264 | const txR26_ = await deployer.sendTransaction(txR26__); 265 | const txR26 = await txR26_.wait(); 266 | expect(cfgR26.underlying).to.equal(address(25)); 267 | expect(txR26.gasUsed).to.equal(25326); 268 | 269 | const cfgU26 = await contract.getTokenConfigByUnderlying(address(25)); 270 | const txU26__ = 271 | await contract.populateTransaction.getTokenConfigByUnderlying( 272 | address(25) 273 | ); 274 | const txU26_ = await deployer.sendTransaction(txU26__); 275 | const txU26 = await txU26_.wait(); 276 | expect(cfgU26.underlying).to.equal(address(25)); 277 | expect(txU26.gasUsed).to.equal(25348); 278 | }); 279 | }); 280 | -------------------------------------------------------------------------------- /test/utils/address.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | 3 | export function address(n: number) { 4 | const hex = `0x${n.toString(16).padStart(40, "0")}`; 5 | return ethers.utils.getAddress(hex); 6 | } 7 | -------------------------------------------------------------------------------- /test/utils/cTokenAddresses.ts: -------------------------------------------------------------------------------- 1 | import { keccak256 } from "./keccak256"; 2 | const parameters = require("../../configuration/parameters"); 3 | 4 | type ParameterConfig = [ 5 | string, 6 | number, 7 | { 8 | // "NAME": "ETH", 9 | cToken: string; 10 | underlying: string; 11 | symbolHash: string; 12 | baseUnit: string; 13 | priceSource: string; 14 | fixedPrice: string; 15 | uniswapMarket: string; 16 | reporter: string; 17 | reporterMultiplier: string; 18 | isUniswapReversed: boolean; 19 | }[] 20 | ]; 21 | 22 | interface TokenAddresses { 23 | [T: string]: { 24 | underlying: string; 25 | cToken: string; 26 | }; 27 | } 28 | 29 | export const getTokenAddresses = (symbols: string[]): TokenAddresses => { 30 | return symbols.reduce((prev, curr) => { 31 | const symbolHash = curr === "WBTC" ? keccak256("BTC") : keccak256(curr); 32 | const [, , tokenParams] = parameters as ParameterConfig; 33 | const token = tokenParams.find((p) => p.symbolHash === symbolHash); 34 | if (!token) 35 | throw new Error(`Token ${curr} is not configured in parameters.js`); 36 | prev[curr] = { 37 | underlying: token.underlying, 38 | cToken: token.cToken, 39 | }; 40 | return prev; 41 | }, {} as TokenAddresses); 42 | }; 43 | -------------------------------------------------------------------------------- /test/utils/erc20.ts: -------------------------------------------------------------------------------- 1 | import { Signer } from "ethers"; 2 | import { EthersContracts } from "typechain-common-abi"; 3 | 4 | export function getErc20ContractAt(address: string, signer: Signer) { 5 | return new EthersContracts.ERC20__factory(signer).attach(address); 6 | } 7 | 8 | // Mainnet WETH 9 | export function getWeth9(signer: Signer) { 10 | return new EthersContracts.WETH9__factory(signer).attach( 11 | "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /test/utils/exp.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from "ethers"; 2 | 3 | export function exp(a: any, b: any) { 4 | return BigNumber.from(a).mul(BigNumber.from("10").pow(b)); 5 | } 6 | -------------------------------------------------------------------------------- /test/utils/expectedPricesToPush.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import { BigNumber } from "ethers"; 3 | 4 | const TARGET_RATIO = ethers.BigNumber.from("1000000000000000000"); 5 | 6 | export const ANCHOR_PRICES = { 7 | [`${ethers.utils.keccak256(ethers.utils.toUtf8Bytes("ETH"))}`]: { 8 | anchor: ethers.BigNumber.from("3950860042"), 9 | multiplier: ethers.BigNumber.from("10000000000000000"), 10 | baseUnit: ethers.BigNumber.from("1000000000000000000"), 11 | }, 12 | [`${ethers.utils.keccak256(ethers.utils.toUtf8Bytes("DAI"))}`]: { 13 | anchor: ethers.BigNumber.from("1002405"), 14 | multiplier: ethers.BigNumber.from("10000000000000000"), 15 | baseUnit: ethers.BigNumber.from("1000000000000000000"), 16 | }, 17 | [`${ethers.utils.keccak256(ethers.utils.toUtf8Bytes("BTC"))}`]: { 18 | anchor: ethers.BigNumber.from("49884198916"), 19 | multiplier: ethers.BigNumber.from("1000000"), 20 | baseUnit: ethers.BigNumber.from("100000000"), 21 | }, 22 | [`${ethers.utils.keccak256(ethers.utils.toUtf8Bytes("BAT"))}`]: { 23 | anchor: ethers.BigNumber.from("879861"), 24 | multiplier: ethers.BigNumber.from("10000000000000000"), 25 | baseUnit: ethers.BigNumber.from("1000000000000000000"), 26 | }, 27 | [`${ethers.utils.keccak256(ethers.utils.toUtf8Bytes("ZRX"))}`]: { 28 | anchor: ethers.BigNumber.from("1156381"), 29 | multiplier: ethers.BigNumber.from("10000000000000000"), 30 | baseUnit: ethers.BigNumber.from("1000000000000000000"), 31 | }, 32 | [`${ethers.utils.keccak256(ethers.utils.toUtf8Bytes("REP"))}`]: { 33 | anchor: ethers.BigNumber.from("29578072"), 34 | multiplier: ethers.BigNumber.from("10000000000000000"), 35 | baseUnit: ethers.BigNumber.from("1000000000000000000"), 36 | }, 37 | [`${ethers.utils.keccak256(ethers.utils.toUtf8Bytes("UNI"))}`]: { 38 | anchor: ethers.BigNumber.from("30094199"), 39 | multiplier: ethers.BigNumber.from("10000000000000000"), 40 | baseUnit: ethers.BigNumber.from("1000000000000000000"), 41 | }, 42 | [`${ethers.utils.keccak256(ethers.utils.toUtf8Bytes("COMP"))}`]: { 43 | anchor: ethers.BigNumber.from("477896498"), 44 | multiplier: ethers.BigNumber.from("10000000000000000"), 45 | baseUnit: ethers.BigNumber.from("1000000000000000000"), 46 | }, 47 | [`${ethers.utils.keccak256(ethers.utils.toUtf8Bytes("LINK"))}`]: { 48 | anchor: ethers.BigNumber.from("31197271"), 49 | multiplier: ethers.BigNumber.from("10000000000000000"), 50 | baseUnit: ethers.BigNumber.from("1000000000000000000"), 51 | }, 52 | [`${ethers.utils.keccak256(ethers.utils.toUtf8Bytes("AAVE"))}`]: { 53 | anchor: ethers.BigNumber.from("403308857"), 54 | multiplier: ethers.BigNumber.from("10000000000000000"), 55 | baseUnit: ethers.BigNumber.from("1000000000000000000"), 56 | }, 57 | [`${ethers.utils.keccak256(ethers.utils.toUtf8Bytes("SUSHI"))}`]: { 58 | anchor: ethers.BigNumber.from("13476839"), 59 | multiplier: ethers.BigNumber.from("10000000000000000"), 60 | baseUnit: ethers.BigNumber.from("1000000000000000000"), 61 | }, 62 | [`${ethers.utils.keccak256(ethers.utils.toUtf8Bytes("SUSHI"))}`]: { 63 | anchor: ethers.BigNumber.from("13476839"), 64 | multiplier: ethers.BigNumber.from("10000000000000000"), 65 | baseUnit: ethers.BigNumber.from("1000000000000000000"), 66 | }, 67 | [`${ethers.utils.keccak256(ethers.utils.toUtf8Bytes("MKR"))}`]: { 68 | anchor: ethers.BigNumber.from("3606500267"), 69 | multiplier: ethers.BigNumber.from("10000000000000000"), 70 | baseUnit: ethers.BigNumber.from("1000000000000000000"), 71 | }, 72 | [`${ethers.utils.keccak256(ethers.utils.toUtf8Bytes("YFI"))}`]: { 73 | anchor: ethers.BigNumber.from("39060773573"), 74 | multiplier: ethers.BigNumber.from("10000000000000000"), 75 | baseUnit: ethers.BigNumber.from("1000000000000000000"), 76 | }, 77 | [`${ethers.utils.keccak256(ethers.utils.toUtf8Bytes("FEI"))}`]: { 78 | anchor: ethers.BigNumber.from("1156034"), 79 | multiplier: ethers.BigNumber.from("10000000000000000"), 80 | baseUnit: ethers.BigNumber.from("1000000000000000000"), 81 | }, 82 | [`${ethers.utils.keccak256(ethers.utils.toUtf8Bytes("MATIC"))}`]: { 83 | anchor: ethers.BigNumber.from("1463577"), 84 | multiplier: ethers.BigNumber.from("10000000000000000"), 85 | baseUnit: ethers.BigNumber.from("1000000000000000000"), 86 | }, 87 | }; 88 | 89 | export const MOCK_REPORTED_PRICES = Object.keys(ANCHOR_PRICES).reduce( 90 | (prev, curr) => { 91 | const { anchor, multiplier, baseUnit } = ANCHOR_PRICES[curr]; 92 | const reportedPriceWithinAnchor = anchor 93 | .mul(ethers.utils.parseEther("1")) 94 | .mul(baseUnit) 95 | .div(TARGET_RATIO) 96 | .div(multiplier); 97 | prev[curr] = reportedPriceWithinAnchor; 98 | return prev; 99 | }, 100 | {} as { [T: string]: BigNumber } 101 | ); 102 | -------------------------------------------------------------------------------- /test/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./address"; 2 | export * from "./erc20"; 3 | export * from "./keccak256"; 4 | export * from "./uint"; 5 | export * from "./exp"; 6 | export * from "./resetFork"; 7 | -------------------------------------------------------------------------------- /test/utils/keccak256.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | 3 | export function keccak256(str: string) { 4 | return ethers.utils.solidityKeccak256(["string"], [str]); 5 | } 6 | -------------------------------------------------------------------------------- /test/utils/resetFork.ts: -------------------------------------------------------------------------------- 1 | import { network, config } from "hardhat"; 2 | 3 | /** 4 | * Resets the state of the forked network to the original block number. 5 | */ 6 | export async function resetFork() { 7 | const { url, blockNumber } = config.networks.hardhat.forking!; 8 | await network.provider.request({ 9 | method: "hardhat_reset", 10 | params: [ 11 | { 12 | forking: { 13 | jsonRpcUrl: url, 14 | blockNumber, 15 | }, 16 | }, 17 | ], 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /test/utils/uint.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from "ethers"; 2 | 3 | export function uint(n: any) { 4 | if (typeof n === "number") { 5 | n = String(n); 6 | } 7 | return BigNumber.from(n); 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "node", 5 | "target": "es2020", 6 | "lib": ["es2020", "DOM"], 7 | "sourceMap": true, 8 | "allowJs": false, 9 | "allowSyntheticDefaultImports": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noImplicitReturns": true, 12 | "noImplicitThis": true, 13 | "noImplicitAny": true, 14 | "strictNullChecks": true, 15 | "noUnusedLocals": true, 16 | "resolveJsonModule": true 17 | }, 18 | "include": ["hardhat.config.ts", "scripts", "test", "deploy", "utils"] 19 | } 20 | --------------------------------------------------------------------------------