├── .build ├── mainnet.json └── ropsten.json ├── .circleci └── config.yml ├── .codecov.yml ├── .dockerignore ├── .gitattributes ├── .github └── CODEOWNERS ├── .gitignore ├── .gitmodules ├── .soliumrc.json ├── Dockerfile ├── LICENSE ├── README.md ├── contracts ├── OpenOracleData.sol ├── OpenOraclePriceData.sol ├── OpenOracleView.sol └── Uniswap │ ├── UniswapAnchoredView.sol │ ├── UniswapConfig.sol │ └── UniswapLib.sol ├── jest.config.js ├── 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 ├── saddle.config.js ├── script ├── coverage ├── deploy └── test ├── 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 ├── test_keys.env ├── tests ├── DockerProvider.js ├── Helpers.js ├── Matchers.js ├── NonReporterPricesTest.js ├── OpenOracleDataTest.js ├── OpenOracleViewTest.js ├── PostRealWorldPricesTest.js ├── UniswapAnchoredViewTest.js ├── UniswapConfigTest.js └── contracts │ ├── MockUniswapAnchoredView.sol │ ├── MockUniswapTokenPair.sol │ ├── ProxyPriceOracle.sol │ └── Test.sol └── 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 | } -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | codecov: codecov/codecov@1.0.3 5 | 6 | jobs: 7 | test_oracle: 8 | docker: 9 | - image: circleci/node:12 10 | working_directory: ~/repo 11 | steps: 12 | - run: 13 | | 14 | sudo wget https://github.com/ethereum/solidity/releases/download/v0.6.10/solc-static-linux -O /usr/local/bin/solc 15 | sudo chmod +x /usr/local/bin/solc 16 | - checkout 17 | - restore_cache: 18 | keys: 19 | - v1-dependencies-{{ checksum "package.json" }} 20 | - v1-dependencies- 21 | - restore_cache: 22 | keys: 23 | - v1-poster-dependencies-{{ checksum "poster/package.json" }} 24 | - v1-poster-dependencies- 25 | - restore_cache: 26 | keys: 27 | - v1-sdk-dependencies-{{ checksum "sdk/javascript/package.json" }} 28 | - v1-sdk-dependencies- 29 | - run: yarn install 30 | - save_cache: 31 | paths: 32 | - node_modules 33 | key: v1-dependencies-{{ checksum "package.json" }} 34 | - run: cd poster && yarn install && yarn prepare 35 | - save_cache: 36 | paths: 37 | - poster/node_modules 38 | key: v1-poster-dependencies-{{ checksum "poster/package.json" }} 39 | - run: cd sdk/javascript && yarn install && yarn prepare 40 | - save_cache: 41 | paths: 42 | - sdk/javascript/node_modules 43 | key: v1-sdk-dependencies-{{ checksum "sdk/javascript/package.json" }} 44 | - run: mkdir ~/junit-oracle 45 | - run: JEST_JUNIT_OUTPUT=~/junit-oracle/test-results.xml script/test tests/*Test.js -- --ci --reporters=default --reporters=jest-junit 46 | - store_test_results: 47 | path: ~/junit-oracle 48 | - store_artifacts: 49 | path: ~/junit-oracle 50 | 51 | test_coverage: 52 | docker: 53 | - image: circleci/node:13 54 | working_directory: ~/repo 55 | steps: 56 | - run: 57 | | 58 | sudo wget https://github.com/ethereum/solidity/releases/download/v0.6.10/solc-static-linux -O /usr/local/bin/solc 59 | sudo chmod +x /usr/local/bin/solc 60 | - checkout 61 | - restore_cache: 62 | keys: 63 | - cov-v1-dependencies-{{ checksum "package.json" }} 64 | - cov-v1-dependencies- 65 | - restore_cache: 66 | keys: 67 | - cov-v1-poster-dependencies-{{ checksum "poster/package.json" }} 68 | - cov-v1-poster-dependencies- 69 | - restore_cache: 70 | keys: 71 | - cov-v1-sdk-dependencies-{{ checksum "sdk/javascript/package.json" }} 72 | - cov-v1-sdk-dependencies- 73 | - run: yarn install 74 | - save_cache: 75 | paths: 76 | - node_modules 77 | key: cov-v1-dependencies-{{ checksum "package.json" }} 78 | - run: cd poster && yarn install && yarn prepare 79 | - save_cache: 80 | paths: 81 | - poster/node_modules 82 | key: cov-v1-poster-dependencies-{{ checksum "poster/package.json" }} 83 | - run: cd sdk/javascript && yarn install && yarn prepare 84 | - save_cache: 85 | paths: 86 | - sdk/javascript/node_modules 87 | key: cov-v1-sdk-dependencies-{{ checksum "sdk/javascript/package.json" }} 88 | - run: mkdir ~/junit-oracle 89 | - run: 90 | shell: /bin/bash -eox pipefail -O globstar 91 | name: yarn coverage 92 | no_output_timeout: 30m 93 | command: JEST_JUNIT_OUTPUT_DIR=~/junit-oracle JEST_JUNIT_OUTPUT_NAME=test-results.xml script/coverage $(circleci tests glob 'tests/**/**Test.js' | circleci tests split --split-by=timings) -- --maxWorkers=4 94 | - store_test_results: 95 | path: ~/junit-oracle 96 | - store_artifacts: 97 | path: ~/repo/coverage/coverage-final.json 98 | destination: coverage-final.json 99 | - store_artifacts: 100 | path: ~/repo/coverage/lcov-report 101 | destination: coverage 102 | - codecov/upload: 103 | file: ~/repo/coverage/coverage-final.json 104 | parallelism: 3 105 | resource_class: xlarge 106 | 107 | test_poster: 108 | docker: 109 | - image: circleci/node:12 110 | working_directory: ~/repo/poster 111 | steps: 112 | - checkout: 113 | path: ~/repo 114 | - restore_cache: 115 | keys: 116 | - v1-poster-dependencies-{{ checksum "package.json" }} 117 | - v1-poster-dependencies- 118 | - run: yarn install 119 | - save_cache: 120 | paths: 121 | - node_modules 122 | key: v1-poster-dependencies-{{ checksum "package.json" }} 123 | - run: mkdir ~/junit-poster 124 | - run: JEST_JUNIT_OUTPUT=~/junit-poster/test-results.xml yarn run test --ci --reporters=default --reporters=jest-junit 125 | - store_test_results: 126 | path: ~/junit-poster 127 | - store_artifacts: 128 | path: ~/junit-poster 129 | 130 | test_reporter_javascript: 131 | docker: 132 | - image: circleci/node:12 133 | working_directory: ~/repo/sdk/javascript 134 | steps: 135 | - checkout: 136 | path: ~/repo 137 | - restore_cache: 138 | keys: 139 | - v1-sdk-javascript-dependencies-{{ checksum "package.json" }} 140 | - v1-sdk-javascript-dependencies- 141 | - run: yarn install 142 | - save_cache: 143 | paths: 144 | - node_modules 145 | key: v1-sdk-javascript-dependencies-{{ checksum "package.json" }} 146 | - run: mkdir ~/junit-sdk-javascript 147 | - run: JEST_JUNIT_OUTPUT=~/junit-sdk-javascript/test-results.xml yarn run test --ci --reporters=default --reporters=jest-junit 148 | - store_test_results: 149 | path: ~/junit-sdk-javascript 150 | - store_artifacts: 151 | path: ~/junit-sdk-javascript 152 | 153 | workflows: 154 | version: 2 155 | test: 156 | jobs: 157 | - test_oracle 158 | - test_coverage 159 | - test_poster 160 | - test_reporter_javascript 161 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .build 2 | .circleci 3 | .dockerbuild 4 | .dockerbuild_cp 5 | .github 6 | node_modules 7 | sdk/javascript/node_modules 8 | sdk/javascript/.tsbuilt 9 | poster/node_modules 10 | poster/.tsbuilt 11 | tests 12 | yarn-error.log 13 | junit.xml 14 | 15 | # Allow files and directories 16 | !tests/contracts 17 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /poster/fixtures/generated/* binary 2 | *.sol linguist-language=Solidity 3 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @coburncoburn @jflatow @hayesgm @maxwolff @torreyatcitty -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "compound-config"] 2 | path = compound-config 3 | url = https://github.com/compound-finance/compound-config.git 4 | -------------------------------------------------------------------------------- /.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 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14.5.0-alpine3.10 2 | WORKDIR /open-oracle 3 | RUN wget https://github.com/ethereum/solidity/releases/download/v0.6.10/solc-static-linux -O /usr/local/bin/solc && chmod +x /usr/local/bin/solc 4 | RUN apk update && apk add --no-cache --virtual .gyp \ 5 | python \ 6 | make \ 7 | g++ \ 8 | yarn \ 9 | nodejs \ 10 | git 11 | 12 | RUN yarn global add node-gyp npx 13 | COPY package.json yarn.lock /open-oracle/ 14 | 15 | RUN yarn install --frozen-lockfile 16 | 17 | ENV PROVIDER PROVIDER 18 | COPY contracts contracts 19 | COPY tests tests 20 | COPY saddle.config.js saddle.config.js 21 | RUN npx saddle compile 22 | 23 | ENTRYPOINT [] 24 | CMD npx saddle 25 | -------------------------------------------------------------------------------- /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 | 2 | ## Open Oracle 3 | 4 | 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). 5 | 6 | ## Contracts 7 | 8 | First, you will need solc 0.6.6 installed. 9 | Additionally for testing, you will need TypeScript installed and will need to build the open-oracle-reporter project by running `cd sdk/javascript && yarn`. 10 | 11 | To fetch dependencies run: 12 | 13 | ``` 14 | yarn install 15 | ``` 16 | 17 | To compile everything run: 18 | 19 | ``` 20 | yarn run compile 21 | ``` 22 | 23 | To deploy contracts locally, you can run: 24 | 25 | ``` 26 | yarn run deploy --network development OpenOraclePriceData 27 | ``` 28 | 29 | Note: you will need to be running an Ethereum node locally in order for this to work. 30 | E.g., start [ganache-cli](https://github.com/trufflesuite/ganache-cli) in another shell. 31 | 32 | You can add a view in `MyView.sol` and run (default is `network=development`): 33 | 34 | ``` 35 | yarn run deploy MyView arg1 arg2 ... 36 | ``` 37 | 38 | To run tests: 39 | 40 | ``` 41 | yarn run test 42 | ``` 43 | 44 | To track deployed contracts in a saddle console: 45 | 46 | ``` 47 | yarn run console 48 | ``` 49 | ## Reporter SDK 50 | 51 | This repository contains a set of SDKs for reporters to easily sign "reporter" data in any supported languages. We currently support the following languages: 52 | 53 | * [JavaScript](./sdk/javascript/README.md) (in TypeScript) 54 | * [Elixir](./sdk/typescript/README.md) 55 | 56 | ## Poster 57 | 58 | The poster is a simple application that reads from a given feed (or set of feeds) and posts... 59 | 60 | ## Contributing 61 | 62 | 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. 63 | 64 | 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. 65 | -------------------------------------------------------------------------------- /contracts/OpenOracleData.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity ^0.6.10; 4 | pragma experimental ABIEncoderV2; 5 | 6 | /** 7 | * @title The Open Oracle Data Base Contract 8 | * @author Compound Labs, Inc. 9 | */ 10 | contract OpenOracleData { 11 | /** 12 | * @notice The event emitted when a source writes to its storage 13 | */ 14 | //event Write(address indexed source, indexed key, string kind, uint64 timestamp, value); 15 | 16 | /** 17 | * @notice Write a bunch of signed datum to the authenticated storage mapping 18 | * @param message The payload containing the timestamp, and (key, value) pairs 19 | * @param signature The cryptographic signature of the message payload, authorizing the source to write 20 | * @return The keys that were written 21 | */ 22 | //function put(bytes calldata message, bytes calldata signature) external returns ( memory); 23 | 24 | /** 25 | * @notice Read a single key with a pre-defined type signature from an authenticated source 26 | * @param source The verifiable author of the data 27 | * @param key The selector for the value to return 28 | * @return The claimed Unix timestamp for the data and the encoded value (defaults to (0, 0x)) 29 | */ 30 | //function get(address source, key) external view returns (uint, ); 31 | 32 | /** 33 | * @notice Recovers the source address which signed a message 34 | * @dev Comparing to a claimed address would add nothing, 35 | * as the caller could simply perform the recover and claim that address. 36 | * @param message The data that was presumably signed 37 | * @param signature The fingerprint of the data + private key 38 | * @return The source address which signed the message, presumably 39 | */ 40 | function source(bytes memory message, bytes memory signature) public pure returns (address) { 41 | (bytes32 r, bytes32 s, uint8 v) = abi.decode(signature, (bytes32, bytes32, uint8)); 42 | bytes32 hash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", keccak256(message))); 43 | return ecrecover(hash, v, r, s); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /contracts/OpenOraclePriceData.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity ^0.6.10; 4 | 5 | import "./OpenOracleData.sol"; 6 | 7 | /** 8 | * @title The Open Oracle Price Data Contract 9 | * @notice Values stored in this contract should represent a USD price with 6 decimals precision 10 | * @author Compound Labs, Inc. 11 | */ 12 | contract OpenOraclePriceData is OpenOracleData { 13 | ///@notice The event emitted when a source writes to its storage 14 | event Write(address indexed source, string key, uint64 timestamp, uint64 value); 15 | ///@notice The event emitted when the timestamp on a price is invalid and it is not written to storage 16 | event NotWritten(uint64 priorTimestamp, uint256 messageTimestamp, uint256 blockTimestamp); 17 | 18 | ///@notice The fundamental unit of storage for a reporter source 19 | struct Datum { 20 | uint64 timestamp; 21 | uint64 value; 22 | } 23 | 24 | /** 25 | * @dev The most recent authenticated data from all sources. 26 | * This is private because dynamic mapping keys preclude auto-generated getters. 27 | */ 28 | mapping(address => mapping(string => Datum)) private data; 29 | 30 | /** 31 | * @notice Write a bunch of signed datum to the authenticated storage mapping 32 | * @param message The payload containing the timestamp, and (key, value) pairs 33 | * @param signature The cryptographic signature of the message payload, authorizing the source to write 34 | * @return The keys that were written 35 | */ 36 | function put(bytes calldata message, bytes calldata signature) external returns (string memory) { 37 | (address source, uint64 timestamp, string memory key, uint64 value) = decodeMessage(message, signature); 38 | return putInternal(source, timestamp, key, value); 39 | } 40 | 41 | function putInternal(address source, uint64 timestamp, string memory key, uint64 value) internal returns (string memory) { 42 | // Only update if newer than stored, according to source 43 | Datum storage prior = data[source][key]; 44 | if (timestamp > prior.timestamp && timestamp < block.timestamp + 60 minutes && source != address(0)) { 45 | data[source][key] = Datum(timestamp, value); 46 | emit Write(source, key, timestamp, value); 47 | } else { 48 | emit NotWritten(prior.timestamp, timestamp, block.timestamp); 49 | } 50 | return key; 51 | } 52 | 53 | function decodeMessage(bytes calldata message, bytes calldata signature) internal pure returns (address, uint64, string memory, uint64) { 54 | // Recover the source address 55 | address source = source(message, signature); 56 | 57 | // Decode the message and check the kind 58 | (string memory kind, uint64 timestamp, string memory key, uint64 value) = abi.decode(message, (string, uint64, string, uint64)); 59 | require(keccak256(abi.encodePacked(kind)) == keccak256(abi.encodePacked("prices")), "Kind of data must be 'prices'"); 60 | return (source, timestamp, key, value); 61 | } 62 | 63 | /** 64 | * @notice Read a single key from an authenticated source 65 | * @param source The verifiable author of the data 66 | * @param key The selector for the value to return 67 | * @return The claimed Unix timestamp for the data and the price value (defaults to (0, 0)) 68 | */ 69 | function get(address source, string calldata key) external view returns (uint64, uint64) { 70 | Datum storage datum = data[source][key]; 71 | return (datum.timestamp, datum.value); 72 | } 73 | 74 | /** 75 | * @notice Read only the value for a single key from an authenticated source 76 | * @param source The verifiable author of the data 77 | * @param key The selector for the value to return 78 | * @return The price value (defaults to 0) 79 | */ 80 | function getPrice(address source, string calldata key) external view returns (uint64) { 81 | return data[source][key].value; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /contracts/OpenOracleView.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity ^0.6.10; 4 | 5 | import "./OpenOracleData.sol"; 6 | 7 | /** 8 | * @title The Open Oracle View Base Contract 9 | * @author Compound Labs, Inc. 10 | */ 11 | contract OpenOracleView { 12 | /** 13 | * @notice The Oracle Data Contract backing this View 14 | */ 15 | OpenOracleData public priceData; 16 | 17 | /** 18 | * @notice The static list of sources used by this View 19 | * @dev Note that while it is possible to create a view with dynamic sources, 20 | * that would not conform to the Open Oracle Standard specification. 21 | */ 22 | address[] public sources; 23 | 24 | /** 25 | * @notice Construct a view given the oracle backing address and the list of sources 26 | * @dev According to the protocol, Views must be immutable to be considered conforming. 27 | * @param data_ The address of the oracle data contract which is backing the view 28 | * @param sources_ The list of source addresses to include in the aggregate value 29 | */ 30 | constructor(OpenOracleData data_, address[] memory sources_) public { 31 | require(sources_.length > 0, "Must initialize with sources"); 32 | priceData = data_; 33 | sources = sources_; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /contracts/Uniswap/UniswapAnchoredView.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity ^0.6.10; 4 | pragma experimental ABIEncoderV2; 5 | 6 | import "../OpenOraclePriceData.sol"; 7 | import "./UniswapConfig.sol"; 8 | import "./UniswapLib.sol"; 9 | 10 | struct Observation { 11 | uint timestamp; 12 | uint acc; 13 | } 14 | 15 | contract UniswapAnchoredView is UniswapConfig { 16 | using FixedPoint for *; 17 | 18 | /// @notice The Open Oracle Price Data contract 19 | OpenOraclePriceData public immutable priceData; 20 | 21 | /// @notice The number of wei in 1 ETH 22 | uint public constant ethBaseUnit = 1e18; 23 | 24 | /// @notice A common scaling factor to maintain precision 25 | uint public constant expScale = 1e18; 26 | 27 | /// @notice The Open Oracle Reporter 28 | address public immutable reporter; 29 | 30 | /// @notice The highest ratio of the new price to the anchor price that will still trigger the price to be updated 31 | uint public immutable upperBoundAnchorRatio; 32 | 33 | /// @notice The lowest ratio of the new price to the anchor price that will still trigger the price to be updated 34 | uint public immutable lowerBoundAnchorRatio; 35 | 36 | /// @notice The minimum amount of time in seconds required for the old uniswap price accumulator to be replaced 37 | uint public immutable anchorPeriod; 38 | 39 | /// @notice Official prices by symbol hash 40 | mapping(bytes32 => uint) public prices; 41 | 42 | /// @notice Circuit breaker for using anchor price oracle directly, ignoring reporter 43 | bool public reporterInvalidated; 44 | 45 | /// @notice The old observation for each symbolHash 46 | mapping(bytes32 => Observation) public oldObservations; 47 | 48 | /// @notice The new observation for each symbolHash 49 | mapping(bytes32 => Observation) public newObservations; 50 | 51 | /// @notice The event emitted when new prices are posted but the stored price is not updated due to the anchor 52 | event PriceGuarded(string symbol, uint reporter, uint anchor); 53 | 54 | /// @notice The event emitted when the stored price is updated 55 | event PriceUpdated(string symbol, uint price); 56 | 57 | /// @notice The event emitted when anchor price is updated 58 | event AnchorPriceUpdated(string symbol, uint anchorPrice, uint oldTimestamp, uint newTimestamp); 59 | 60 | /// @notice The event emitted when the uniswap window changes 61 | event UniswapWindowUpdated(bytes32 indexed symbolHash, uint oldTimestamp, uint newTimestamp, uint oldPrice, uint newPrice); 62 | 63 | /// @notice The event emitted when reporter invalidates itself 64 | event ReporterInvalidated(address reporter); 65 | 66 | bytes32 constant ethHash = keccak256(abi.encodePacked("ETH")); 67 | bytes32 constant rotateHash = keccak256(abi.encodePacked("rotate")); 68 | 69 | /** 70 | * @notice Construct a uniswap anchored view for a set of token configurations 71 | * @dev Note that to avoid immature TWAPs, the system must run for at least a single anchorPeriod before using. 72 | * @param reporter_ The reporter whose prices are to be used 73 | * @param anchorToleranceMantissa_ The percentage tolerance that the reporter may deviate from the uniswap anchor 74 | * @param anchorPeriod_ The minimum amount of time required for the old uniswap price accumulator to be replaced 75 | * @param configs The static token configurations which define what prices are supported and how 76 | */ 77 | constructor(OpenOraclePriceData priceData_, 78 | address reporter_, 79 | uint anchorToleranceMantissa_, 80 | uint anchorPeriod_, 81 | TokenConfig[] memory configs) UniswapConfig(configs) public { 82 | priceData = priceData_; 83 | reporter = reporter_; 84 | anchorPeriod = anchorPeriod_; 85 | 86 | // Allow the tolerance to be whatever the deployer chooses, but prevent under/overflow (and prices from being 0) 87 | upperBoundAnchorRatio = anchorToleranceMantissa_ > uint(-1) - 100e16 ? uint(-1) : 100e16 + anchorToleranceMantissa_; 88 | lowerBoundAnchorRatio = anchorToleranceMantissa_ < 100e16 ? 100e16 - anchorToleranceMantissa_ : 1; 89 | 90 | for (uint i = 0; i < configs.length; i++) { 91 | TokenConfig memory config = configs[i]; 92 | require(config.baseUnit > 0, "baseUnit must be greater than zero"); 93 | address uniswapMarket = config.uniswapMarket; 94 | if (config.priceSource == PriceSource.REPORTER) { 95 | require(uniswapMarket != address(0), "reported prices must have an anchor"); 96 | bytes32 symbolHash = config.symbolHash; 97 | uint cumulativePrice = currentCumulativePrice(config); 98 | oldObservations[symbolHash].timestamp = block.timestamp; 99 | newObservations[symbolHash].timestamp = block.timestamp; 100 | oldObservations[symbolHash].acc = cumulativePrice; 101 | newObservations[symbolHash].acc = cumulativePrice; 102 | emit UniswapWindowUpdated(symbolHash, block.timestamp, block.timestamp, cumulativePrice, cumulativePrice); 103 | } else { 104 | require(uniswapMarket == address(0), "only reported prices utilize an anchor"); 105 | } 106 | } 107 | } 108 | 109 | /** 110 | * @notice Get the official price for a symbol 111 | * @param symbol The symbol to fetch the price of 112 | * @return Price denominated in USD, with 6 decimals 113 | */ 114 | function price(string memory symbol) external view returns (uint) { 115 | TokenConfig memory config = getTokenConfigBySymbol(symbol); 116 | return priceInternal(config); 117 | } 118 | 119 | function priceInternal(TokenConfig memory config) internal view returns (uint) { 120 | if (config.priceSource == PriceSource.REPORTER) return prices[config.symbolHash]; 121 | if (config.priceSource == PriceSource.FIXED_USD) return config.fixedPrice; 122 | if (config.priceSource == PriceSource.FIXED_ETH) { 123 | uint usdPerEth = prices[ethHash]; 124 | require(usdPerEth > 0, "ETH price not set, cannot convert to dollars"); 125 | return mul(usdPerEth, config.fixedPrice) / ethBaseUnit; 126 | } 127 | } 128 | 129 | /** 130 | * @notice Get the underlying price of a cToken 131 | * @dev Implements the PriceOracle interface for Compound v2. 132 | * @param cToken The cToken address for price retrieval 133 | * @return Price denominated in USD, with 18 decimals, for the given cToken address 134 | */ 135 | function getUnderlyingPrice(address cToken) external view returns (uint) { 136 | TokenConfig memory config = getTokenConfigByCToken(cToken); 137 | // Comptroller needs prices in the format: ${raw price} * 1e(36 - baseUnit) 138 | // Since the prices in this view have 6 decimals, we must scale them by 1e(36 - 6 - baseUnit) 139 | return mul(1e30, priceInternal(config)) / config.baseUnit; 140 | } 141 | 142 | /** 143 | * @notice Post open oracle reporter prices, and recalculate stored price by comparing to anchor 144 | * @dev We let anyone pay to post anything, but only prices from configured reporter will be stored in the view. 145 | * @param messages The messages to post to the oracle 146 | * @param signatures The signatures for the corresponding messages 147 | * @param symbols The symbols to compare to anchor for authoritative reading 148 | */ 149 | function postPrices(bytes[] calldata messages, bytes[] calldata signatures, string[] calldata symbols) external { 150 | require(messages.length == signatures.length, "messages and signatures must be 1:1"); 151 | 152 | // Save the prices 153 | for (uint i = 0; i < messages.length; i++) { 154 | priceData.put(messages[i], signatures[i]); 155 | } 156 | 157 | uint ethPrice = fetchEthPrice(); 158 | 159 | // Try to update the view storage 160 | for (uint i = 0; i < symbols.length; i++) { 161 | postPriceInternal(symbols[i], ethPrice); 162 | } 163 | } 164 | 165 | function postPriceInternal(string memory symbol, uint ethPrice) internal { 166 | TokenConfig memory config = getTokenConfigBySymbol(symbol); 167 | require(config.priceSource == PriceSource.REPORTER, "only reporter prices get posted"); 168 | 169 | bytes32 symbolHash = keccak256(abi.encodePacked(symbol)); 170 | uint reporterPrice = priceData.getPrice(reporter, symbol); 171 | uint anchorPrice; 172 | if (symbolHash == ethHash) { 173 | anchorPrice = ethPrice; 174 | } else { 175 | anchorPrice = fetchAnchorPrice(symbol, config, ethPrice); 176 | } 177 | 178 | if (reporterInvalidated) { 179 | prices[symbolHash] = anchorPrice; 180 | emit PriceUpdated(symbol, anchorPrice); 181 | } else if (isWithinAnchor(reporterPrice, anchorPrice)) { 182 | prices[symbolHash] = reporterPrice; 183 | emit PriceUpdated(symbol, reporterPrice); 184 | } else { 185 | emit PriceGuarded(symbol, reporterPrice, anchorPrice); 186 | } 187 | } 188 | 189 | function isWithinAnchor(uint reporterPrice, uint anchorPrice) internal view returns (bool) { 190 | if (reporterPrice > 0) { 191 | uint anchorRatio = mul(anchorPrice, 100e16) / reporterPrice; 192 | return anchorRatio <= upperBoundAnchorRatio && anchorRatio >= lowerBoundAnchorRatio; 193 | } 194 | return false; 195 | } 196 | 197 | /** 198 | * @dev Fetches the current token/eth price accumulator from uniswap. 199 | */ 200 | function currentCumulativePrice(TokenConfig memory config) internal view returns (uint) { 201 | (uint cumulativePrice0, uint cumulativePrice1,) = UniswapV2OracleLibrary.currentCumulativePrices(config.uniswapMarket); 202 | if (config.isUniswapReversed) { 203 | return cumulativePrice1; 204 | } else { 205 | return cumulativePrice0; 206 | } 207 | } 208 | 209 | /** 210 | * @dev Fetches the current eth/usd price from uniswap, with 6 decimals of precision. 211 | * Conversion factor is 1e18 for eth/usdc market, since we decode uniswap price statically with 18 decimals. 212 | */ 213 | function fetchEthPrice() internal returns (uint) { 214 | return fetchAnchorPrice("ETH", getTokenConfigBySymbolHash(ethHash), ethBaseUnit); 215 | } 216 | 217 | /** 218 | * @dev Fetches the current token/usd price from uniswap, with 6 decimals of precision. 219 | * @param conversionFactor 1e18 if seeking the ETH price, and a 6 decimal ETH-USDC price in the case of other assets 220 | */ 221 | function fetchAnchorPrice(string memory symbol, TokenConfig memory config, uint conversionFactor) internal virtual returns (uint) { 222 | (uint nowCumulativePrice, uint oldCumulativePrice, uint oldTimestamp) = pokeWindowValues(config); 223 | 224 | // This should be impossible, but better safe than sorry 225 | require(block.timestamp > oldTimestamp, "now must come after before"); 226 | uint timeElapsed = block.timestamp - oldTimestamp; 227 | 228 | // Calculate uniswap time-weighted average price 229 | // Underflow is a property of the accumulators: https://uniswap.org/audit.html#orgc9b3190 230 | FixedPoint.uq112x112 memory priceAverage = FixedPoint.uq112x112(uint224((nowCumulativePrice - oldCumulativePrice) / timeElapsed)); 231 | uint rawUniswapPriceMantissa = priceAverage.decode112with18(); 232 | uint unscaledPriceMantissa = mul(rawUniswapPriceMantissa, conversionFactor); 233 | uint anchorPrice; 234 | 235 | // Adjust rawUniswapPrice according to the units of the non-ETH asset 236 | // In the case of ETH, we would have to scale by 1e6 / USDC_UNITS, but since baseUnit2 is 1e6 (USDC), it cancels 237 | 238 | // In the case of non-ETH tokens 239 | // a. pokeWindowValues already handled uniswap reversed cases, so priceAverage will always be Token/ETH TWAP price. 240 | // b. conversionFactor = ETH price * 1e6 241 | // unscaledPriceMantissa = priceAverage(token/ETH TWAP price) * expScale * conversionFactor 242 | // so -> 243 | // anchorPrice = priceAverage * tokenBaseUnit / ethBaseUnit * ETH_price * 1e6 244 | // = priceAverage * conversionFactor * tokenBaseUnit / ethBaseUnit 245 | // = unscaledPriceMantissa / expScale * tokenBaseUnit / ethBaseUnit 246 | anchorPrice = mul(unscaledPriceMantissa, config.baseUnit) / ethBaseUnit / expScale; 247 | 248 | emit AnchorPriceUpdated(symbol, anchorPrice, oldTimestamp, block.timestamp); 249 | 250 | return anchorPrice; 251 | } 252 | 253 | /** 254 | * @dev Get time-weighted average prices for a token at the current timestamp. 255 | * Update new and old observations of lagging window if period elapsed. 256 | */ 257 | function pokeWindowValues(TokenConfig memory config) internal returns (uint, uint, uint) { 258 | bytes32 symbolHash = config.symbolHash; 259 | uint cumulativePrice = currentCumulativePrice(config); 260 | 261 | Observation memory newObservation = newObservations[symbolHash]; 262 | 263 | // Update new and old observations if elapsed time is greater than or equal to anchor period 264 | uint timeElapsed = block.timestamp - newObservation.timestamp; 265 | if (timeElapsed >= anchorPeriod) { 266 | oldObservations[symbolHash].timestamp = newObservation.timestamp; 267 | oldObservations[symbolHash].acc = newObservation.acc; 268 | 269 | newObservations[symbolHash].timestamp = block.timestamp; 270 | newObservations[symbolHash].acc = cumulativePrice; 271 | emit UniswapWindowUpdated(config.symbolHash, newObservation.timestamp, block.timestamp, newObservation.acc, cumulativePrice); 272 | } 273 | return (cumulativePrice, oldObservations[symbolHash].acc, oldObservations[symbolHash].timestamp); 274 | } 275 | 276 | /** 277 | * @notice Invalidate the reporter, and fall back to using anchor directly in all cases 278 | * @dev Only the reporter may sign a message which allows it to invalidate itself. 279 | * To be used in cases of emergency, if the reporter thinks their key may be compromised. 280 | * @param message The data that was presumably signed 281 | * @param signature The fingerprint of the data + private key 282 | */ 283 | function invalidateReporter(bytes memory message, bytes memory signature) external { 284 | (string memory decodedMessage, ) = abi.decode(message, (string, address)); 285 | require(keccak256(abi.encodePacked(decodedMessage)) == rotateHash, "invalid message must be 'rotate'"); 286 | require(source(message, signature) == reporter, "invalidation message must come from the reporter"); 287 | reporterInvalidated = true; 288 | emit ReporterInvalidated(reporter); 289 | } 290 | 291 | /** 292 | * @notice Recovers the source address which signed a message 293 | * @dev Comparing to a claimed address would add nothing, 294 | * as the caller could simply perform the recover and claim that address. 295 | * @param message The data that was presumably signed 296 | * @param signature The fingerprint of the data + private key 297 | * @return The source address which signed the message, presumably 298 | */ 299 | function source(bytes memory message, bytes memory signature) public pure returns (address) { 300 | (bytes32 r, bytes32 s, uint8 v) = abi.decode(signature, (bytes32, bytes32, uint8)); 301 | bytes32 hash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", keccak256(message))); 302 | return ecrecover(hash, v, r, s); 303 | } 304 | 305 | /// @dev Overflow proof multiplication 306 | function mul(uint a, uint b) internal pure returns (uint) { 307 | if (a == 0) return 0; 308 | uint c = a * b; 309 | require(c / a == b, "multiplication overflow"); 310 | return c; 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /contracts/Uniswap/UniswapLib.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity ^0.6.10; 4 | 5 | // Based on code from https://github.com/Uniswap/uniswap-v2-periphery 6 | 7 | // a library for handling binary fixed point numbers (https://en.wikipedia.org/wiki/Q_(number_format)) 8 | library FixedPoint { 9 | // range: [0, 2**112 - 1] 10 | // resolution: 1 / 2**112 11 | struct uq112x112 { 12 | uint224 _x; 13 | } 14 | 15 | // returns a uq112x112 which represents the ratio of the numerator to the denominator 16 | // equivalent to encode(numerator).div(denominator) 17 | function fraction(uint112 numerator, uint112 denominator) internal pure returns (uq112x112 memory) { 18 | require(denominator > 0, "FixedPoint: DIV_BY_ZERO"); 19 | return uq112x112((uint224(numerator) << 112) / denominator); 20 | } 21 | 22 | // decode a uq112x112 into a uint with 18 decimals of precision 23 | function decode112with18(uq112x112 memory self) internal pure returns (uint) { 24 | // we only have 256 - 224 = 32 bits to spare, so scaling up by ~60 bits is dangerous 25 | // instead, get close to: 26 | // (x * 1e18) >> 112 27 | // without risk of overflowing, e.g.: 28 | // (x) / 2 ** (112 - lg(1e18)) 29 | return uint(self._x) / 5192296858534827; 30 | } 31 | } 32 | 33 | // library with helper methods for oracles that are concerned with computing average prices 34 | library UniswapV2OracleLibrary { 35 | using FixedPoint for *; 36 | 37 | // helper function that returns the current block timestamp within the range of uint32, i.e. [0, 2**32 - 1] 38 | function currentBlockTimestamp() internal view returns (uint32) { 39 | return uint32(block.timestamp % 2 ** 32); 40 | } 41 | 42 | // produces the cumulative price using counterfactuals to save gas and avoid a call to sync. 43 | function currentCumulativePrices( 44 | address pair 45 | ) internal view returns (uint price0Cumulative, uint price1Cumulative, uint32 blockTimestamp) { 46 | blockTimestamp = currentBlockTimestamp(); 47 | price0Cumulative = IUniswapV2Pair(pair).price0CumulativeLast(); 48 | price1Cumulative = IUniswapV2Pair(pair).price1CumulativeLast(); 49 | 50 | // if time has elapsed since the last update on the pair, mock the accumulated price values 51 | (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast) = IUniswapV2Pair(pair).getReserves(); 52 | if (blockTimestampLast != blockTimestamp) { 53 | // subtraction overflow is desired 54 | uint32 timeElapsed = blockTimestamp - blockTimestampLast; 55 | // addition overflow is desired 56 | // counterfactual 57 | price0Cumulative += uint(FixedPoint.fraction(reserve1, reserve0)._x) * timeElapsed; 58 | // counterfactual 59 | price1Cumulative += uint(FixedPoint.fraction(reserve0, reserve1)._x) * timeElapsed; 60 | } 61 | } 62 | } 63 | 64 | interface IUniswapV2Pair { 65 | function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast); 66 | function price0CumulativeLast() external view returns (uint); 67 | function price1CumulativeLast() external view returns (uint); 68 | } 69 | -------------------------------------------------------------------------------- /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: null, 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: null, 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: null, 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: ["/tests/Matchers.js"], 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 | // Timeout of a test in milliseconds, default timeout is 5000 ms. 135 | testTimeout: 300000, 136 | 137 | // Options that will be passed to the testEnvironment 138 | // testEnvironmentOptions: {}, 139 | 140 | // Adds a location field to test results 141 | // testLocationInResults: false, 142 | 143 | // The glob patterns Jest uses to detect test files 144 | // testMatch: [ 145 | // "**/__tests__/**/*.[jt]s?(x)", 146 | // "**/?(*.)+(spec|test).[tj]s?(x)" 147 | // ], 148 | 149 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 150 | // testPathIgnorePatterns: [ 151 | // "/node_modules/" 152 | // ], 153 | 154 | // The regexp pattern or array of patterns that Jest uses to detect test files 155 | // testRegex: [], 156 | 157 | // This option allows the use of a custom results processor 158 | // testResultsProcessor: null, 159 | 160 | // This option allows use of a custom test runner 161 | // testRunner: "jasmine2", 162 | 163 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 164 | // testURL: "http://localhost", 165 | 166 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 167 | // timers: "real", 168 | 169 | // A map from regular expressions to paths to transformers 170 | // transform: null, 171 | 172 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 173 | // transformIgnorePatterns: [ 174 | // "/node_modules/" 175 | // ], 176 | 177 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 178 | // unmockedModulePathPatterns: undefined, 179 | 180 | // Indicates whether each individual test should be reported during the run 181 | // verbose: null, 182 | 183 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 184 | // watchPathIgnorePatterns: [], 185 | 186 | // Whether to use watchman for file crawling 187 | // watchman: true, 188 | }; 189 | -------------------------------------------------------------------------------- /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 | "bignumber.js": "^9.0.0", 11 | "eth-saddle": "0.1.21", 12 | "web3": "^1.2.4", 13 | "yargs": "^15.0.2" 14 | }, 15 | "devDependencies": { 16 | "istanbul": "^0.4.5", 17 | "jest": "^24.9.0", 18 | "jest-cli": "^24.9.0", 19 | "jest-junit": "^10.0.0" 20 | }, 21 | "scripts": { 22 | "test": "script/test", 23 | "coverage": "script/coverage", 24 | "console": "npx -n --experimental-repl-await saddle console", 25 | "compile": "npx saddle compile", 26 | "deploy": "npx saddle deploy" 27 | }, 28 | "resolutions": { 29 | "**/ganache-core": "https://github.com/compound-finance/ganache-core.git#compound", 30 | "scrypt.js": "https://registry.npmjs.org/@compound-finance/ethereumjs-wallet/-/ethereumjs-wallet-0.6.3.tgz", 31 | "**/sha3": "^2.1.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /saddle.config.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | // solc: "solc", // Solc command to run 4 | // solc_args: [], // Extra solc args 5 | build_dir: process.env['SADDLE_BUILD'] || ".build", // Directory to place built contracts 6 | contracts: process.env['SADDLE_CONTRACTS'] || "contracts/*.sol contracts/**/*.sol tests/contracts/*.sol", // Glob to match contract files 7 | tests: ['**/tests/*Test.js'], // Glob to match test files 8 | networks: { // Define configuration for each network 9 | ropsten: { 10 | providers: [ 11 | {env: "PROVIDER"}, 12 | {file: "~/.ethereum/ropsten-url"}, // Load from given file with contents as the URL (e.g. https://infura.io/api-key) 13 | ], 14 | web3: { 15 | gas: [ 16 | {env: "GAS"}, 17 | {default: "8000000"} 18 | ], 19 | gas_price: [ 20 | {env: "GAS_PRICE"}, 21 | {default: "12000000000"} 22 | ], 23 | options: { 24 | transactionConfirmationBlocks: 1, 25 | transactionBlockTimeout: 5 26 | } 27 | }, 28 | accounts: [ 29 | {env: "ACCOUNT"}, 30 | {file: "~/.ethereum/ropsten"} // Load from given file with contents as the private key (e.g. 0x...) 31 | ] 32 | }, 33 | development: { 34 | providers: [ // How to load provider (processed in order) 35 | { env: "PROVIDER" }, // Try to load Http provider from `PROVIDER` env variable (e.g. env PROVIDER=http://...) 36 | { http: "http://127.0.0.1:8545" } // Fallback to localhost provider 37 | ], 38 | web3: { // Web3 options for immediate confirmation in development mode 39 | gas: [ 40 | { env: "GAS" }, 41 | { default: "4600000" } 42 | ], 43 | gas_price: [ 44 | { env: "GAS_PRICE" }, 45 | { default: "12000000000" } 46 | ], 47 | options: { 48 | transactionConfirmationBlocks: 1, 49 | transactionBlockTimeout: 5 50 | } 51 | }, 52 | accounts: [ // How to load default account for transactions 53 | { env: "ACCOUNT" }, // Load from `ACCOUNT` env variable (e.g. env ACCOUNT=0x...) 54 | { unlocked: 0 } // Else, try to grab first "unlocked" account from provider 55 | ] 56 | }, 57 | test: { 58 | providers: [ 59 | { env: "PROVIDER" }, 60 | { 61 | ganache: { 62 | gasLimit: 80000000 63 | } 64 | }, // In test mode, connect to a new ganache provider. Any options will be passed to ganache 65 | ], 66 | web3: { 67 | gas: [ 68 | { env: "GAS" }, 69 | { default: "8000000" } 70 | ], 71 | gas_price: [ 72 | { env: "GAS_PRICE" }, 73 | { default: "12000000000" } 74 | ], 75 | options: { 76 | transactionConfirmationBlocks: 1, 77 | transactionBlockTimeout: 5 78 | } 79 | }, 80 | accounts: [ 81 | { env: "ACCOUNT" }, 82 | { unlocked: 0 } 83 | ] 84 | }, 85 | rinkeby: { 86 | providers: [ 87 | { env: "PROVIDER" }, 88 | { file: "~/.ethereum/rinkeby-url" }, // Load from given file with contents as the URL (e.g. https://infura.io/api-key) 89 | { http: "https://rinkeby-eth.compound.finance" } 90 | ], 91 | web3: { 92 | gas: [ 93 | { env: "GAS" }, 94 | { default: "4600000" } 95 | ], 96 | gas_price: [ 97 | { env: "GAS_PRICE" }, 98 | { default: "12000000000" } 99 | ], 100 | options: { 101 | transactionConfirmationBlocks: 1, 102 | transactionBlockTimeout: 5 103 | } 104 | }, 105 | accounts: [ 106 | { env: "ACCOUNT" }, 107 | { file: "~/.ethereum/rinkeby" } // Load from given file with contents as the private key (e.g. 0x...) 108 | ] 109 | }, 110 | mainnet: { 111 | providers: [ 112 | { env: "PROVIDER" }, 113 | { file: "~/.ethereum/mainnet-url" }, // Load from given file with contents as the URL (e.g. https://infura.io/api-key) 114 | { http: "https://mainnet-eth.compound.finance" } 115 | ], 116 | web3: { 117 | gas: [ 118 | { env: "GAS" }, 119 | { default: "4600000" } 120 | ], 121 | gas_price: [ 122 | { env: "GAS_PRICE" }, 123 | { default: "6000000000" } 124 | ], 125 | options: { 126 | transactionConfirmationBlocks: 1, 127 | transactionBlockTimeout: 5 128 | } 129 | }, 130 | accounts: [ 131 | { env: "ACCOUNT" }, 132 | { file: "~/.ethereum/mainnet" } // Load from given file with contents as the private key (e.g. 0x...) 133 | ] 134 | } 135 | }, 136 | get_network_file: (network) => { 137 | return null; 138 | }, 139 | read_network_file: (network) => { 140 | const fs = require('fs'); 141 | const path = require('path'); 142 | const util = require('util'); 143 | 144 | const networkFile = path.join(process.cwd(), 'compound-config', 'networks', `${network}.json`); 145 | return util.promisify(fs.readFile)(networkFile).then((json) => { 146 | const contracts = JSON.parse(json)['Contracts'] || {}; 147 | 148 | return Object.fromEntries(Object.entries(contracts).map(([contract, address]) => { 149 | const mapper = { 150 | PriceFeed: 'UniswapAnchoredView', 151 | PriceData: 'OpenOraclePriceData' 152 | }; 153 | 154 | return [mapper[contract] || contract, address]; 155 | })); 156 | }); 157 | } 158 | }; 159 | -------------------------------------------------------------------------------- /script/coverage: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eo pipefail 4 | 5 | dir=`dirname $0` 6 | reporter_sdk_dir=$(cd "$dir/../sdk/javascript" && pwd) 7 | coverage_root="$dir/../coverage" 8 | 9 | if [ ! -f "$reporter_sdk_dir/.tsbuilt/reporter.js" ]; then 10 | echo "Compiling Reporter SDK..." 11 | ( 12 | cd $reporter_sdk_dir && 13 | yarn install --ignore-optional && 14 | yarn prepare 15 | ) && echo "Reporter Compiled" || (echo "Compilation failed" && exit 1) 16 | fi 17 | 18 | echo "Compiling contracts with trace..." 19 | npx saddle compile --trace 20 | 21 | rm -rf "$coverage_root" 22 | 23 | echo "Running coverage..." 24 | npx saddle coverage $@ 25 | coverage_code=$? 26 | 27 | npx istanbul report --root="$coverage_root" lcov json 28 | 29 | echo "Coverage generated. Report at $(cd $coverage_root && pwd)/lcov-report/index.html" 30 | 31 | exit $coverage_code 32 | -------------------------------------------------------------------------------- /script/deploy: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eox pipefail 4 | 5 | export network="$1" 6 | api_key="$2" 7 | reporters="$3" 8 | 9 | if [ -z "$network" -o -z "$api_key" ]; then 10 | echo "script/deploy [network] [etherscan_api_key] [reporters]"; 11 | exit 1; 12 | fi 13 | 14 | npx saddle compile 15 | npx saddle deploy OpenOraclePriceData -n "$network" 16 | sleep 15; # allow Etherscan time to see contract 17 | data_addr=`node -e "const contracts = require('./.build/${network}.json'); console.log(contracts.OpenOraclePriceData);"` 18 | npx saddle verify "$api_key" "$data_addr" OpenOraclePriceData -n "$network" -vvv 19 | 20 | npx saddle deploy AnchoredView "$data_addr" "$reporter" "$anchor_address" "$anchor_tolerance_mantissa" "$ctokens":struct -n "$network" 21 | sleep 15; # allow Etherscan time to see contract 22 | view_addr=`node -e "const contracts = require('./.build/${network}.json'); console.log(contracts.AnchoredView);"` 23 | npx saddle verify "$api_key" "$view_addr" AnchoredView "$data_addr" "$reporter" "$anchor_address" "$anchor_tolerance_mantissa" "$ctokens":struct -n "$network" 24 | -------------------------------------------------------------------------------- /script/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eo pipefail 4 | 5 | dir=`dirname $0` 6 | reporter_sdk_dir=$(cd "$dir/../sdk/javascript" && pwd) 7 | 8 | if [ ! -f "$reporter_sdk_dir/.tsbuilt/reporter.js" ]; then 9 | echo "Compiling Reporter SDK..." 10 | ( 11 | cd $reporter_sdk_dir && 12 | yarn install --ignore-optional && 13 | yarn prepare 14 | ) && echo "Reporter Compiled" || (echo "Compilation failed" && exit 1) 15 | fi 16 | 17 | echo "Compiling contracts..." 18 | npx saddle compile 19 | 20 | echo "Running tests..." 21 | npx saddle test $@ 22 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test_keys.env: -------------------------------------------------------------------------------- 1 | # Reporter 1 2 | REPORTER_1_ADDRESS=0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1 3 | REPORTER_1_PRIVATE_KEY=0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d 4 | 5 | # Reporter 2 6 | REPORTER_2_ADDRESS=0xffcf8fdee72ac11b5c542428b35eef5769c409f0 7 | REPORTER_2_PRIVATE_KEY=0x6cbed15c793ce57650b9877cf6fa156fbef513c4e6134f022a85b1ffdd59b2a1 8 | 9 | # Poster (from the ganache-cli settings) 10 | POSTER_KEY=0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d 11 | 12 | # Anchor 13 | ANCHOR_ADDRESS=0x8d7f3515625aaf54ec5a2f9eaf462ba0a15b8d6b 14 | ANCHOR_PRIVATE_KEY=0x852508391239bc5b5c62e20bb23e4e66849f9b9c0c44ac7ec77d10c666fd3390 15 | 16 | ANCHOR_MANTISSA_HEX=0x16345785d8a0000 17 | 18 | # Ctokens struct 19 | CTOKENS={"cEthAddress":"0x0000000000000000000000000000000000000001","cUsdcAddress":"0x0000000000000000000000000000000000000002","cDaiAddress":"0x0000000000000000000000000000000000000003","cRepAddress":"0x0000000000000000000000000000000000000004","cWbtcAddress":"0x0000000000000000000000000000000000000005","cBatAddress":"0x0000000000000000000000000000000000000006","cZrxAddress":"0x0000000000000000000000000000000000000007","cSaiAddress":"0x0000000000000000000000000000000000000008","cUsdtAddress":"0x0000000000000000000000000000000000000009"} 20 | -------------------------------------------------------------------------------- /tests/DockerProvider.js: -------------------------------------------------------------------------------- 1 | let { errors } = require('web3-core-helpers'); 2 | const { exec } = require('child_process'); 3 | const util = require('util'); 4 | 5 | const execute = util.promisify(exec); 6 | 7 | /** 8 | * DockerProvider should be used to send rpc calls via a Docker node 9 | */ 10 | class DockerProvider { 11 | constructor(endpoint, service) { 12 | this.endpoint = endpoint; 13 | this.service = service; 14 | } 15 | 16 | async send(payload, callback) { 17 | const opts = JSON.stringify({ 18 | method: 'post', 19 | body: payload, 20 | json: true, 21 | url: this.endpoint 22 | }); 23 | const cmd = `node -e 'require("request")(${opts}, (err, res, body) => { if (err) { throw err; }; console.log(JSON.stringify(body)) } )'`; 24 | try { 25 | const data = await execute(`docker exec ${this.service} ${cmd}`); 26 | 27 | try { 28 | return callback(null, JSON.parse(data.stdout)); 29 | } catch(e) { 30 | return callback(errors.InvalidResponse(data.stdout)); 31 | } 32 | } catch (e) { 33 | callback(e); 34 | } 35 | } 36 | 37 | disconnect() {} 38 | }; 39 | 40 | module.exports = DockerProvider; 41 | -------------------------------------------------------------------------------- /tests/Helpers.js: -------------------------------------------------------------------------------- 1 | const Web3 = require('web3'); 2 | const BigNumber = require("bignumber.js"); 3 | 4 | const web3 = new Web3(); // no provider, since we won't make any calls 5 | 6 | const fixed = num => { 7 | return (new BigNumber(num).toFixed()); 8 | }; 9 | 10 | function uint(n) { 11 | return web3.utils.toBN(n).toString(); 12 | } 13 | 14 | function keccak256(str) { 15 | return web3.utils.keccak256(str); 16 | } 17 | 18 | function address(n) { 19 | return `0x${n.toString(16).padStart(40, '0')}`; 20 | } 21 | 22 | function bytes(str) { 23 | return web3.eth.abi.encodeParameter('string', str); 24 | } 25 | 26 | function uint256(int) { 27 | return web3.eth.abi.encodeParameter('uint256', int); 28 | } 29 | 30 | function numToHex(num) { 31 | return web3.utils.numberToHex(num); 32 | } 33 | 34 | function numToBigNum(num) { 35 | return web3.utils.toBN(num); 36 | } 37 | 38 | function time(){ 39 | return Math.floor(new Date() / 1000); 40 | } 41 | 42 | async function currentBlockTimestamp(web3_) { 43 | const blockNumber = await sendRPC(web3_, "eth_blockNumber", []); 44 | const block = await sendRPC(web3_, "eth_getBlockByNumber", [ blockNumber.result, false]); 45 | return block.result.timestamp; 46 | } 47 | 48 | function sendRPC(web3_, method, params) { 49 | return new Promise((resolve, reject) => { 50 | if (!web3_.currentProvider || typeof (web3_.currentProvider) === 'string') { 51 | return reject(`cannot send from currentProvider=${web3_.currentProvider}`); 52 | } 53 | 54 | web3_.currentProvider.send( 55 | { 56 | jsonrpc: '2.0', 57 | method: method, 58 | params: params, 59 | id: new Date().getTime() // Id of the request; anything works, really 60 | }, 61 | (err, response) => { 62 | if (err) { 63 | reject(err); 64 | } else { 65 | resolve(response); 66 | } 67 | } 68 | ); 69 | }); 70 | } 71 | 72 | module.exports = { 73 | sendRPC, 74 | address, 75 | bytes, 76 | time, 77 | numToBigNum, 78 | numToHex, 79 | uint256, 80 | uint, 81 | keccak256, 82 | currentBlockTimestamp, 83 | fixed 84 | }; 85 | -------------------------------------------------------------------------------- /tests/Matchers.js: -------------------------------------------------------------------------------- 1 | const BigNumber = require("bignumber.js"); 2 | 3 | expect.extend({ 4 | addrEquals(actual, expected) { 5 | return { 6 | pass: actual.toLowerCase() == expected.toLowerCase(), 7 | message: () => `expected (${actual}) == (${expected})` 8 | } 9 | }, 10 | 11 | numEquals(actual, expected) { 12 | return { 13 | pass: actual.toString() == expected.toString(), 14 | message: () => `expected (${actual.toString()}) == (${expected.toString()})` 15 | } 16 | } 17 | }); 18 | 19 | expect.extend({ 20 | greaterThan(actual, expected) { 21 | return { 22 | pass: (new BigNumber(actual)).gt(new BigNumber(expected)), 23 | message: () => `expected ${actual.toString()} to be greater than ${expected.toString()}` 24 | } 25 | } 26 | }); 27 | 28 | expect.extend({ 29 | toRevert(actual, msg='revert') { 30 | return { 31 | pass: !!actual['message'] && actual.message === `VM Exception while processing transaction: ${msg}`, 32 | message: () => `expected revert, got: ${actual && actual.message ? actual : JSON.stringify(actual)}` 33 | } 34 | } 35 | }); 36 | 37 | expect.extend({ 38 | toBeWithinRange(received, floor, ceiling) { 39 | const pass = received >= floor && received <= ceiling; 40 | if (pass) { 41 | return { 42 | message: () => 43 | `expected ${received} not to be within range ${floor} - ${ceiling}`, 44 | pass: true, 45 | }; 46 | } else { 47 | return { 48 | message: () => 49 | `expected ${received} to be within range ${floor} - ${ceiling}`, 50 | pass: false, 51 | }; 52 | } 53 | }, 54 | }); 55 | -------------------------------------------------------------------------------- /tests/NonReporterPricesTest.js: -------------------------------------------------------------------------------- 1 | const { sendRPC } = require('./Helpers'); 2 | 3 | function address(n) { 4 | return `0x${n.toString(16).padStart(40, '0')}`; 5 | } 6 | 7 | function keccak256(str) { 8 | return web3.utils.keccak256(str); 9 | } 10 | 11 | function uint(n) { 12 | return web3.utils.toBN(n).toString(); 13 | } 14 | 15 | const PriceSource = { 16 | FIXED_ETH: 0, 17 | FIXED_USD: 1, 18 | REPORTER: 2 19 | }; 20 | 21 | describe('UniswapAnchoredView', () => { 22 | it('handles fixed_usd prices', async () => { 23 | const USDC = {cToken: address(1), underlying: address(2), symbolHash: keccak256("USDC"), baseUnit: uint(1e6), priceSource: PriceSource.FIXED_USD, fixedPrice: uint(1e6), uniswapMarket: address(0), isUniswapReversed: false}; 24 | const USDT = {cToken: address(3), underlying: address(4), symbolHash: keccak256("USDT"), baseUnit: uint(1e6), priceSource: PriceSource.FIXED_USD, fixedPrice: uint(1e6), uniswapMarket: address(0), isUniswapReversed: false}; 25 | const priceData = await deploy("OpenOraclePriceData", []); 26 | const oracle = await deploy('UniswapAnchoredView', [priceData._address, address(0), 0, 0, [USDC, USDT]]); 27 | expect(await call(oracle, 'price', ["USDC"])).numEquals(1e6); 28 | expect(await call(oracle, 'price', ["USDT"])).numEquals(1e6); 29 | }); 30 | 31 | it('reverts fixed_eth prices if no ETH price', async () => { 32 | const SAI = {cToken: address(5), underlying: address(6), symbolHash: keccak256("SAI"), baseUnit: uint(1e18), priceSource: PriceSource.FIXED_ETH, fixedPrice: uint(5285551943761727), uniswapMarket: address(0), isUniswapReversed: false}; 33 | const priceData = await deploy("OpenOraclePriceData", []); 34 | const oracle = await deploy('UniswapAnchoredView', [priceData._address, address(0), 0, 0, [SAI]]); 35 | expect(call(oracle, 'price', ["SAI"])).rejects.toRevert('revert ETH price not set, cannot convert to dollars'); 36 | }); 37 | 38 | it('reverts if ETH has no uniswap market', async () => { 39 | if (!coverage) { 40 | // This test for some reason is breaking coverage in CI, skip for now 41 | const ETH = {cToken: address(5), underlying: address(6), symbolHash: keccak256("ETH"), baseUnit: uint(1e18), priceSource: PriceSource.REPORTER, fixedPrice: 0, uniswapMarket: address(0), isUniswapReversed: true}; 42 | const SAI = {cToken: address(5), underlying: address(6), symbolHash: keccak256("SAI"), baseUnit: uint(1e18), priceSource: PriceSource.FIXED_ETH, fixedPrice: uint(5285551943761727), uniswapMarket: address(0), isUniswapReversed: false}; 43 | const priceData = await deploy("OpenOraclePriceData", []); 44 | expect(deploy('UniswapAnchoredView', [priceData._address, address(0), 0, 0, [ETH, SAI]])).rejects.toRevert('revert reported prices must have an anchor'); 45 | } 46 | }); 47 | 48 | it('reverts if non-reporter has a uniswap market', async () => { 49 | if (!coverage) { 50 | const ETH = {cToken: address(5), underlying: address(6), symbolHash: keccak256("ETH"), baseUnit: uint(1e18), priceSource: PriceSource.FIXED_ETH, fixedPrice: 14, uniswapMarket: address(112), isUniswapReversed: true}; 51 | const SAI = {cToken: address(5), underlying: address(6), symbolHash: keccak256("SAI"), baseUnit: uint(1e18), priceSource: PriceSource.FIXED_ETH, fixedPrice: uint(5285551943761727), uniswapMarket: address(0), isUniswapReversed: false}; 52 | const priceData = await deploy("OpenOraclePriceData", []); 53 | expect(deploy('UniswapAnchoredView', [priceData._address, address(0), 0, 0, [ETH, SAI]])).rejects.toRevert('revert only reported prices utilize an anchor'); 54 | } 55 | }); 56 | 57 | it('handles fixed_eth prices', async () => { 58 | if (!coverage) { 59 | const usdc_eth_pair = await deploy("MockUniswapTokenPair", [ 60 | "1865335786147", 61 | "8202340665419053945756", 62 | "1593755855", 63 | "119785032308978310142960133641565753500432674230537", 64 | "5820053774558372823476814618189", 65 | ]); 66 | const reporter = "0xfCEAdAFab14d46e20144F48824d0C09B1a03F2BC"; 67 | const messages = ["0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000005efebe9800000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000d84ec180000000000000000000000000000000000000000000000000000000000000006707269636573000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000034554480000000000000000000000000000000000000000000000000000000000"]; 68 | const signatures = ["0xb8ba87c37228468f9d107a97eeb92ebd49a50993669cab1737fea77e5b884f2591affbf4058bcfa29e38756021deeafaeeab7a5c4f5ce584c7d1e12346c88d4e000000000000000000000000000000000000000000000000000000000000001b"]; 69 | const ETH = {cToken: address(5), underlying: address(6), symbolHash: keccak256("ETH"), baseUnit: uint(1e18), priceSource: PriceSource.REPORTER, fixedPrice: 0, uniswapMarket: usdc_eth_pair._address, isUniswapReversed: true}; 70 | const SAI = {cToken: address(7), underlying: address(8), symbolHash: keccak256("SAI"), baseUnit: uint(1e18), priceSource: PriceSource.FIXED_ETH, fixedPrice: uint(5285551943761727), uniswapMarket: address(0), isUniswapReversed: false}; 71 | const priceData = await deploy("OpenOraclePriceData", []); 72 | const oracle = await deploy('UniswapAnchoredView', [priceData._address, reporter, uint(20e16), 60, [ETH, SAI]]); 73 | await sendRPC(web3, 'evm_increaseTime', [30 * 60]); 74 | await send(oracle, "postPrices", [messages, signatures, ['ETH']]); 75 | expect(await call(oracle, 'price', ["ETH"])).numEquals(226815000); 76 | expect(await call(oracle, 'price', ["SAI"])).numEquals(1198842); 77 | } 78 | }); 79 | }); -------------------------------------------------------------------------------- /tests/OpenOracleDataTest.js: -------------------------------------------------------------------------------- 1 | const { address, bytes, numToHex, time } = require('./Helpers'); 2 | 3 | const { encode, sign } = require('../sdk/javascript/.tsbuilt/reporter'); 4 | 5 | describe('OpenOracleData', () => { 6 | let oracleData; 7 | let priceData; 8 | const privateKey = 9 | '0x177ee777e72b8c042e05ef41d1db0f17f1fcb0e8150b37cfad6993e4373bdf10'; 10 | const signer = '0x1826265c3156c3B9b9e751DC4635376F3CD6ee06'; 11 | 12 | beforeEach(async done => { 13 | oracleData = await deploy('OpenOracleData', []); 14 | priceData = await deploy('OpenOraclePriceData', []); 15 | done(); 16 | }); 17 | 18 | it('has correct default data', async () => { 19 | let { 0: timestamp, 1: value } = await call(priceData, 'get', [ 20 | address(0), 21 | 'ETH' 22 | ]); 23 | 24 | expect(timestamp).numEquals(0); 25 | expect(value).numEquals(0); 26 | }); 27 | 28 | it('source() should ecrecover correctly', async () => { 29 | const [{ message, signature }] = sign( 30 | encode('prices', time(), [['ETH', 700]]), 31 | privateKey 32 | ); 33 | await send(priceData, 'put', [message, signature], { 34 | gas: 1000000 35 | }); 36 | 37 | expect(await call(oracleData, 'source', [message, signature])).toEqual( 38 | signer 39 | ); 40 | expect( 41 | await call(oracleData, 'source', [bytes('bad'), signature]) 42 | ).not.toEqual(signer); 43 | await expect( 44 | call(oracleData, 'source', [message, bytes('0xbad')]) 45 | ).rejects.toRevert(); 46 | }); 47 | 48 | it('should save data from put()', async () => { 49 | const timestamp = time() - 1; 50 | const ethPrice = 700; 51 | const [{ message, signature }] = sign( 52 | encode('prices', timestamp, [['ETH', ethPrice]]), 53 | privateKey 54 | ); 55 | 56 | const putTx = await send(priceData, 'put', [message, signature], { 57 | gas: 1000000 58 | }); 59 | expect(putTx.gasUsed).toBeLessThan(86000); 60 | }); 61 | 62 | 63 | it('sending data from before previous checkpoint should fail', async () => { 64 | const timestamp = time() - 1; 65 | let [{ message, signature }] = sign( 66 | encode('prices', timestamp, [['ABC', 100]]), 67 | privateKey 68 | ); 69 | await send(priceData, 'put', [message, signature], { 70 | gas: 1000000 71 | }); 72 | 73 | const timestamp2 = timestamp - 1; 74 | const [{ message: message2, signature: signature2 }] = sign( 75 | encode('prices', timestamp2, [['ABC', 150]]), 76 | privateKey 77 | ); 78 | const putTx = await send(priceData, 'put', [message2, signature2], { 79 | gas: 1000000 80 | }); 81 | 82 | expect(putTx.events.NotWritten).not.toBe(undefined); 83 | 84 | ({ 0: signedTimestamp, 1: value } = await call(priceData, 'get', [ 85 | signer, 86 | 'ABC' 87 | ])); 88 | expect(value / 1e6).toBe(100); 89 | }); 90 | 91 | it('signing future timestamp should not write to storage', async () => { 92 | const timestamp = time() + 3601; 93 | const [{ message, signature }] = sign( 94 | encode('prices', timestamp, [['ABC', 100]]), 95 | privateKey 96 | ); 97 | const putTx = await send(priceData, 'put', [message, signature], { 98 | gas: 1000000 99 | }); 100 | expect(putTx.events.NotWritten).not.toBe(undefined); 101 | ({ 0: signedTimestamp, 1: value } = await call(priceData, 'get', [ 102 | signer, 103 | 'ABC' 104 | ])); 105 | expect(+value).toBe(0); 106 | }); 107 | 108 | it('two pairs with update', async () => { 109 | const timestamp = time() - 2; 110 | const signed = sign( 111 | encode('prices', timestamp, [['ABC', 100], ['BTC', 9000]]), 112 | privateKey 113 | ); 114 | 115 | for ({ message, signature } of signed) { 116 | await send(priceData, 'put', [message, signature], { 117 | gas: 1000000 118 | }); 119 | } 120 | 121 | ({ 0: signedTime, 1: value } = await call(priceData, 'get', [ 122 | signer, 123 | 'BTC' 124 | ])); 125 | expect(value / 1e6).numEquals(9000); 126 | 127 | ({ 0: signedTime, 1: value } = await call(priceData, 'get', [ 128 | signer, 129 | 'ABC' 130 | ])); 131 | expect(value / 1e6).numEquals(100); 132 | 133 | //2nd tx 134 | const later = timestamp + 1; 135 | 136 | const signed2 = sign( 137 | encode('prices', later, [['ABC', 101], ['BTC', 9001]]), 138 | privateKey 139 | ); 140 | 141 | for ({ message, signature } of signed2) { 142 | const wrote2b = await send(priceData, 'put', [message, signature], { 143 | gas: 1000000 144 | }); 145 | expect(wrote2b.gasUsed).toBeLessThan(75000); 146 | } 147 | 148 | ({ 0: signedTime, 1: value } = await call(priceData, 'get', [ 149 | signer, 150 | 'BTC' 151 | ])); 152 | expect(value / 1e6).numEquals(9001); 153 | 154 | ({ 0: signedTime, 1: value } = await call(priceData, 'get', [ 155 | signer, 156 | 'ABC' 157 | ])); 158 | expect(value / 1e6).numEquals(101); 159 | }); 160 | }); 161 | -------------------------------------------------------------------------------- /tests/OpenOracleViewTest.js: -------------------------------------------------------------------------------- 1 | 2 | describe('OpenOracleView', () => { 3 | it('view with no sources is invalid', async () => { 4 | const oracleData = await saddle.deploy('OpenOracleData', []); 5 | await expect(saddle.deploy('OpenOracleView', [oracleData._address, []])).rejects.toRevert('revert Must initialize with sources'); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /tests/UniswapAnchoredViewTest.js: -------------------------------------------------------------------------------- 1 | const { encode, sign, encodeRotationMessage } = require('../sdk/javascript/.tsbuilt/reporter'); 2 | const { uint, keccak256, time, numToHex, address, sendRPC, currentBlockTimestamp, fixed } = require('./Helpers'); 3 | const BigNumber = require('bignumber.js'); 4 | 5 | const PriceSource = { 6 | FIXED_ETH: 0, 7 | FIXED_USD: 1, 8 | REPORTER: 2 9 | }; 10 | const reporterPrivateKey = '0x177ee777e72b8c042e05ef41d1db0f17f1fcb0e8150b37cfad6993e4373bdf10'; 11 | const FIXED_ETH_AMOUNT = 0.005e18; 12 | 13 | async function setup({isMockedView, freeze}) { 14 | const reporter = 15 | web3.eth.accounts.privateKeyToAccount(reporterPrivateKey); 16 | const anchorMantissa = numToHex(1e17); 17 | const priceData = await deploy('OpenOraclePriceData', []); 18 | const anchorPeriod = 60; 19 | const timestamp = Math.floor(Date.now() / 1000); 20 | 21 | if (freeze) { 22 | await sendRPC(web3, 'evm_freezeTime', [timestamp]); 23 | } else { 24 | await sendRPC(web3, 'evm_mine', [timestamp]); 25 | } 26 | 27 | const mockPair = await deploy("MockUniswapTokenPair", [ 28 | fixed(1.8e12), 29 | fixed(8.2e21), 30 | fixed(1.6e9), 31 | fixed(1.19e50), 32 | fixed(5.8e30), 33 | ]); 34 | 35 | // Initialize REP pair with values from mainnet 36 | const mockRepPair = await deploy("MockUniswapTokenPair", [ 37 | fixed(4e22), 38 | fixed(3e21), 39 | fixed(1.6e9), 40 | fixed(1.32e39), 41 | fixed(3.15e41), 42 | ]); 43 | 44 | const cToken = {ETH: address(1), DAI: address(2), REP: address(3), USDT: address(4), SAI: address(5), WBTC: address(6)}; 45 | const dummyAddress = address(0); 46 | const tokenConfigs = [ 47 | {cToken: cToken.ETH, underlying: dummyAddress, symbolHash: keccak256('ETH'), baseUnit: uint(1e18), priceSource: PriceSource.REPORTER, fixedPrice: 0, uniswapMarket: mockPair._address, isUniswapReversed: true}, 48 | {cToken: cToken.DAI, underlying: dummyAddress, symbolHash: keccak256('DAI'), baseUnit: uint(1e18), priceSource: PriceSource.REPORTER, fixedPrice: 0, uniswapMarket: mockPair._address, isUniswapReversed: false}, 49 | {cToken: cToken.REP, underlying: dummyAddress, symbolHash: keccak256('REP'), baseUnit: uint(1e18), priceSource: PriceSource.REPORTER, fixedPrice: 0, uniswapMarket: mockRepPair._address, isUniswapReversed: false}, 50 | {cToken: cToken.USDT, underlying: dummyAddress, symbolHash: keccak256('USDT'), baseUnit: uint(1e6), priceSource: PriceSource.FIXED_USD, fixedPrice: uint(1e6), uniswapMarket: address(0), isUniswapReversed: false}, 51 | {cToken: cToken.SAI, underlying: dummyAddress, symbolHash: keccak256('SAI'), baseUnit: uint(1e18), priceSource: PriceSource.FIXED_ETH, fixedPrice: uint(FIXED_ETH_AMOUNT), uniswapMarket: address(0), isUniswapReversed: false}, 52 | {cToken: cToken.WBTC, underlying: dummyAddress, symbolHash: keccak256('BTC'), baseUnit: uint(1e8), priceSource: PriceSource.REPORTER, fixedPrice: 0, uniswapMarket: mockPair._address, isUniswapReversed: false}, 53 | ]; 54 | 55 | let uniswapAnchoredView; 56 | if (isMockedView) { 57 | uniswapAnchoredView = await deploy('MockUniswapAnchoredView', [priceData._address, reporter.address, anchorMantissa, anchorPeriod, tokenConfigs]); 58 | } else { 59 | uniswapAnchoredView = await deploy('UniswapAnchoredView', [priceData._address, reporter.address, anchorMantissa, anchorPeriod, tokenConfigs]); 60 | } 61 | 62 | async function postPrices(timestamp, prices2dArr, symbols, signer=reporter) { 63 | let { 64 | messages, 65 | signatures 66 | } = prices2dArr.reduce(({messages, signatures}, prices, i) => { 67 | const signedMessages = sign( 68 | encode( 69 | 'prices', 70 | timestamp, 71 | prices 72 | ), 73 | signer.privateKey 74 | ); 75 | 76 | return signedMessages.reduce(({messages, signatures}, {message, signature}) => { 77 | return { 78 | messages: [...messages, message], 79 | signatures: [...signatures, signature], 80 | }; 81 | }, {messages, signatures}); 82 | }, { messages: [], signatures: [] }); 83 | 84 | return send(uniswapAnchoredView, 'postPrices', [messages, signatures, symbols]); 85 | } 86 | 87 | return { 88 | anchorMantissa, 89 | anchorPeriod, 90 | cToken, 91 | mockPair, 92 | postPrices, 93 | priceData, 94 | reporter, 95 | timestamp, 96 | tokenConfigs, 97 | uniswapAnchoredView, 98 | }; 99 | } 100 | 101 | describe('UniswapAnchoredView', () => { 102 | let cToken; 103 | let reporter; 104 | let anchorMantissa; 105 | let priceData; 106 | let anchorPeriod; 107 | let uniswapAnchoredView; 108 | let tokenConfigs; 109 | let postPrices; 110 | let mockPair; 111 | let timestamp; 112 | 113 | describe('postPrices', () => { 114 | beforeEach(async () => { 115 | ({ 116 | anchorMantissa, 117 | postPrices, 118 | priceData, 119 | reporter, 120 | uniswapAnchoredView, 121 | } = await setup({isMockedView: true})); 122 | }); 123 | 124 | it('should not update view if sender is not reporter', async () => { 125 | const timestamp = time() - 5; 126 | const nonSource = web3.eth.accounts.privateKeyToAccount('0x666ee777e72b8c042e05ef41d1db0f17f1fcb0e8150b37cfad6993e4373bdf10'); 127 | await send(uniswapAnchoredView, 'setAnchorPrice', ['ETH', 91e6]); 128 | await postPrices(timestamp, [[['ETH', 91]]], ['ETH'], reporter); 129 | 130 | const tx = await postPrices(timestamp, [[['ETH', 95]]], ['ETH'], nonSource); 131 | expect(tx.events.PriceGuarded).toBe(undefined); 132 | expect(tx.events.PricePosted).toBe(undefined); 133 | expect(await call(uniswapAnchoredView, 'prices', [keccak256('ETH')])).numEquals(91e6); 134 | }); 135 | 136 | it('should update view if ETH price is within anchor bounds', async () => { 137 | const timestamp = time() - 5; 138 | await send(uniswapAnchoredView, 'setAnchorPrice', ['ETH', 91e6]); 139 | const tx = await postPrices(timestamp, [[['ETH', 91]]], ['ETH']); 140 | 141 | expect(tx.events.PriceGuarded).toBe(undefined); 142 | expect(tx.events.PriceUpdated.returnValues.price).numEquals(91e6); 143 | expect(tx.events.PriceUpdated.returnValues.symbol).toBe('ETH'); 144 | expect(await call(uniswapAnchoredView, 'prices', [keccak256('ETH')])).numEquals(91e6); 145 | expect(await call(priceData, 'getPrice', [reporter.address, 'ETH'])).numEquals(91e6); 146 | }); 147 | 148 | it('should update view if ERC20 price is within anchor bounds', async () => { 149 | const timestamp = time() - 5; 150 | await send(uniswapAnchoredView, 'setAnchorPrice', ['REP', 17e6]); 151 | const tx = await postPrices(timestamp, [[['REP', 17]]], ['REP']); 152 | 153 | expect(tx.events.PriceGuarded).toBe(undefined); 154 | expect(tx.events.PriceUpdated.returnValues.price).numEquals(17e6); 155 | expect(tx.events.PriceUpdated.returnValues.symbol).toBe('REP'); 156 | expect(await call(uniswapAnchoredView, 'prices', [keccak256('REP')])).numEquals(17e6); 157 | expect(await call(priceData, 'getPrice', [reporter.address, 'REP'])).numEquals(17e6); 158 | }); 159 | 160 | it('should not update view if ETH price is below anchor bounds', async () => { 161 | // anchorMantissa is 1e17, so 10% tolerance 162 | const timestamp = time() - 5; 163 | await send(uniswapAnchoredView, 'setAnchorPrice', ['ETH', 89.9e6]); 164 | const tx = await postPrices(timestamp, [[['ETH', 100]]], ['ETH']); 165 | 166 | expect(tx.events.PriceGuarded.returnValues.symbol).toBe('ETH'); 167 | expect(tx.events.PriceGuarded.returnValues.reporter).numEquals(100e6); 168 | expect(tx.events.PriceGuarded.returnValues.anchor).numEquals(89.9e6); 169 | expect(tx.events.PriceUpdated).toBe(undefined); 170 | expect(await call(uniswapAnchoredView, 'prices', [keccak256('ETH')])).numEquals(0); 171 | expect(await call(priceData, 'getPrice', [reporter.address, 'ETH'])).numEquals(100e6); 172 | }); 173 | 174 | it('should not update view if ERC20 price is below anchor bounds', async () => { 175 | const timestamp = time() - 5; 176 | // anchorMantissa is 1e17, so 10% tolerance 177 | await send(uniswapAnchoredView, 'setAnchorPrice', ['REP', 15e6]); 178 | const tx = await postPrices(timestamp, [[['REP', 17]]], ['REP']); 179 | 180 | expect(tx.events.PriceGuarded.returnValues.reporter).numEquals(17e6); 181 | expect(tx.events.PriceGuarded.returnValues.anchor).numEquals(15e6); 182 | expect(await call(uniswapAnchoredView, 'prices', [keccak256('REP')])).numEquals(0); 183 | expect(await call(priceData, 'getPrice', [reporter.address, 'REP'])).numEquals(17e6); 184 | }); 185 | 186 | it('should not update view if ETH price is above anchor bounds', async () => { 187 | // anchorMantissa is 1e17, so 10% tolerance 188 | const timestamp = time() - 5; 189 | await send(uniswapAnchoredView, 'setAnchorPrice', ['ETH', 110.1e6]); 190 | const tx = await postPrices(timestamp, [[['ETH', 100]]], ['ETH']); 191 | 192 | expect(tx.events.PriceGuarded.returnValues.reporter).numEquals(100e6); 193 | expect(tx.events.PriceGuarded.returnValues.anchor).numEquals(110.1e6); 194 | expect(tx.events.PriceUpdated).toBe(undefined); 195 | expect(await call(uniswapAnchoredView, 'prices', [keccak256('ETH')])).numEquals(0); 196 | expect(await call(priceData, 'getPrice', [reporter.address, 'ETH'])).numEquals(100e6); 197 | }); 198 | 199 | it('should not update view if ERC20 price is above anchor bounds', async () => { 200 | const timestamp = time() - 5; 201 | // anchorMantissa is 1e17, so 10% tolerance 202 | await send(uniswapAnchoredView, 'setAnchorPrice', ['REP', 19e6]); 203 | const tx = await postPrices(timestamp, [[['REP', 17]]], ['REP']); 204 | 205 | expect(tx.events.PriceGuarded.returnValues.reporter).numEquals(17e6); 206 | expect(tx.events.PriceGuarded.returnValues.anchor).numEquals(19e6); 207 | expect(await call(uniswapAnchoredView, 'prices', [keccak256('REP')])).numEquals(0); 208 | expect(await call(priceData, 'getPrice', [reporter.address, 'REP'])).numEquals(17e6); 209 | }); 210 | 211 | it('should revert on posting arrays of messages and signatures with different lengths', async () => { 212 | await expect( 213 | send(uniswapAnchoredView, 'postPrices', [['0xabc'], ['0x123', '0x123'], []]) 214 | ).rejects.toRevert("revert messages and signatures must be 1:1"); 215 | 216 | await expect( 217 | send(uniswapAnchoredView, 'postPrices', [['0xabc', '0xabc'], ['0x123'], []]) 218 | ).rejects.toRevert("revert messages and signatures must be 1:1"); 219 | }); 220 | 221 | it('should revert on posting arrays with invalid symbols', async () => { 222 | const timestamp = time() - 5; 223 | await send(uniswapAnchoredView, 'setAnchorPrice', ['ETH', 91e6]); 224 | 225 | await expect( 226 | postPrices(timestamp, [[['ETH', 91]]], ['HOHO']) 227 | ).rejects.toRevert("revert token config not found"); 228 | 229 | await expect( 230 | postPrices(timestamp, [[['HOHO', 91]]], ['HOHO']) 231 | ).rejects.toRevert("revert token config not found"); 232 | 233 | await expect( 234 | postPrices(timestamp, [[['ETH', 91], ['WBTC', 1000]]], ['ETH', 'HOHO']) 235 | ).rejects.toRevert("revert token config not found"); 236 | }); 237 | 238 | it('should revert on posting arrays with invalid symbols', async () => { 239 | const timestamp = time() - 5; 240 | await send(uniswapAnchoredView, 'setAnchorPrice', ['ETH', 91e6]); 241 | 242 | await expect( 243 | postPrices(timestamp, [[['ETH', 91]]], ['HOHO']) 244 | ).rejects.toRevert("revert token config not found"); 245 | 246 | await expect( 247 | postPrices(timestamp, [[['HOHO', 91]]], ['HOHO']) 248 | ).rejects.toRevert("revert token config not found"); 249 | 250 | await expect( 251 | postPrices(timestamp, [[['ETH', 91], ['WBTC', 1000]]], ['ETH', 'HOHO']) 252 | ).rejects.toRevert("revert token config not found"); 253 | }); 254 | 255 | it("should revert on posting FIXED_USD prices", async () => { 256 | await expect( 257 | postPrices(time() - 5, [[['USDT', 1]]], ['USDT']) 258 | ).rejects.toRevert("revert only reporter prices get posted"); 259 | }); 260 | 261 | it("should revert on posting FIXED_ETH prices", async () => { 262 | await expect( 263 | postPrices(time() - 5, [[['SAI', 1]]], ['SAI']) 264 | ).rejects.toRevert("revert only reporter prices get posted"); 265 | }); 266 | }); 267 | 268 | describe('getUnderlyingPrice', () => { 269 | // everything must return 1e36 - underlying units 270 | 271 | beforeEach(async () => { 272 | ({ 273 | cToken, 274 | postPrices, 275 | uniswapAnchoredView, 276 | } = await setup({isMockedView: true})); 277 | }); 278 | 279 | it('should work correctly for USDT fixed USD price source', async () => { 280 | // 1 * (1e(36 - 6)) = 1e30 281 | let expected = new BigNumber('1e30'); 282 | expect(await call(uniswapAnchoredView, 'getUnderlyingPrice', [cToken.USDT])).numEquals(expected.toFixed()); 283 | }); 284 | 285 | it('should return fixed ETH amount if SAI', async () => { 286 | const timestamp = time() - 5; 287 | await send(uniswapAnchoredView, 'setAnchorPrice', ['ETH', 200e6]); 288 | const tx = await postPrices(timestamp, [[['ETH', 200]]], ['ETH']); 289 | // priceInternal: returns 200e6 * 0.005e18 / 1e18 = 1e6 290 | // getUnderlyingPrice: 1e30 * 1e6 / 1e18 = 1e18 291 | expect(await call(uniswapAnchoredView, 'getUnderlyingPrice', [cToken.SAI])).numEquals(1e18); 292 | }); 293 | 294 | it('should return reported ETH price', async () => { 295 | const timestamp = time() - 5; 296 | await send(uniswapAnchoredView, 'setAnchorPrice', ['ETH', 200e6]); 297 | const tx = await postPrices(timestamp, [[['ETH', 200]]], ['ETH']); 298 | // priceInternal: returns 200e6 299 | // getUnderlyingPrice: 1e30 * 200e6 / 1e18 = 200e18 300 | expect(await call(uniswapAnchoredView, 'getUnderlyingPrice', [cToken.ETH])).numEquals(200e18); 301 | }); 302 | 303 | it('should return reported WBTC price', async () => { 304 | const timestamp = time() - 5; 305 | await send(uniswapAnchoredView, 'setAnchorPrice', ['ETH', 200e6]); 306 | await send(uniswapAnchoredView, 'setAnchorPrice', ['BTC', 10000e6]); 307 | 308 | const tx = await postPrices(timestamp, [[['ETH', 200], ['BTC', 10000]]], ['ETH', 'BTC']); 309 | const btcPrice = await call(uniswapAnchoredView, 'prices', [keccak256('BTC')]); 310 | 311 | expect(btcPrice).numEquals(10000e6); 312 | // priceInternal: returns 10000e6 313 | // getUnderlyingPrice: 1e30 * 10000e6 / 1e8 = 1e32 314 | let expected = new BigNumber('1e32'); 315 | expect(await call(uniswapAnchoredView, 'getUnderlyingPrice', [cToken.WBTC])).numEquals(expected.toFixed()); 316 | }); 317 | 318 | }); 319 | 320 | describe('pokeWindowValues', () => { 321 | beforeEach(async () => { 322 | ({ 323 | mockPair, 324 | anchorPeriod, 325 | uniswapAnchoredView, 326 | postPrices, 327 | tokenConfigs, 328 | timestamp 329 | } = await setup({isMockedView: false, freeze: true})); 330 | }); 331 | 332 | it('should not update window values if not enough time elapsed', async () => { 333 | await sendRPC(web3, 'evm_freezeTime', [timestamp + anchorPeriod - 5]); 334 | const tx = await postPrices(timestamp, [[['ETH', 227]]], ['ETH']); 335 | expect(tx.events.UniswapWindowUpdated).toBe(undefined); 336 | }); 337 | 338 | it('should update window values if enough time elapsed', async () => { 339 | const ethHash = keccak256('ETH'); 340 | const mkt = mockPair._address;// ETH's mock market 341 | const newObs1 = await call(uniswapAnchoredView, 'newObservations', [ethHash]); 342 | const oldObs1 = await call(uniswapAnchoredView, 'oldObservations', [ethHash]); 343 | 344 | let timestampLater = timestamp + anchorPeriod; 345 | await sendRPC(web3, 'evm_freezeTime', [timestampLater]); 346 | 347 | const tx1 = await postPrices(timestampLater, [[['ETH', 227]]], ['ETH']); 348 | const updateEvent = tx1.events.AnchorPriceUpdated.returnValues; 349 | expect(updateEvent.newTimestamp).greaterThan(updateEvent.oldTimestamp); 350 | expect(tx1.events.PriceGuarded).toBe(undefined); 351 | 352 | // on the first update, we expect the new observation to change 353 | const newObs2 = await call(uniswapAnchoredView, 'newObservations', [ethHash]); 354 | const oldObs2 = await call(uniswapAnchoredView, 'oldObservations', [ethHash]); 355 | expect(newObs2.acc).greaterThan(newObs1.acc); 356 | expect(newObs2.timestamp).greaterThan(newObs1.timestamp); 357 | expect(oldObs2.acc).numEquals(oldObs1.acc); 358 | expect(oldObs2.timestamp).numEquals(oldObs1.timestamp); 359 | 360 | let timestampEvenLater = timestampLater + anchorPeriod; 361 | await sendRPC(web3, 'evm_freezeTime', [timestampEvenLater]); 362 | const tx2 = await postPrices(timestampEvenLater, [[['ETH', 201]]], ['ETH']); 363 | 364 | const windowUpdate = tx2.events.UniswapWindowUpdated.returnValues; 365 | expect(windowUpdate.symbolHash).toEqual(ethHash); 366 | expect(timestampEvenLater).greaterThan(windowUpdate.oldTimestamp); 367 | expect(windowUpdate.newPrice).greaterThan(windowUpdate.oldPrice);// accumulator should always go up 368 | 369 | // this time, both should change 370 | const newObs3 = await call(uniswapAnchoredView, 'newObservations', [ethHash]); 371 | const oldObs3 = await call(uniswapAnchoredView, 'oldObservations', [ethHash]); 372 | expect(newObs3.acc).greaterThan(newObs2.acc); 373 | expect(newObs3.acc).greaterThan(newObs2.timestamp); 374 | // old becomes last new 375 | expect(oldObs3.acc).numEquals(newObs2.acc); 376 | expect(oldObs3.timestamp).numEquals(newObs2.timestamp); 377 | 378 | const anchorPriceUpdated = tx2.events.AnchorPriceUpdated.returnValues; 379 | expect(anchorPriceUpdated.symbol).toBe("ETH"); 380 | expect(anchorPriceUpdated.newTimestamp).greaterThan(anchorPriceUpdated.oldTimestamp); 381 | expect(oldObs3.timestamp).toBe(anchorPriceUpdated.oldTimestamp); 382 | }); 383 | }) 384 | 385 | describe('constructor', () => { 386 | 387 | it('should prevent bounds from under/overflow', async () => { 388 | const reporter = web3.eth.accounts.privateKeyToAccount('0x177ee777e72b8c042e05ef41d1db0f17f1fcb0e8150b37cfad6993e4373bdf10'); 389 | const priceData = await deploy('OpenOraclePriceData', []); 390 | const anchorPeriod = 30, configs = []; 391 | const UINT256_MAX = (1n<<256n) - 1n, exp = (a, b) => BigInt(a) * 10n**BigInt(b); 392 | 393 | const anchorMantissa1 = exp(100, 16); 394 | const view1 = await deploy('UniswapAnchoredView', [priceData._address, reporter.address, anchorMantissa1, anchorPeriod, configs]); 395 | expect(await call(view1, 'upperBoundAnchorRatio')).numEquals(2e18); 396 | expect(await call(view1, 'lowerBoundAnchorRatio')).numEquals(1); 397 | 398 | const anchorMantissa2 = UINT256_MAX - exp(99, 16); 399 | const view2 = await deploy('UniswapAnchoredView', [priceData._address, reporter.address, anchorMantissa2, anchorPeriod, configs]); 400 | expect(await call(view2, 'upperBoundAnchorRatio')).numEquals(UINT256_MAX.toString()); 401 | expect(await call(view2, 'lowerBoundAnchorRatio')).numEquals(1); 402 | }); 403 | 404 | it('should fail if baseUnit == 0', async () => { 405 | const reporter = web3.eth.accounts.privateKeyToAccount('0x177ee777e72b8c042e05ef41d1db0f17f1fcb0e8150b37cfad6993e4373bdf10'); 406 | const priceData = await deploy('OpenOraclePriceData', []); 407 | const anchorMantissa = numToHex(1e17); 408 | 409 | const dummyAddress = address(0); 410 | const mockPair = await deploy("MockUniswapTokenPair", [ 411 | fixed(1.8e12), 412 | fixed(8.2e21), 413 | fixed(1.6e9), 414 | fixed(1.19e50), 415 | fixed(5.8e30), 416 | ]); 417 | const tokenConfigs = [ 418 | // Set dummy address as a uniswap market address 419 | {cToken: address(1), underlying: dummyAddress, symbolHash: keccak256('ETH'), baseUnit: uint(1e18), priceSource: PriceSource.REPORTER, fixedPrice: 0, uniswapMarket: mockPair._address, isUniswapReversed: true}, 420 | {cToken: address(2), underlying: dummyAddress, symbolHash: keccak256('DAI'), baseUnit: 0, priceSource: PriceSource.REPORTER, fixedPrice: 0, uniswapMarket: mockPair._address, isUniswapReversed: false}, 421 | {cToken: address(3), underlying: dummyAddress, symbolHash: keccak256('REP'), baseUnit: uint(1e18), priceSource: PriceSource.REPORTER, fixedPrice: 0, uniswapMarket: mockPair._address, isUniswapReversed: false}]; 422 | await expect( 423 | deploy('UniswapAnchoredView', [priceData._address, reporter.address, anchorMantissa, 30, tokenConfigs]) 424 | ).rejects.toRevert("revert baseUnit must be greater than zero"); 425 | }); 426 | 427 | it('should fail if uniswap market is not defined', async () => { 428 | const reporter = web3.eth.accounts.privateKeyToAccount('0x177ee777e72b8c042e05ef41d1db0f17f1fcb0e8150b37cfad6993e4373bdf10'); 429 | const priceData = await deploy('OpenOraclePriceData', []); 430 | const anchorMantissa = numToHex(1e17); 431 | 432 | const dummyAddress = address(0); 433 | const tokenConfigs = [ 434 | // Set dummy address as a uniswap market address 435 | {cToken: address(1), underlying: dummyAddress, symbolHash: keccak256('ETH'), baseUnit: uint(1e18), priceSource: PriceSource.REPORTER, fixedPrice: 0, uniswapMarket: dummyAddress, isUniswapReversed: true}, 436 | {cToken: address(2), underlying: dummyAddress, symbolHash: keccak256('DAI'), baseUnit: uint(1e18), priceSource: PriceSource.REPORTER, fixedPrice: 0, uniswapMarket: address(4), isUniswapReversed: false}, 437 | {cToken: address(3), underlying: dummyAddress, symbolHash: keccak256('REP'), baseUnit: uint(1e18), priceSource: PriceSource.REPORTER, fixedPrice: 0, uniswapMarket: address(5), isUniswapReversed: false}]; 438 | await expect( 439 | deploy('UniswapAnchoredView', [priceData._address, reporter.address, anchorMantissa, 30, tokenConfigs]) 440 | ).rejects.toRevert("revert reported prices must have an anchor"); 441 | }); 442 | 443 | it('should fail if non-reporter price utilizes an anchor', async () => { 444 | const reporter = web3.eth.accounts.privateKeyToAccount('0x177ee777e72b8c042e05ef41d1db0f17f1fcb0e8150b37cfad6993e4373bdf10'); 445 | const priceData = await deploy('OpenOraclePriceData', []); 446 | const anchorMantissa = numToHex(1e17); 447 | 448 | const dummyAddress = address(0); 449 | const tokenConfigs1 = [ 450 | {cToken: address(2), underlying: dummyAddress, symbolHash: keccak256('USDT'), baseUnit: uint(1e18), priceSource: PriceSource.FIXED_USD, fixedPrice: 0, uniswapMarket: address(5), isUniswapReversed: false}]; 451 | await expect( 452 | deploy('UniswapAnchoredView', [priceData._address, reporter.address, anchorMantissa, 30, tokenConfigs1]) 453 | ).rejects.toRevert("revert only reported prices utilize an anchor"); 454 | 455 | const tokenConfigs2 = [ 456 | {cToken: address(2), underlying: dummyAddress, symbolHash: keccak256('USDT'), baseUnit: uint(1e18), priceSource: PriceSource.FIXED_ETH, fixedPrice: 0, uniswapMarket: address(5), isUniswapReversed: false}]; 457 | await expect( 458 | deploy('UniswapAnchoredView', [priceData._address, reporter.address, anchorMantissa, 30, tokenConfigs2]) 459 | ).rejects.toRevert("revert only reported prices utilize an anchor"); 460 | }); 461 | 462 | it('basic scenario, successfully initialize observations initial state', async () => { 463 | ({reporter, anchorMantissa, priceData, anchorPeriod, uniswapAnchoredView, tokenConfigs, postPrices, cToken, mockPair} = await setup({isMockedView: true})); 464 | expect(await call(uniswapAnchoredView, 'reporter')).toBe(reporter.address); 465 | expect(await call(uniswapAnchoredView, 'anchorPeriod')).numEquals(anchorPeriod); 466 | expect(await call(uniswapAnchoredView, 'upperBoundAnchorRatio')).numEquals(new BigNumber(anchorMantissa).plus(1e18)); 467 | expect(await call(uniswapAnchoredView, 'lowerBoundAnchorRatio')).numEquals(new BigNumber(1e18).minus(anchorMantissa)); 468 | 469 | await Promise.all(tokenConfigs.map(async config => { 470 | const oldObservation = await call(uniswapAnchoredView, 'oldObservations', [config.uniswapMarket]); 471 | const newObservation = await call(uniswapAnchoredView, 'newObservations', [config.uniswapMarket]); 472 | expect(oldObservation.timestamp).numEquals(newObservation.timestamp); 473 | expect(oldObservation.acc).numEquals(newObservation.acc); 474 | if (config.priceSource != PriceSource.REPORTER) { 475 | expect(oldObservation.acc).numEquals(0); 476 | expect(newObservation.acc).numEquals(0); 477 | expect(oldObservation.timestamp).numEquals(0); 478 | expect(newObservation.timestamp).numEquals(0); 479 | } 480 | })) 481 | }); 482 | }) 483 | 484 | describe('invalidateReporter', () => { 485 | 486 | beforeEach(async done => { 487 | ({uniswapAnchoredView, postPrices} = await setup({isMockedView: true})); 488 | done(); 489 | }) 490 | 491 | it("reverts if given wrong message", async () => { 492 | const rotationTarget = '0xAbcdef0123456789000000000000000000000005'; 493 | const reporter = web3.eth.accounts.privateKeyToAccount('0x177ee777e72b8c042e05ef41d1db0f17f1fcb0e8150b37cfad6993e4373bdf10'); 494 | let encoded = web3.eth.abi.encodeParameters(['string', 'address'], ['stay still', rotationTarget]); 495 | const [ signed ] = sign(encoded, reporter.privateKey); 496 | 497 | await expect( 498 | send(uniswapAnchoredView, 'invalidateReporter', [encoded, signed.signature]) 499 | ).rejects.toRevert("revert invalid message must be 'rotate'"); 500 | }); 501 | 502 | it("reverts if given wrong signature", async () => { 503 | const rotationTarget = '0xAbcdef0123456789000000000000000000000005'; 504 | let encoded = encodeRotationMessage(rotationTarget); 505 | // sign rotation message with wrong key 506 | const [ signed ] = sign(encoded, '0x666ee777e72b8c042e05ef41d1db0f17f1fcb0e8150b37cfad6993e4373bdf10'); 507 | 508 | await expect( 509 | send(uniswapAnchoredView, 'invalidateReporter', [encoded, signed.signature]) 510 | ).rejects.toRevert("revert invalidation message must come from the reporter"); 511 | }); 512 | 513 | it("basic scenario, sets reporterInvalidated and emits ReporterInvalidated event", async () => { 514 | const rotationTarget = '0xAbcdef0123456789000000000000000000000005'; 515 | const reporter = web3.eth.accounts.privateKeyToAccount('0x177ee777e72b8c042e05ef41d1db0f17f1fcb0e8150b37cfad6993e4373bdf10'); 516 | let encoded = web3.eth.abi.encodeParameters(['string', 'address'], ['rotate', rotationTarget]); 517 | const [ signed ] = sign(encoded, reporter.privateKey); 518 | 519 | // Check that reporterInvalidated variable is properly set 520 | expect(await call(uniswapAnchoredView, 'reporterInvalidated')).toBe(false); 521 | const tx = await send(uniswapAnchoredView, 'invalidateReporter', [encoded, signed.signature]); 522 | expect(await call(uniswapAnchoredView, 'reporterInvalidated')).toBe(true); 523 | 524 | // Check that event is emitted 525 | expect(tx.events.ReporterInvalidated).not.toBe(undefined); 526 | expect(tx.events.ReporterInvalidated.returnValues.reporter).toBe(reporter.address); 527 | }); 528 | 529 | it("basic scenario, return anchor price after reporter is invalidated", async () => { 530 | const timestamp = time() - 5; 531 | await send(uniswapAnchoredView, 'setAnchorPrice', ['ETH', 200e6]); 532 | await send(uniswapAnchoredView, 'setAnchorPrice', ['BTC', 10000e6]); 533 | 534 | await postPrices(timestamp, [[['ETH', 201], ['BTC', 10001]]], ['ETH', 'BTC']); 535 | 536 | // Check that prices = posted prices 537 | const wbtcPrice1 = await call(uniswapAnchoredView, 'prices', [keccak256('BTC')]); 538 | const ethPrice1 = await call(uniswapAnchoredView, 'prices', [keccak256('ETH')]); 539 | expect(wbtcPrice1).numEquals(10001e6); 540 | expect(ethPrice1).numEquals(201e6); 541 | 542 | const rotationTarget = '0xAbcdef0123456789000000000000000000000005'; 543 | const reporter = web3.eth.accounts.privateKeyToAccount('0x177ee777e72b8c042e05ef41d1db0f17f1fcb0e8150b37cfad6993e4373bdf10'); 544 | let encoded = web3.eth.abi.encodeParameters(['string', 'address'], ['rotate', rotationTarget]); 545 | const [ signed ] = sign(encoded, reporter.privateKey); 546 | 547 | await send(uniswapAnchoredView, 'invalidateReporter', [encoded, signed.signature]); 548 | await postPrices(timestamp, [[['ETH', 201], ['BTC', 10001]]], ['ETH', 'BTC']); 549 | 550 | // Check that prices = anchor prices 551 | const wbtcPrice2 = await call(uniswapAnchoredView, 'prices', [keccak256('BTC')]); 552 | const ethPrice2 = await call(uniswapAnchoredView, 'prices', [keccak256('ETH')]); 553 | expect(wbtcPrice2).numEquals(10000e6); 554 | expect(ethPrice2).numEquals(200e6); 555 | }); 556 | }) 557 | }); 558 | -------------------------------------------------------------------------------- /tests/UniswapConfigTest.js: -------------------------------------------------------------------------------- 1 | function address(n) { 2 | return `0x${n.toString(16).padStart(40, '0')}`; 3 | } 4 | 5 | function keccak256(str) { 6 | return web3.utils.keccak256(str); 7 | } 8 | 9 | function uint(n) { 10 | return web3.utils.toBN(n).toString(); 11 | } 12 | 13 | describe('UniswapConfig', () => { 14 | it('basically works', async () => { 15 | const unlistedButUnderlying = await deploy('MockCToken', [address(4)]) 16 | const unlistedNorUnderlying = await deploy('MockCToken', [address(5)]) 17 | const contract = await deploy('UniswapConfig', [[ 18 | {cToken: address(1), underlying: address(0), symbolHash: keccak256('ETH'), baseUnit: uint(1e18), priceSource: 0, fixedPrice: 0, uniswapMarket: address(6), isUniswapReversed: false}, 19 | {cToken: address(2), underlying: address(3), symbolHash: keccak256('BTC'), baseUnit: uint(1e18), priceSource: 1, fixedPrice: 1, uniswapMarket: address(7), isUniswapReversed: true}, 20 | {cToken: unlistedButUnderlying._address, underlying: address(4), symbolHash: keccak256('REP'), baseUnit: uint(1e18), priceSource: 1, fixedPrice: 1, uniswapMarket: address(7), isUniswapReversed: true} 21 | ]]); 22 | 23 | const cfg0 = await call(contract, 'getTokenConfig', [0]); 24 | const cfg1 = await call(contract, 'getTokenConfig', [1]); 25 | const cfg2 = await call(contract, 'getTokenConfig', [2]); 26 | const cfgETH = await call(contract, 'getTokenConfigBySymbol', ['ETH']); 27 | const cfgBTC = await call(contract, 'getTokenConfigBySymbol', ['BTC']); 28 | const cfgCT0 = await call(contract, 'getTokenConfigByCToken', [address(1)]); 29 | const cfgCT1 = await call(contract, 'getTokenConfigByCToken', [address(2)]); 30 | const cfgU2 = await call(contract, 'getTokenConfigByCToken', [unlistedButUnderlying._address]) 31 | expect(cfg0).toEqual(cfgETH); 32 | expect(cfgETH).toEqual(cfgCT0); 33 | expect(cfg1).toEqual(cfgBTC); 34 | expect(cfgBTC).toEqual(cfgCT1); 35 | expect(cfg0).not.toEqual(cfg1); 36 | expect(cfgU2).toEqual(cfg2); 37 | 38 | await expect(call(contract, 'getTokenConfig', [3])).rejects.toRevert('revert token config not found'); 39 | await expect(call(contract, 'getTokenConfigBySymbol', ['COMP'])).rejects.toRevert('revert token config not found'); 40 | await expect(call(contract, 'getTokenConfigByCToken', [address(3)])).rejects.toRevert('revert'); // not a ctoken 41 | await expect(call(contract, 'getTokenConfigByCToken', [unlistedNorUnderlying._address])).rejects.toRevert('revert token config not found'); 42 | }); 43 | 44 | it('returns configs exactly as specified', async () => { 45 | const symbols = Array(30).fill(0).map((_, i) => String.fromCharCode('a'.charCodeAt(0) + i)); 46 | const configs = symbols.map((symbol, i) => { 47 | return {cToken: address(i + 1), underlying: address(i), symbolHash: keccak256(symbol), baseUnit: uint(1e6), priceSource: 0, fixedPrice: 1, uniswapMarket: address(i + 50), isUniswapReversed: i % 2 == 0} 48 | }); 49 | const contract = await deploy('UniswapConfig', [configs]); 50 | 51 | await Promise.all(configs.map(async (config, i) => { 52 | const cfgByIndex = await call(contract, 'getTokenConfig', [i]); 53 | const cfgBySymbol = await call(contract, 'getTokenConfigBySymbol', [symbols[i]]); 54 | const cfgByCToken = await call(contract, 'getTokenConfigByCToken', [address(i + 1)]); 55 | const cfgByUnderlying = await call(contract, 'getTokenConfigByUnderlying', [address(i)]); 56 | expect({ 57 | cToken: cfgByIndex.cToken.toLowerCase(), 58 | underlying: cfgByIndex.underlying.toLowerCase(), 59 | symbolHash: cfgByIndex.symbolHash, 60 | baseUnit: cfgByIndex.baseUnit, 61 | priceSource: cfgByIndex.priceSource, 62 | fixedPrice: cfgByIndex.fixedPrice, 63 | uniswapMarket: cfgByIndex.uniswapMarket.toLowerCase(), 64 | isUniswapReversed: cfgByIndex.isUniswapReversed 65 | }).toEqual({ 66 | cToken: config.cToken, 67 | underlying: config.underlying, 68 | symbolHash: config.symbolHash, 69 | baseUnit: `${config.baseUnit}`, 70 | priceSource: `${config.priceSource}`, 71 | fixedPrice: `${config.fixedPrice}`, 72 | uniswapMarket: config.uniswapMarket, 73 | isUniswapReversed: config.isUniswapReversed 74 | }); 75 | expect(cfgByIndex).toEqual(cfgBySymbol); 76 | expect(cfgBySymbol).toEqual(cfgByCToken); 77 | expect(cfgByUnderlying).toEqual(cfgBySymbol); 78 | })); 79 | }); 80 | 81 | it('checks gas', async () => { 82 | const configs = Array(26).fill(0).map((_, i) => { 83 | const symbol = String.fromCharCode('a'.charCodeAt(0) + i); 84 | return { 85 | cToken: address(i), 86 | underlying: address(i + 1), 87 | symbolHash: keccak256(symbol), 88 | baseUnit: uint(1e6), 89 | priceSource: 0, 90 | fixedPrice: 1, 91 | uniswapMarket: address(i + 50), 92 | isUniswapReversed: i % 2 == 0} 93 | }); 94 | const contract = await deploy('UniswapConfig', [configs]); 95 | 96 | const cfg9 = await call(contract, 'getTokenConfig', [9]); 97 | const tx9 = await send(contract, 'getTokenConfig', [9]); 98 | expect(cfg9.underlying).addrEquals(address(10)); 99 | expect(tx9.gasUsed).toEqual(22619); 100 | 101 | const cfg25 = await call(contract, 'getTokenConfig', [25]); 102 | const tx25 = await send(contract, 'getTokenConfig', [25]); 103 | expect(cfg25.underlying).addrEquals(address(26)); 104 | expect(tx25.gasUsed).toEqual(23035); 105 | 106 | const cfgZ = await call(contract, 'getTokenConfigBySymbol', ['z']); 107 | const txZ = await send(contract, 'getTokenConfigBySymbol', ['z']); 108 | expect(cfgZ.cToken).addrEquals(address(25)); 109 | expect(cfgZ.underlying).addrEquals(address(26)); 110 | expect(txZ.gasUsed).toEqual(25273); 111 | 112 | const cfgCT26 = await call(contract, 'getTokenConfigByCToken', [address(25)]); 113 | const txCT26 = await send(contract, 'getTokenConfigByCToken', [address(25)]); 114 | expect(cfgCT26.cToken).addrEquals(address(25)); 115 | expect(cfgCT26.underlying).addrEquals(address(26)); 116 | expect(txCT26.gasUsed).toEqual(25136); 117 | 118 | const cfgU26 = await call(contract, 'getTokenConfigByUnderlying', [address(26)]); 119 | const txU26 = await send(contract, 'getTokenConfigByUnderlying', [address(26)]); 120 | expect(cfgU26.cToken).addrEquals(address(25)); 121 | expect(cfgU26.underlying).addrEquals(address(26)); 122 | expect(txU26.gasUsed).toEqual(25137); 123 | }); 124 | }); -------------------------------------------------------------------------------- /tests/contracts/MockUniswapAnchoredView.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity ^0.6.10; 4 | pragma experimental ABIEncoderV2; 5 | 6 | import "../../contracts/Uniswap/UniswapAnchoredView.sol"; 7 | 8 | contract MockUniswapAnchoredView is UniswapAnchoredView { 9 | mapping(bytes32 => uint) public anchorPrices; 10 | 11 | constructor(OpenOraclePriceData priceData_, 12 | address reporter_, 13 | uint anchorToleranceMantissa_, 14 | uint anchorPeriod_, 15 | TokenConfig[] memory configs) UniswapAnchoredView(priceData_, reporter_, anchorToleranceMantissa_, anchorPeriod_, configs) public {} 16 | 17 | function setAnchorPrice(string memory symbol, uint price) external { 18 | anchorPrices[keccak256(abi.encodePacked(symbol))] = price; 19 | } 20 | 21 | function fetchAnchorPrice(string memory _symbol, TokenConfig memory config, uint _conversionFactor) internal override returns (uint) { 22 | _symbol; // Shh 23 | _conversionFactor; // Shh 24 | return anchorPrices[config.symbolHash]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/contracts/MockUniswapTokenPair.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity ^0.6.10; 4 | 5 | contract MockUniswapTokenPair { 6 | uint112 public reserve0; 7 | uint112 public reserve1; 8 | uint32 public blockTimestampLast; 9 | 10 | uint256 public price0CumulativeLast; 11 | uint256 public price1CumulativeLast; 12 | 13 | constructor( 14 | uint112 reserve0_, 15 | uint112 reserve1_, 16 | uint32 blockTimestampLast_, 17 | uint256 price0CumulativeLast_, 18 | uint256 price1CumulativeLast_ 19 | ) public { 20 | reserve0 = reserve0_; 21 | reserve1 = reserve1_; 22 | blockTimestampLast = blockTimestampLast_; 23 | price0CumulativeLast = price0CumulativeLast_; 24 | price1CumulativeLast = price1CumulativeLast_; 25 | } 26 | 27 | function update( 28 | uint112 reserve0_, 29 | uint112 reserve1_, 30 | uint32 blockTimestampLast_, 31 | uint256 price0CumulativeLast_, 32 | uint256 price1CumulativeLast_ 33 | ) public { 34 | reserve0 = reserve0_; 35 | reserve1 = reserve1_; 36 | blockTimestampLast = blockTimestampLast_; 37 | price0CumulativeLast = price0CumulativeLast_; 38 | price1CumulativeLast = price1CumulativeLast_; 39 | } 40 | 41 | function getReserves() external view returns(uint112, uint112, uint32) { 42 | return (reserve0, reserve1, blockTimestampLast); 43 | } 44 | 45 | function getReservesFraction(bool reversedMarket) external view returns (uint224) { 46 | require(reserve0 > 0, "Reserve is equal to 0"); 47 | require(reserve1 > 0, "Reserve is equal to 0"); 48 | if (reversedMarket) { 49 | return (uint224(reserve0) << 112) / reserve1; 50 | } else { 51 | return (uint224(reserve1) << 112) / reserve0; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/contracts/ProxyPriceOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity ^0.6.10; 4 | 5 | // @dev mock version of v1 price oracle, allowing manually setting return values 6 | contract ProxyPriceOracle { 7 | 8 | mapping(address => uint256) public prices; 9 | 10 | function setUnderlyingPrice(address ctoken, uint price) external { 11 | prices[ctoken] = price; 12 | } 13 | 14 | function getUnderlyingPrice(address ctoken) external view returns (uint) { 15 | return prices[ctoken]; 16 | } 17 | } 18 | 19 | 20 | contract MockAnchorOracle { 21 | struct Anchor { 22 | // floor(block.number / numBlocksPerPeriod) + 1 23 | uint period; 24 | 25 | // Price in ETH, scaled by 10**18 26 | uint priceMantissa; 27 | } 28 | mapping(address => uint256) public assetPrices; 29 | 30 | function setPrice(address asset, uint price) external { 31 | assetPrices[asset] = price; 32 | } 33 | 34 | function setUnderlyingPrice(MockCToken asset, uint price) external { 35 | assetPrices[asset.underlying()] = price; 36 | } 37 | 38 | 39 | uint public constant numBlocksPerPeriod = 240; 40 | 41 | mapping(address => Anchor) public anchors; 42 | function setAnchorPeriod(address asset, uint period) external { 43 | // dont care about anchor price, only period 44 | anchors[asset] = Anchor({period: period, priceMantissa: 1e18}); 45 | } 46 | } 47 | 48 | contract MockCToken { 49 | address public underlying; 50 | constructor(address underlying_) public { 51 | underlying = underlying_; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/contracts/Test.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity ^0.6.10; 4 | pragma experimental ABIEncoderV2; 5 | 6 | contract TestOverflow { 7 | 8 | function testOverflow() public pure { 9 | uint128 sum = uint128(uint64(-1)) + uint128(uint64(-1)); 10 | require(sum == 36893488147419103230, "overflows");// (2^64 -1)*2 11 | uint64 half = uint64(sum / 2); 12 | require(half == 18446744073709551615, "overflow2");// 2 ^ 64 - 1 13 | } 14 | } 15 | --------------------------------------------------------------------------------