From d0a0d0301bff08457d9dfc5861080d3124d079cd Mon Sep 17 00:00:00 2001 From: Antonina Norair Date: Thu, 11 Jun 2020 15:15:58 -0700 Subject: [PATCH] Add a uniswap anchored view This commit adds a Uniswap anchored Open Oracle view. We intend to propose switching the Compound Protocol to use this oracle, together with the Coinbase reporter feed. The idea is that the price can only be set to a value coming from the reporter, but must be reasonably close to the anchor price. The configuration for the oracle is passed up entirely in the constructor, including which uniswap market to use as an anchor for each reported price. Tokens can also be configured to have FIXED_ETH or FIXED_USD prices. FIXED_USD prices are constant (since the oracle returns prices denominated in USD). FIXED_ETH prices are a multiple of the ETH price, which must be reported in order to be used. The view also directly exposes the interface for compatibility with the Compound Protocol, which currently expects to fetch prices by CToken address. In the unexpected event that the reporter believes their key to be compromised, they may sign a 'rotate' message which can be used to invalidate the reporter. In such an event, the view is configured to fall back to using the anchor price directly. In order to track the Uniswap price, the price accumulators those markets expose are tracked by block timestamp. Whenever prices are posted, the tracked accumulators are updated in such a way as to always have an accumulator from at least a certain period ago. When a current anchor price is needed, it is always the time-weighted average price over at least the configured anchor period. --- .circleci/config.yml | 2 +- Dockerfile | 2 +- contracts/AnchoredView/AnchoredView.sol | 14 +- .../AnchoredView/SymbolConfiguration.sol | 6 +- contracts/DelFiPrice.sol | 10 +- contracts/OpenOracleData.sol | 4 +- contracts/OpenOraclePriceData.sol | 10 +- contracts/OpenOracleView.sol | 4 +- contracts/Uniswap/UniswapAnchoredView.sol | 287 +++++++ contracts/Uniswap/UniswapConfig.sol | 714 ++++++++++++++++++ contracts/Uniswap/UniswapLib.sol | 66 ++ package.json | 1 + poster/src/index.ts | 5 +- poster/src/poster.ts | 120 +-- poster/src/prev_price.ts | 11 +- poster/tests/poster_test.ts | 153 +++- tests/AnchoredViewTest.js | 39 +- tests/Helpers.js | 13 +- tests/IntegrationTest.js | 2 +- tests/NonReporterPricesTest.js | 72 ++ tests/PostRealWorldPricesTest.js | 389 ++++++++++ tests/UniswapAnchoredViewTest.js | 171 +++++ tests/UniswapConfigTest.js | 99 +++ tests/contracts/MockUniswapAnchoredView.sol | 25 + tests/contracts/MockUniswapTokenPair.sol | 44 ++ tests/contracts/ProxyPriceOracle.sol | 4 +- tests/contracts/Test.sol | 4 +- 27 files changed, 2150 insertions(+), 121 deletions(-) create mode 100644 contracts/Uniswap/UniswapAnchoredView.sol create mode 100644 contracts/Uniswap/UniswapConfig.sol create mode 100644 contracts/Uniswap/UniswapLib.sol create mode 100644 tests/NonReporterPricesTest.js create mode 100644 tests/PostRealWorldPricesTest.js create mode 100644 tests/UniswapAnchoredViewTest.js create mode 100644 tests/UniswapConfigTest.js create mode 100644 tests/contracts/MockUniswapAnchoredView.sol create mode 100644 tests/contracts/MockUniswapTokenPair.sol diff --git a/.circleci/config.yml b/.circleci/config.yml index 043be19e..66d2d38f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -10,7 +10,7 @@ jobs: docker_layer_caching: true - run: | - sudo wget https://github.com/ethereum/solidity/releases/download/v0.6.6/solc-static-linux -O /usr/local/bin/solc + sudo wget https://github.com/ethereum/solidity/releases/download/v0.6.10/solc-static-linux -O /usr/local/bin/solc sudo chmod +x /usr/local/bin/solc - checkout - restore_cache: diff --git a/Dockerfile b/Dockerfile index 20dadc9b..379f7521 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM node:13.6.0-alpine3.10 WORKDIR /open-oracle -RUN wget https://github.com/ethereum/solidity/releases/download/v0.6.6/solc-static-linux -O /usr/local/bin/solc && chmod +x /usr/local/bin/solc +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 RUN apk update && apk add --no-cache --virtual .gyp \ python \ make \ diff --git a/contracts/AnchoredView/AnchoredView.sol b/contracts/AnchoredView/AnchoredView.sol index 6af211ac..55fd5414 100644 --- a/contracts/AnchoredView/AnchoredView.sol +++ b/contracts/AnchoredView/AnchoredView.sol @@ -1,4 +1,6 @@ -pragma solidity ^0.6.6; +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.6.10; pragma experimental ABIEncoderV2; import "./SymbolConfiguration.sol"; @@ -45,14 +47,13 @@ contract AnchoredView is SymbolConfiguration { /// @notice The Open Oracle Price Data contract OpenOraclePriceData public immutable priceData; - /// @notice The highest ratio of the new median price to the anchor price that will still trigger the median price to be updated + /// @dev The highest ratio of the new median price to the anchor price that will still trigger the median price to be updated uint immutable upperBoundAnchorRatio; - /// @notice The lowest ratio of the new median price to the anchor price that will still trigger the median price to be updated + /// @dev The lowest ratio of the new median price to the anchor price that will still trigger the median price to be updated uint immutable lowerBoundAnchorRatio; - /// @notice Average blocks per day, for checking anchor staleness - /// @dev 1 day / 15 + /// @dev Average blocks per day, for checking anchor staleness (1 day / 15) uint constant blocksInADay = 5760; /// @notice The event emitted when the median price is updated @@ -114,7 +115,6 @@ contract AnchoredView is SymbolConfiguration { uint reporterPrice = priceData.getPrice(reporter, tokenConfig.openOracleKey); uint anchorPrice = getAnchorInUsd(tokenConfig, usdcPrice); - uint anchorRatio = mul(anchorPrice, 100e16) / reporterPrice; bool withinAnchor = anchorRatio <= upperBoundAnchorRatio && anchorRatio >= lowerBoundAnchorRatio; @@ -217,7 +217,7 @@ contract AnchoredView is SymbolConfiguration { /// @notice invalidate the reporter, and fall back to using anchor directly in all cases function invalidate(bytes memory message, bytes memory signature) public { (string memory decoded_message, ) = abi.decode(message, (string, address)); - require(keccak256(abi.encodePacked(decoded_message)) == keccak256(abi.encodePacked("rotate")), "invalid message must be 'rotate'"); + require(keccak256(abi.encodePacked(decoded_message)) == keccak256(abi.encodePacked("rotate")), "invalidation message must be 'rotate'"); require(priceData.source(message, signature) == reporter, "invalidation message must come from the reporter"); reporterBreaker = true; diff --git a/contracts/AnchoredView/SymbolConfiguration.sol b/contracts/AnchoredView/SymbolConfiguration.sol index 7e3a04ee..f8da58d3 100644 --- a/contracts/AnchoredView/SymbolConfiguration.sol +++ b/contracts/AnchoredView/SymbolConfiguration.sol @@ -1,4 +1,6 @@ -pragma solidity ^0.6.6; +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.6.10; pragma experimental ABIEncoderV2; @@ -13,7 +15,7 @@ contract SymbolConfiguration { address public constant cDaiAnchorKey = address(2); /// @notice standard amount for the Dollar - uint constant oneDollar = 1e6; + uint public constant oneDollar = 1e6; // Address of the oracle key (underlying) for cTokens non special keyed tokens address public immutable cRepAnchorKey; diff --git a/contracts/DelFiPrice.sol b/contracts/DelFiPrice.sol index f3c5a71e..1826f62f 100644 --- a/contracts/DelFiPrice.sol +++ b/contracts/DelFiPrice.sol @@ -1,4 +1,6 @@ -pragma solidity ^0.6.6; +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.6.10; pragma experimental ABIEncoderV2; import "./OpenOraclePriceData.sol"; @@ -15,13 +17,13 @@ contract DelFiPrice is OpenOracleView { /// @notice The event emitted when new prices are posted but the median price is not updated due to the anchor event PriceGuarded(string symbol, uint64 median, uint64 anchor); - /// @notice The reporter address whose prices checked against the median for safety + /// @dev The reporter address whose prices checked against the median for safety address anchor; - /// @notice The highest ratio of the new median price to the anchor price that will still trigger the median price to be updated + /// @dev The highest ratio of the new median price to the anchor price that will still trigger the median price to be updated uint256 upperBoundAnchorRatio; - /// @notice The lowest ratio of the new median price to the anchor price that will still trigger the median price to be updated + /// @dev The lowest ratio of the new median price to the anchor price that will still trigger the median price to be updated uint256 lowerBoundAnchorRatio; /// @notice The mapping of medianized prices per symbol diff --git a/contracts/OpenOracleData.sol b/contracts/OpenOracleData.sol index 8b501a73..25e441ae 100644 --- a/contracts/OpenOracleData.sol +++ b/contracts/OpenOracleData.sol @@ -1,4 +1,6 @@ -pragma solidity ^0.6.6; +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.6.10; pragma experimental ABIEncoderV2; /** diff --git a/contracts/OpenOraclePriceData.sol b/contracts/OpenOraclePriceData.sol index c7418f7c..d586ae98 100644 --- a/contracts/OpenOraclePriceData.sol +++ b/contracts/OpenOraclePriceData.sol @@ -1,4 +1,6 @@ -pragma solidity ^0.6.6; +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.6.10; import "./OpenOracleData.sol"; @@ -20,8 +22,8 @@ contract OpenOraclePriceData is OpenOracleData { } /** - * @notice The most recent authenticated data from all sources - * @dev This is private because dynamic mapping keys preclude auto-generated getters. + * @dev The most recent authenticated data from all sources. + * This is private because dynamic mapping keys preclude auto-generated getters. */ mapping(address => mapping(string => Datum)) private data; @@ -41,7 +43,7 @@ contract OpenOraclePriceData is OpenOracleData { // Only update if newer than stored, according to source Datum storage prior = data[source][key]; - if (timestamp > prior.timestamp && timestamp < block.timestamp + 60 minutes) { + if (timestamp > prior.timestamp && timestamp < block.timestamp + 60 minutes && source != address(0)) { data[source][key] = Datum(timestamp, value); emit Write(source, key, timestamp, value); } else { diff --git a/contracts/OpenOracleView.sol b/contracts/OpenOracleView.sol index 275e9085..01c736eb 100644 --- a/contracts/OpenOracleView.sol +++ b/contracts/OpenOracleView.sol @@ -1,4 +1,6 @@ -pragma solidity ^0.6.6; +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.6.10; import "./OpenOracleData.sol"; diff --git a/contracts/Uniswap/UniswapAnchoredView.sol b/contracts/Uniswap/UniswapAnchoredView.sol new file mode 100644 index 00000000..bbd16d50 --- /dev/null +++ b/contracts/Uniswap/UniswapAnchoredView.sol @@ -0,0 +1,287 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.6.10; +pragma experimental ABIEncoderV2; + +import "../OpenOraclePriceData.sol"; +import "./UniswapConfig.sol"; +import "./UniswapLib.sol"; + +struct Observation { + uint timestamp; + uint acc; +} + +contract UniswapAnchoredView is UniswapConfig { + using FixedPoint for *; + + /// @notice The Open Oracle Price Data contract + OpenOraclePriceData public immutable priceData; + + /// @notice the Open Oracle Reporter + address public immutable reporter; + + /// @notice The highest ratio of the new median price to the anchor price that will still trigger the median price to be updated + uint public immutable upperBoundAnchorRatio; + + /// @notice The lowest ratio of the new median price to the anchor price that will still trigger the median price to be updated + uint public immutable lowerBoundAnchorRatio; + + /// @notice The minimum amount of time required for the old uniswap price accumulator to be replaced + uint public immutable anchorPeriod; + + /// @notice Official prices by symbol hash + mapping(bytes32 => uint) public prices; + + /// @notice Circuit breaker for using anchor price oracle directly, ignoring reporter + bool public reporterInvalidated; + + /// @notice The old observation for each uniswap market + mapping(address => Observation) public oldObservations; + /// @notice The new observation for each uniswap market + mapping(address => Observation) public newObservations; + + /// @notice The event emitted when the stored price is updated + event PriceUpdated(string symbol, uint price); + + /// @notice The event emitted when new prices are posted but the stored price is not updated due to the anchor + event PriceGuarded(string symbol, uint reporter, uint anchor); + + /// @notice The event emitted when reporter invalidates itself + event ReporterInvalidated(address reporter); + + /// @notice The event emitted when the uniswap window changes + event UniswapWindowUpdate(address indexed uniswapMarket, uint oldTimestamp, uint newTimestamp, uint oldPrice, uint newPrice); + + /// @notice The event emitted when anchor price is updated + event AnchorPriceUpdate(address indexed uniswapMarket, uint anchorPrice, uint nowCumulativePrice, uint oldCumulativePrice, uint oldTimestamp); + + bytes32 constant ethHash = keccak256(abi.encodePacked("ETH")); + bytes32 constant rotateHash = keccak256(abi.encodePacked("rotate")); + + /** + * @notice Construct a uniswap anchored view for a set of token configurations + * @param reporter_ The reporter whose prices are to be used + * @param anchorToleranceMantissa_ The percentage tolerance that the reporter may deviate from the uniswap anchor + * @param anchorPeriod_ The minimum amount of time required for the old uniswap price accumulator to be replaced + */ + constructor(OpenOraclePriceData priceData_, + address reporter_, + uint anchorToleranceMantissa_, + uint anchorPeriod_, + TokenConfig[] memory configs) UniswapConfig(configs) public { + priceData = priceData_; + reporter = reporter_; + anchorPeriod = anchorPeriod_; + + require(anchorToleranceMantissa_ < 100e16, "anchor tolerance is too high"); + upperBoundAnchorRatio = 100e16 + anchorToleranceMantissa_; + lowerBoundAnchorRatio = 100e16 - anchorToleranceMantissa_; + + for (uint i = 0; i < configs.length; i++) { + TokenConfig memory config = configs[i]; + address uniswapMarket = config.uniswapMarket; + if (config.priceSource == PriceSource.REPORTER) { + require(uniswapMarket != address(0), "reported prices must have an anchor"); + uint cumulativePrice = currentCumulativePrice(config); + oldObservations[uniswapMarket].timestamp = block.timestamp; + newObservations[uniswapMarket].timestamp = block.timestamp; + oldObservations[uniswapMarket].acc = cumulativePrice; + newObservations[uniswapMarket].acc = cumulativePrice; + } else { + require(uniswapMarket == address(0), "only reported prices utilize an anchor"); + } + } + } + + /** + * @notice Get the official price for a symbol + * @param symbol The symbol to fetch the price of + * @return Price denominated in USD, with 6 decimals + */ + function price(string memory symbol) public view returns (uint) { + TokenConfig memory config = getTokenConfigBySymbol(symbol); + return priceInternal(config); + } + + function priceInternal(TokenConfig memory config) internal view returns (uint) { + if (config.priceSource == PriceSource.REPORTER) return prices[config.symbolHash]; + if (config.priceSource == PriceSource.FIXED_USD) return config.fixedPrice; + if (config.priceSource == PriceSource.FIXED_ETH) { + uint usdPerEth = prices[ethHash]; + require(usdPerEth > 0, "ETH price not set, cannot convert to dollars"); + return mul(usdPerEth, config.fixedPrice) / config.baseUnit; + } + } + + /** + * @notice Get the underlying price of a cToken + * @dev Implements the PriceOracle interface for Compound v2. + * @param cToken The cToken address for price retrieval + * @return The price for the given cToken address + */ + function getUnderlyingPrice(address cToken) public view returns (uint) { + TokenConfig memory config = getTokenConfigByCToken(cToken); + return mul(1e30, priceInternal(config)) / config.baseUnit; + } + + /** + * @notice Post open oracle reporter prices, and recalculate stored price by comparing to anchor + * @dev We let anyone pay to post anything, but only prices from configured reporter will be stored in the view. + * @param messages The messages to post to the oracle + * @param signatures The signatures for the corresponding messages + * @param symbols The symbols to compare to anchor for authoritative reading + */ + function postPrices(bytes[] calldata messages, bytes[] calldata signatures, string[] calldata symbols) external { + require(messages.length == signatures.length, "messages and signatures must be 1:1"); + + // Save the prices + for (uint i = 0; i < messages.length; i++) { + priceData.put(messages[i], signatures[i]); + } + + uint ethPrice = fetchEthPrice(); + + // Try to update the view storage + for (uint i = 0; i < symbols.length; i++) { + TokenConfig memory config = getTokenConfigBySymbol(symbols[i]); + string memory symbol = symbols[i]; + bytes32 symbolHash = keccak256(abi.encodePacked(symbol)); + if (source(messages[i], signatures[i]) != reporter) continue; + + uint reporterPrice = priceData.getPrice(reporter, symbol); + uint anchorPrice; + if (symbolHash == ethHash) { + anchorPrice = ethPrice; + } else { + anchorPrice = fetchAnchorPrice(config, ethPrice); + } + + if (reporterInvalidated == true) { + prices[symbolHash] = anchorPrice; + emit PriceUpdated(symbol, anchorPrice); + } else if (isWithinAnchor(reporterPrice, anchorPrice)) { + prices[symbolHash] = reporterPrice; + emit PriceUpdated(symbol, reporterPrice); + } else { + emit PriceGuarded(symbol, reporterPrice, anchorPrice); + } + } + } + + function isWithinAnchor(uint reporterPrice, uint anchorPrice) internal view returns (bool) { + if (reporterPrice > 0) { + uint anchorRatio = mul(anchorPrice, 100e16) / reporterPrice; + return anchorRatio <= upperBoundAnchorRatio && anchorRatio >= lowerBoundAnchorRatio; + } + return false; + } + + /** + * @dev Fetches the current token/eth price accumulator from uniswap. + */ + function currentCumulativePrice(TokenConfig memory config) internal view returns (uint) { + (uint cumulativePrice0, uint cumulativePrice1,) = UniswapV2OracleLibrary.currentCumulativePrices(config.uniswapMarket); + if (config.isUniswapReversed) { + return cumulativePrice1; + } else { + return cumulativePrice0; + } + } + + /** + * @dev Fetches the current eth/usd price from unsiwap, with 6 decimals of precision. + * Conversion factor is 1e18 for eth/usdc market, since we decode uniswap price statically with 18 decimals. + */ + function fetchEthPrice() internal returns (uint) { + return fetchAnchorPrice(getTokenConfigBySymbolHash(ethHash), 1e18); + } + + /** + * @dev Fetches the current token/usd price from uniswap, with 6 decimals of precision. + */ + function fetchAnchorPrice(TokenConfig memory config, uint conversionFactor) internal virtual returns (uint) { + (uint nowCumulativePrice, uint oldCumulativePrice, uint oldTimestamp) = pokeWindowValues(config); + + // This should be impossible, but better safe than sorry + require(block.timestamp > oldTimestamp, "now must come after before"); + uint timeElapsed = block.timestamp - oldTimestamp; + + // Calculate uniswap time-weighted average price + FixedPoint.uq112x112 memory priceAverage = FixedPoint.uq112x112(uint224((nowCumulativePrice - oldCumulativePrice) / timeElapsed)); + uint anchorPriceUnscaled = mul(priceAverage.decode112with18(), conversionFactor); + uint anchorPrice; + + // Adjust anchor price to val * 1e6 decimals format + if (config.isUniswapReversed) { + anchorPrice = anchorPriceUnscaled / config.baseUnit; + } else { + anchorPrice = mul(anchorPriceUnscaled, config.baseUnit) / 1e36; + } + + emit AnchorPriceUpdate(config.uniswapMarket, anchorPrice, nowCumulativePrice, oldCumulativePrice, oldTimestamp); + + return anchorPrice; + } + + /** + * @dev Get time-weighted average prices for a token at the current timestamp. + * Update new and old observations of lagging window if period elapsed. + */ + function pokeWindowValues(TokenConfig memory config) internal returns (uint, uint, uint) { + address uniswapMarket = config.uniswapMarket; + uint cumulativePrice = currentCumulativePrice(config); + + Observation storage newObservation = newObservations[uniswapMarket]; + Observation storage oldObservation = oldObservations[uniswapMarket]; + + // Update new and old observations if elapsed time is greater than or equal to anchor period + uint timeElapsed = block.timestamp - newObservation.timestamp; + if (timeElapsed >= anchorPeriod) { + emit UniswapWindowUpdate(uniswapMarket, oldObservation.timestamp, newObservation.timestamp, oldObservation.acc, newObservation.acc); + oldObservation.timestamp = newObservation.timestamp; + oldObservation.acc = newObservation.acc; + + newObservation.timestamp = block.timestamp; + newObservation.acc = cumulativePrice; + } + return (cumulativePrice, oldObservation.acc, oldObservation.timestamp); + } + + /** + * @notice Invalidate the reporter, and fall back to using anchor directly in all cases + * @dev Only the reporter may sign a message which allows it to invalidate itself. + * To be used in cases of emergency, if the reporter thinks their key may be compromised. + * @param message The data that was presumably signed + * @param signature The fingerprint of the data + private key + */ + function invalidateReporter(bytes memory message, bytes memory signature) external { + (string memory decoded_message, ) = abi.decode(message, (string, address)); + require(keccak256(abi.encodePacked(decoded_message)) == rotateHash, "invalid message must be 'rotate'"); + require(source(message, signature) == reporter, "invalidation message must come from the reporter"); + reporterInvalidated = true; + emit ReporterInvalidated(reporter); + } + + /** + * @notice Recovers the source address which signed a message + * @dev Comparing to a claimed address would add nothing, + * as the caller could simply perform the recover and claim that address. + * @param message The data that was presumably signed + * @param signature The fingerprint of the data + private key + * @return The source address which signed the message, presumably + */ + function source(bytes memory message, bytes memory signature) public pure returns (address) { + (bytes32 r, bytes32 s, uint8 v) = abi.decode(signature, (bytes32, bytes32, uint8)); + bytes32 hash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", keccak256(message))); + return ecrecover(hash, v, r, s); + } + + /// @dev Overflow proof multiplication + function mul(uint a, uint b) internal pure returns (uint) { + if (a == 0) return 0; + uint c = a * b; + require(c / a == b, "multiplication overflow"); + return c; + } +} diff --git a/contracts/Uniswap/UniswapConfig.sol b/contracts/Uniswap/UniswapConfig.sol new file mode 100644 index 00000000..2bdce23f --- /dev/null +++ b/contracts/Uniswap/UniswapConfig.sol @@ -0,0 +1,714 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.6.10; +pragma experimental ABIEncoderV2; + +interface CErc20 { + function underlying() external view returns (address); +} + +contract UniswapConfig { + enum PriceSource {FIXED_ETH, FIXED_USD, REPORTER} + struct TokenConfig { + address cToken; + address underlying; + bytes32 symbolHash; + uint256 baseUnit; + PriceSource priceSource; + uint256 fixedPrice; + address uniswapMarket; + bool isUniswapReversed; + } + + uint public constant maxTokens = 30; + uint public immutable numTokens; + + address internal immutable cToken00; + address internal immutable cToken01; + address internal immutable cToken02; + address internal immutable cToken03; + address internal immutable cToken04; + address internal immutable cToken05; + address internal immutable cToken06; + address internal immutable cToken07; + address internal immutable cToken08; + address internal immutable cToken09; + address internal immutable cToken10; + address internal immutable cToken11; + address internal immutable cToken12; + address internal immutable cToken13; + address internal immutable cToken14; + address internal immutable cToken15; + address internal immutable cToken16; + address internal immutable cToken17; + address internal immutable cToken18; + address internal immutable cToken19; + address internal immutable cToken20; + address internal immutable cToken21; + address internal immutable cToken22; + address internal immutable cToken23; + address internal immutable cToken24; + address internal immutable cToken25; + address internal immutable cToken26; + address internal immutable cToken27; + address internal immutable cToken28; + address internal immutable cToken29; + + address internal immutable underlying00; + address internal immutable underlying01; + address internal immutable underlying02; + address internal immutable underlying03; + address internal immutable underlying04; + address internal immutable underlying05; + address internal immutable underlying06; + address internal immutable underlying07; + address internal immutable underlying08; + address internal immutable underlying09; + address internal immutable underlying10; + address internal immutable underlying11; + address internal immutable underlying12; + address internal immutable underlying13; + address internal immutable underlying14; + address internal immutable underlying15; + address internal immutable underlying16; + address internal immutable underlying17; + address internal immutable underlying18; + address internal immutable underlying19; + address internal immutable underlying20; + address internal immutable underlying21; + address internal immutable underlying22; + address internal immutable underlying23; + address internal immutable underlying24; + address internal immutable underlying25; + address internal immutable underlying26; + address internal immutable underlying27; + address internal immutable underlying28; + address internal immutable underlying29; + + bytes32 internal immutable symbolHash00; + bytes32 internal immutable symbolHash01; + bytes32 internal immutable symbolHash02; + bytes32 internal immutable symbolHash03; + bytes32 internal immutable symbolHash04; + bytes32 internal immutable symbolHash05; + bytes32 internal immutable symbolHash06; + bytes32 internal immutable symbolHash07; + bytes32 internal immutable symbolHash08; + bytes32 internal immutable symbolHash09; + bytes32 internal immutable symbolHash10; + bytes32 internal immutable symbolHash11; + bytes32 internal immutable symbolHash12; + bytes32 internal immutable symbolHash13; + bytes32 internal immutable symbolHash14; + bytes32 internal immutable symbolHash15; + bytes32 internal immutable symbolHash16; + bytes32 internal immutable symbolHash17; + bytes32 internal immutable symbolHash18; + bytes32 internal immutable symbolHash19; + bytes32 internal immutable symbolHash20; + bytes32 internal immutable symbolHash21; + bytes32 internal immutable symbolHash22; + bytes32 internal immutable symbolHash23; + bytes32 internal immutable symbolHash24; + bytes32 internal immutable symbolHash25; + bytes32 internal immutable symbolHash26; + bytes32 internal immutable symbolHash27; + bytes32 internal immutable symbolHash28; + bytes32 internal immutable symbolHash29; + + uint256 internal immutable baseUnit00; + uint256 internal immutable baseUnit01; + uint256 internal immutable baseUnit02; + uint256 internal immutable baseUnit03; + uint256 internal immutable baseUnit04; + uint256 internal immutable baseUnit05; + uint256 internal immutable baseUnit06; + uint256 internal immutable baseUnit07; + uint256 internal immutable baseUnit08; + uint256 internal immutable baseUnit09; + uint256 internal immutable baseUnit10; + uint256 internal immutable baseUnit11; + uint256 internal immutable baseUnit12; + uint256 internal immutable baseUnit13; + uint256 internal immutable baseUnit14; + uint256 internal immutable baseUnit15; + uint256 internal immutable baseUnit16; + uint256 internal immutable baseUnit17; + uint256 internal immutable baseUnit18; + uint256 internal immutable baseUnit19; + uint256 internal immutable baseUnit20; + uint256 internal immutable baseUnit21; + uint256 internal immutable baseUnit22; + uint256 internal immutable baseUnit23; + uint256 internal immutable baseUnit24; + uint256 internal immutable baseUnit25; + uint256 internal immutable baseUnit26; + uint256 internal immutable baseUnit27; + uint256 internal immutable baseUnit28; + uint256 internal immutable baseUnit29; + + PriceSource internal immutable priceSource00; + PriceSource internal immutable priceSource01; + PriceSource internal immutable priceSource02; + PriceSource internal immutable priceSource03; + PriceSource internal immutable priceSource04; + PriceSource internal immutable priceSource05; + PriceSource internal immutable priceSource06; + PriceSource internal immutable priceSource07; + PriceSource internal immutable priceSource08; + PriceSource internal immutable priceSource09; + PriceSource internal immutable priceSource10; + PriceSource internal immutable priceSource11; + PriceSource internal immutable priceSource12; + PriceSource internal immutable priceSource13; + PriceSource internal immutable priceSource14; + PriceSource internal immutable priceSource15; + PriceSource internal immutable priceSource16; + PriceSource internal immutable priceSource17; + PriceSource internal immutable priceSource18; + PriceSource internal immutable priceSource19; + PriceSource internal immutable priceSource20; + PriceSource internal immutable priceSource21; + PriceSource internal immutable priceSource22; + PriceSource internal immutable priceSource23; + PriceSource internal immutable priceSource24; + PriceSource internal immutable priceSource25; + PriceSource internal immutable priceSource26; + PriceSource internal immutable priceSource27; + PriceSource internal immutable priceSource28; + PriceSource internal immutable priceSource29; + + uint256 internal immutable fixedPrice00; + uint256 internal immutable fixedPrice01; + uint256 internal immutable fixedPrice02; + uint256 internal immutable fixedPrice03; + uint256 internal immutable fixedPrice04; + uint256 internal immutable fixedPrice05; + uint256 internal immutable fixedPrice06; + uint256 internal immutable fixedPrice07; + uint256 internal immutable fixedPrice08; + uint256 internal immutable fixedPrice09; + uint256 internal immutable fixedPrice10; + uint256 internal immutable fixedPrice11; + uint256 internal immutable fixedPrice12; + uint256 internal immutable fixedPrice13; + uint256 internal immutable fixedPrice14; + uint256 internal immutable fixedPrice15; + uint256 internal immutable fixedPrice16; + uint256 internal immutable fixedPrice17; + uint256 internal immutable fixedPrice18; + uint256 internal immutable fixedPrice19; + uint256 internal immutable fixedPrice20; + uint256 internal immutable fixedPrice21; + uint256 internal immutable fixedPrice22; + uint256 internal immutable fixedPrice23; + uint256 internal immutable fixedPrice24; + uint256 internal immutable fixedPrice25; + uint256 internal immutable fixedPrice26; + uint256 internal immutable fixedPrice27; + uint256 internal immutable fixedPrice28; + uint256 internal immutable fixedPrice29; + + address internal immutable uniswapMarket00; + address internal immutable uniswapMarket01; + address internal immutable uniswapMarket02; + address internal immutable uniswapMarket03; + address internal immutable uniswapMarket04; + address internal immutable uniswapMarket05; + address internal immutable uniswapMarket06; + address internal immutable uniswapMarket07; + address internal immutable uniswapMarket08; + address internal immutable uniswapMarket09; + address internal immutable uniswapMarket10; + address internal immutable uniswapMarket11; + address internal immutable uniswapMarket12; + address internal immutable uniswapMarket13; + address internal immutable uniswapMarket14; + address internal immutable uniswapMarket15; + address internal immutable uniswapMarket16; + address internal immutable uniswapMarket17; + address internal immutable uniswapMarket18; + address internal immutable uniswapMarket19; + address internal immutable uniswapMarket20; + address internal immutable uniswapMarket21; + address internal immutable uniswapMarket22; + address internal immutable uniswapMarket23; + address internal immutable uniswapMarket24; + address internal immutable uniswapMarket25; + address internal immutable uniswapMarket26; + address internal immutable uniswapMarket27; + address internal immutable uniswapMarket28; + address internal immutable uniswapMarket29; + + bool internal immutable isUniswapReversed00; + bool internal immutable isUniswapReversed01; + bool internal immutable isUniswapReversed02; + bool internal immutable isUniswapReversed03; + bool internal immutable isUniswapReversed04; + bool internal immutable isUniswapReversed05; + bool internal immutable isUniswapReversed06; + bool internal immutable isUniswapReversed07; + bool internal immutable isUniswapReversed08; + bool internal immutable isUniswapReversed09; + bool internal immutable isUniswapReversed10; + bool internal immutable isUniswapReversed11; + bool internal immutable isUniswapReversed12; + bool internal immutable isUniswapReversed13; + bool internal immutable isUniswapReversed14; + bool internal immutable isUniswapReversed15; + bool internal immutable isUniswapReversed16; + bool internal immutable isUniswapReversed17; + bool internal immutable isUniswapReversed18; + bool internal immutable isUniswapReversed19; + bool internal immutable isUniswapReversed20; + bool internal immutable isUniswapReversed21; + bool internal immutable isUniswapReversed22; + bool internal immutable isUniswapReversed23; + bool internal immutable isUniswapReversed24; + bool internal immutable isUniswapReversed25; + bool internal immutable isUniswapReversed26; + bool internal immutable isUniswapReversed27; + bool internal immutable isUniswapReversed28; + bool internal immutable isUniswapReversed29; + + constructor(TokenConfig[] memory configs) public { + require(configs.length <= maxTokens, "too many configs"); + numTokens = configs.length; + + cToken00 = get(configs, 0).cToken; + cToken01 = get(configs, 1).cToken; + cToken02 = get(configs, 2).cToken; + cToken03 = get(configs, 3).cToken; + cToken04 = get(configs, 4).cToken; + cToken05 = get(configs, 5).cToken; + cToken06 = get(configs, 6).cToken; + cToken07 = get(configs, 7).cToken; + cToken08 = get(configs, 8).cToken; + cToken09 = get(configs, 9).cToken; + cToken10 = get(configs, 10).cToken; + cToken11 = get(configs, 11).cToken; + cToken12 = get(configs, 12).cToken; + cToken13 = get(configs, 13).cToken; + cToken14 = get(configs, 14).cToken; + cToken15 = get(configs, 15).cToken; + cToken16 = get(configs, 16).cToken; + cToken17 = get(configs, 17).cToken; + cToken18 = get(configs, 18).cToken; + cToken19 = get(configs, 19).cToken; + cToken20 = get(configs, 20).cToken; + cToken21 = get(configs, 21).cToken; + cToken22 = get(configs, 22).cToken; + cToken23 = get(configs, 23).cToken; + cToken24 = get(configs, 24).cToken; + cToken25 = get(configs, 25).cToken; + cToken26 = get(configs, 26).cToken; + cToken27 = get(configs, 27).cToken; + cToken28 = get(configs, 28).cToken; + cToken29 = get(configs, 29).cToken; + + underlying00 = get(configs, 0).underlying; + underlying01 = get(configs, 1).underlying; + underlying02 = get(configs, 2).underlying; + underlying03 = get(configs, 3).underlying; + underlying04 = get(configs, 4).underlying; + underlying05 = get(configs, 5).underlying; + underlying06 = get(configs, 6).underlying; + underlying07 = get(configs, 7).underlying; + underlying08 = get(configs, 8).underlying; + underlying09 = get(configs, 9).underlying; + underlying10 = get(configs, 10).underlying; + underlying11 = get(configs, 11).underlying; + underlying12 = get(configs, 12).underlying; + underlying13 = get(configs, 13).underlying; + underlying14 = get(configs, 14).underlying; + underlying15 = get(configs, 15).underlying; + underlying16 = get(configs, 16).underlying; + underlying17 = get(configs, 17).underlying; + underlying18 = get(configs, 18).underlying; + underlying19 = get(configs, 19).underlying; + underlying20 = get(configs, 20).underlying; + underlying21 = get(configs, 21).underlying; + underlying22 = get(configs, 22).underlying; + underlying23 = get(configs, 23).underlying; + underlying24 = get(configs, 24).underlying; + underlying25 = get(configs, 25).underlying; + underlying26 = get(configs, 26).underlying; + underlying27 = get(configs, 27).underlying; + underlying28 = get(configs, 28).underlying; + underlying29 = get(configs, 29).underlying; + + symbolHash00 = get(configs, 0).symbolHash; + symbolHash01 = get(configs, 1).symbolHash; + symbolHash02 = get(configs, 2).symbolHash; + symbolHash03 = get(configs, 3).symbolHash; + symbolHash04 = get(configs, 4).symbolHash; + symbolHash05 = get(configs, 5).symbolHash; + symbolHash06 = get(configs, 6).symbolHash; + symbolHash07 = get(configs, 7).symbolHash; + symbolHash08 = get(configs, 8).symbolHash; + symbolHash09 = get(configs, 9).symbolHash; + symbolHash10 = get(configs, 10).symbolHash; + symbolHash11 = get(configs, 11).symbolHash; + symbolHash12 = get(configs, 12).symbolHash; + symbolHash13 = get(configs, 13).symbolHash; + symbolHash14 = get(configs, 14).symbolHash; + symbolHash15 = get(configs, 15).symbolHash; + symbolHash16 = get(configs, 16).symbolHash; + symbolHash17 = get(configs, 17).symbolHash; + symbolHash18 = get(configs, 18).symbolHash; + symbolHash19 = get(configs, 19).symbolHash; + symbolHash20 = get(configs, 20).symbolHash; + symbolHash21 = get(configs, 21).symbolHash; + symbolHash22 = get(configs, 22).symbolHash; + symbolHash23 = get(configs, 23).symbolHash; + symbolHash24 = get(configs, 24).symbolHash; + symbolHash25 = get(configs, 25).symbolHash; + symbolHash26 = get(configs, 26).symbolHash; + symbolHash27 = get(configs, 27).symbolHash; + symbolHash28 = get(configs, 28).symbolHash; + symbolHash29 = get(configs, 29).symbolHash; + + baseUnit00 = get(configs, 0).baseUnit; + baseUnit01 = get(configs, 1).baseUnit; + baseUnit02 = get(configs, 2).baseUnit; + baseUnit03 = get(configs, 3).baseUnit; + baseUnit04 = get(configs, 4).baseUnit; + baseUnit05 = get(configs, 5).baseUnit; + baseUnit06 = get(configs, 6).baseUnit; + baseUnit07 = get(configs, 7).baseUnit; + baseUnit08 = get(configs, 8).baseUnit; + baseUnit09 = get(configs, 9).baseUnit; + baseUnit10 = get(configs, 10).baseUnit; + baseUnit11 = get(configs, 11).baseUnit; + baseUnit12 = get(configs, 12).baseUnit; + baseUnit13 = get(configs, 13).baseUnit; + baseUnit14 = get(configs, 14).baseUnit; + baseUnit15 = get(configs, 15).baseUnit; + baseUnit16 = get(configs, 16).baseUnit; + baseUnit17 = get(configs, 17).baseUnit; + baseUnit18 = get(configs, 18).baseUnit; + baseUnit19 = get(configs, 19).baseUnit; + baseUnit20 = get(configs, 20).baseUnit; + baseUnit21 = get(configs, 21).baseUnit; + baseUnit22 = get(configs, 22).baseUnit; + baseUnit23 = get(configs, 23).baseUnit; + baseUnit24 = get(configs, 24).baseUnit; + baseUnit25 = get(configs, 25).baseUnit; + baseUnit26 = get(configs, 26).baseUnit; + baseUnit27 = get(configs, 27).baseUnit; + baseUnit28 = get(configs, 28).baseUnit; + baseUnit29 = get(configs, 29).baseUnit; + + priceSource00 = get(configs, 0).priceSource; + priceSource01 = get(configs, 1).priceSource; + priceSource02 = get(configs, 2).priceSource; + priceSource03 = get(configs, 3).priceSource; + priceSource04 = get(configs, 4).priceSource; + priceSource05 = get(configs, 5).priceSource; + priceSource06 = get(configs, 6).priceSource; + priceSource07 = get(configs, 7).priceSource; + priceSource08 = get(configs, 8).priceSource; + priceSource09 = get(configs, 9).priceSource; + priceSource10 = get(configs, 10).priceSource; + priceSource11 = get(configs, 11).priceSource; + priceSource12 = get(configs, 12).priceSource; + priceSource13 = get(configs, 13).priceSource; + priceSource14 = get(configs, 14).priceSource; + priceSource15 = get(configs, 15).priceSource; + priceSource16 = get(configs, 16).priceSource; + priceSource17 = get(configs, 17).priceSource; + priceSource18 = get(configs, 18).priceSource; + priceSource19 = get(configs, 19).priceSource; + priceSource20 = get(configs, 20).priceSource; + priceSource21 = get(configs, 21).priceSource; + priceSource22 = get(configs, 22).priceSource; + priceSource23 = get(configs, 23).priceSource; + priceSource24 = get(configs, 24).priceSource; + priceSource25 = get(configs, 25).priceSource; + priceSource26 = get(configs, 26).priceSource; + priceSource27 = get(configs, 27).priceSource; + priceSource28 = get(configs, 28).priceSource; + priceSource29 = get(configs, 29).priceSource; + + fixedPrice00 = get(configs, 0).fixedPrice; + fixedPrice01 = get(configs, 1).fixedPrice; + fixedPrice02 = get(configs, 2).fixedPrice; + fixedPrice03 = get(configs, 3).fixedPrice; + fixedPrice04 = get(configs, 4).fixedPrice; + fixedPrice05 = get(configs, 5).fixedPrice; + fixedPrice06 = get(configs, 6).fixedPrice; + fixedPrice07 = get(configs, 7).fixedPrice; + fixedPrice08 = get(configs, 8).fixedPrice; + fixedPrice09 = get(configs, 9).fixedPrice; + fixedPrice10 = get(configs, 10).fixedPrice; + fixedPrice11 = get(configs, 11).fixedPrice; + fixedPrice12 = get(configs, 12).fixedPrice; + fixedPrice13 = get(configs, 13).fixedPrice; + fixedPrice14 = get(configs, 14).fixedPrice; + fixedPrice15 = get(configs, 15).fixedPrice; + fixedPrice16 = get(configs, 16).fixedPrice; + fixedPrice17 = get(configs, 17).fixedPrice; + fixedPrice18 = get(configs, 18).fixedPrice; + fixedPrice19 = get(configs, 19).fixedPrice; + fixedPrice20 = get(configs, 20).fixedPrice; + fixedPrice21 = get(configs, 21).fixedPrice; + fixedPrice22 = get(configs, 22).fixedPrice; + fixedPrice23 = get(configs, 23).fixedPrice; + fixedPrice24 = get(configs, 24).fixedPrice; + fixedPrice25 = get(configs, 25).fixedPrice; + fixedPrice26 = get(configs, 26).fixedPrice; + fixedPrice27 = get(configs, 27).fixedPrice; + fixedPrice28 = get(configs, 28).fixedPrice; + fixedPrice29 = get(configs, 29).fixedPrice; + + uniswapMarket00 = get(configs, 0).uniswapMarket; + uniswapMarket01 = get(configs, 1).uniswapMarket; + uniswapMarket02 = get(configs, 2).uniswapMarket; + uniswapMarket03 = get(configs, 3).uniswapMarket; + uniswapMarket04 = get(configs, 4).uniswapMarket; + uniswapMarket05 = get(configs, 5).uniswapMarket; + uniswapMarket06 = get(configs, 6).uniswapMarket; + uniswapMarket07 = get(configs, 7).uniswapMarket; + uniswapMarket08 = get(configs, 8).uniswapMarket; + uniswapMarket09 = get(configs, 9).uniswapMarket; + uniswapMarket10 = get(configs, 10).uniswapMarket; + uniswapMarket11 = get(configs, 11).uniswapMarket; + uniswapMarket12 = get(configs, 12).uniswapMarket; + uniswapMarket13 = get(configs, 13).uniswapMarket; + uniswapMarket14 = get(configs, 14).uniswapMarket; + uniswapMarket15 = get(configs, 15).uniswapMarket; + uniswapMarket16 = get(configs, 16).uniswapMarket; + uniswapMarket17 = get(configs, 17).uniswapMarket; + uniswapMarket18 = get(configs, 18).uniswapMarket; + uniswapMarket19 = get(configs, 19).uniswapMarket; + uniswapMarket20 = get(configs, 20).uniswapMarket; + uniswapMarket21 = get(configs, 21).uniswapMarket; + uniswapMarket22 = get(configs, 22).uniswapMarket; + uniswapMarket23 = get(configs, 23).uniswapMarket; + uniswapMarket24 = get(configs, 24).uniswapMarket; + uniswapMarket25 = get(configs, 25).uniswapMarket; + uniswapMarket26 = get(configs, 26).uniswapMarket; + uniswapMarket27 = get(configs, 27).uniswapMarket; + uniswapMarket28 = get(configs, 28).uniswapMarket; + uniswapMarket29 = get(configs, 29).uniswapMarket; + + isUniswapReversed00 = get(configs, 0).isUniswapReversed; + isUniswapReversed01 = get(configs, 1).isUniswapReversed; + isUniswapReversed02 = get(configs, 2).isUniswapReversed; + isUniswapReversed03 = get(configs, 3).isUniswapReversed; + isUniswapReversed04 = get(configs, 4).isUniswapReversed; + isUniswapReversed05 = get(configs, 5).isUniswapReversed; + isUniswapReversed06 = get(configs, 6).isUniswapReversed; + isUniswapReversed07 = get(configs, 7).isUniswapReversed; + isUniswapReversed08 = get(configs, 8).isUniswapReversed; + isUniswapReversed09 = get(configs, 9).isUniswapReversed; + isUniswapReversed10 = get(configs, 10).isUniswapReversed; + isUniswapReversed11 = get(configs, 11).isUniswapReversed; + isUniswapReversed12 = get(configs, 12).isUniswapReversed; + isUniswapReversed13 = get(configs, 13).isUniswapReversed; + isUniswapReversed14 = get(configs, 14).isUniswapReversed; + isUniswapReversed15 = get(configs, 15).isUniswapReversed; + isUniswapReversed16 = get(configs, 16).isUniswapReversed; + isUniswapReversed17 = get(configs, 17).isUniswapReversed; + isUniswapReversed18 = get(configs, 18).isUniswapReversed; + isUniswapReversed19 = get(configs, 19).isUniswapReversed; + isUniswapReversed20 = get(configs, 20).isUniswapReversed; + isUniswapReversed21 = get(configs, 21).isUniswapReversed; + isUniswapReversed22 = get(configs, 22).isUniswapReversed; + isUniswapReversed23 = get(configs, 23).isUniswapReversed; + isUniswapReversed24 = get(configs, 24).isUniswapReversed; + isUniswapReversed25 = get(configs, 25).isUniswapReversed; + isUniswapReversed26 = get(configs, 26).isUniswapReversed; + isUniswapReversed27 = get(configs, 27).isUniswapReversed; + isUniswapReversed28 = get(configs, 28).isUniswapReversed; + isUniswapReversed29 = get(configs, 29).isUniswapReversed; + } + + function get(TokenConfig[] memory configs, uint i) internal pure returns (TokenConfig memory) { + if (i < configs.length) + return configs[i]; + return TokenConfig({ + cToken: address(0), + underlying: address(0), + symbolHash: bytes32(0), + baseUnit: uint256(0), + priceSource: PriceSource(0), + fixedPrice: uint256(0), + uniswapMarket: address(0), + isUniswapReversed: false + }); + } + + function getCTokenIndex(address cToken) internal view returns (uint) { + if (cToken == cToken00) return 0; + if (cToken == cToken01) return 1; + if (cToken == cToken02) return 2; + if (cToken == cToken03) return 3; + if (cToken == cToken04) return 4; + if (cToken == cToken05) return 5; + if (cToken == cToken06) return 6; + if (cToken == cToken07) return 7; + if (cToken == cToken08) return 8; + if (cToken == cToken09) return 9; + if (cToken == cToken10) return 10; + if (cToken == cToken11) return 11; + if (cToken == cToken12) return 12; + if (cToken == cToken13) return 13; + if (cToken == cToken14) return 14; + if (cToken == cToken15) return 15; + if (cToken == cToken16) return 16; + if (cToken == cToken17) return 17; + if (cToken == cToken18) return 18; + if (cToken == cToken19) return 19; + if (cToken == cToken20) return 20; + if (cToken == cToken21) return 21; + if (cToken == cToken22) return 22; + if (cToken == cToken23) return 23; + if (cToken == cToken24) return 24; + if (cToken == cToken25) return 25; + if (cToken == cToken26) return 26; + if (cToken == cToken27) return 27; + if (cToken == cToken28) return 28; + if (cToken == cToken29) return 29; + + return uint(-1); + } + + function getUnderlyingIndex(address underlying) internal view returns (uint) { + if (underlying == underlying00) return 0; + if (underlying == underlying01) return 1; + if (underlying == underlying02) return 2; + if (underlying == underlying03) return 3; + if (underlying == underlying04) return 4; + if (underlying == underlying05) return 5; + if (underlying == underlying06) return 6; + if (underlying == underlying07) return 7; + if (underlying == underlying08) return 8; + if (underlying == underlying09) return 9; + if (underlying == underlying10) return 10; + if (underlying == underlying11) return 11; + if (underlying == underlying12) return 12; + if (underlying == underlying13) return 13; + if (underlying == underlying14) return 14; + if (underlying == underlying15) return 15; + if (underlying == underlying16) return 16; + if (underlying == underlying17) return 17; + if (underlying == underlying18) return 18; + if (underlying == underlying19) return 19; + if (underlying == underlying20) return 20; + if (underlying == underlying21) return 21; + if (underlying == underlying22) return 22; + if (underlying == underlying23) return 23; + if (underlying == underlying24) return 24; + if (underlying == underlying25) return 25; + if (underlying == underlying26) return 26; + if (underlying == underlying27) return 27; + if (underlying == underlying28) return 28; + if (underlying == underlying29) return 29; + + return uint(-1); + } + + function getSymbolHashIndex(bytes32 symbolHash) internal view returns (uint) { + if (symbolHash == symbolHash00) return 0; + if (symbolHash == symbolHash01) return 1; + if (symbolHash == symbolHash02) return 2; + if (symbolHash == symbolHash03) return 3; + if (symbolHash == symbolHash04) return 4; + if (symbolHash == symbolHash05) return 5; + if (symbolHash == symbolHash06) return 6; + if (symbolHash == symbolHash07) return 7; + if (symbolHash == symbolHash08) return 8; + if (symbolHash == symbolHash09) return 9; + if (symbolHash == symbolHash10) return 10; + if (symbolHash == symbolHash11) return 11; + if (symbolHash == symbolHash12) return 12; + if (symbolHash == symbolHash13) return 13; + if (symbolHash == symbolHash14) return 14; + if (symbolHash == symbolHash15) return 15; + if (symbolHash == symbolHash16) return 16; + if (symbolHash == symbolHash17) return 17; + if (symbolHash == symbolHash18) return 18; + if (symbolHash == symbolHash19) return 19; + if (symbolHash == symbolHash20) return 20; + if (symbolHash == symbolHash21) return 21; + if (symbolHash == symbolHash22) return 22; + if (symbolHash == symbolHash23) return 23; + if (symbolHash == symbolHash24) return 24; + if (symbolHash == symbolHash25) return 25; + if (symbolHash == symbolHash26) return 26; + if (symbolHash == symbolHash27) return 27; + if (symbolHash == symbolHash28) return 28; + if (symbolHash == symbolHash29) return 29; + + return uint(-1); + } + + function getTokenConfig(uint i) public view returns (TokenConfig memory) { + require(i < numTokens, "token config not found"); + + if (i == 0) return TokenConfig({cToken: cToken00, underlying: underlying00, symbolHash: symbolHash00, baseUnit: baseUnit00, priceSource: priceSource00, fixedPrice: fixedPrice00, uniswapMarket: uniswapMarket00, isUniswapReversed: isUniswapReversed00}); + if (i == 1) return TokenConfig({cToken: cToken01, underlying: underlying01, symbolHash: symbolHash01, baseUnit: baseUnit01, priceSource: priceSource01, fixedPrice: fixedPrice01, uniswapMarket: uniswapMarket01, isUniswapReversed: isUniswapReversed01}); + if (i == 2) return TokenConfig({cToken: cToken02, underlying: underlying02, symbolHash: symbolHash02, baseUnit: baseUnit02, priceSource: priceSource02, fixedPrice: fixedPrice02, uniswapMarket: uniswapMarket02, isUniswapReversed: isUniswapReversed02}); + if (i == 3) return TokenConfig({cToken: cToken03, underlying: underlying03, symbolHash: symbolHash03, baseUnit: baseUnit03, priceSource: priceSource03, fixedPrice: fixedPrice03, uniswapMarket: uniswapMarket03, isUniswapReversed: isUniswapReversed03}); + if (i == 4) return TokenConfig({cToken: cToken04, underlying: underlying04, symbolHash: symbolHash04, baseUnit: baseUnit04, priceSource: priceSource04, fixedPrice: fixedPrice04, uniswapMarket: uniswapMarket04, isUniswapReversed: isUniswapReversed04}); + if (i == 5) return TokenConfig({cToken: cToken05, underlying: underlying05, symbolHash: symbolHash05, baseUnit: baseUnit05, priceSource: priceSource05, fixedPrice: fixedPrice05, uniswapMarket: uniswapMarket05, isUniswapReversed: isUniswapReversed05}); + if (i == 6) return TokenConfig({cToken: cToken06, underlying: underlying06, symbolHash: symbolHash06, baseUnit: baseUnit06, priceSource: priceSource06, fixedPrice: fixedPrice06, uniswapMarket: uniswapMarket06, isUniswapReversed: isUniswapReversed06}); + if (i == 7) return TokenConfig({cToken: cToken07, underlying: underlying07, symbolHash: symbolHash07, baseUnit: baseUnit07, priceSource: priceSource07, fixedPrice: fixedPrice07, uniswapMarket: uniswapMarket07, isUniswapReversed: isUniswapReversed07}); + if (i == 8) return TokenConfig({cToken: cToken08, underlying: underlying08, symbolHash: symbolHash08, baseUnit: baseUnit08, priceSource: priceSource08, fixedPrice: fixedPrice08, uniswapMarket: uniswapMarket08, isUniswapReversed: isUniswapReversed08}); + if (i == 9) return TokenConfig({cToken: cToken09, underlying: underlying09, symbolHash: symbolHash09, baseUnit: baseUnit09, priceSource: priceSource09, fixedPrice: fixedPrice09, uniswapMarket: uniswapMarket09, isUniswapReversed: isUniswapReversed09}); + + if (i == 10) return TokenConfig({cToken: cToken10, underlying: underlying10, symbolHash: symbolHash10, baseUnit: baseUnit10, priceSource: priceSource10, fixedPrice: fixedPrice10, uniswapMarket: uniswapMarket10, isUniswapReversed: isUniswapReversed10}); + if (i == 11) return TokenConfig({cToken: cToken11, underlying: underlying11, symbolHash: symbolHash11, baseUnit: baseUnit11, priceSource: priceSource11, fixedPrice: fixedPrice11, uniswapMarket: uniswapMarket11, isUniswapReversed: isUniswapReversed11}); + if (i == 12) return TokenConfig({cToken: cToken12, underlying: underlying12, symbolHash: symbolHash12, baseUnit: baseUnit12, priceSource: priceSource12, fixedPrice: fixedPrice12, uniswapMarket: uniswapMarket12, isUniswapReversed: isUniswapReversed12}); + if (i == 13) return TokenConfig({cToken: cToken13, underlying: underlying13, symbolHash: symbolHash13, baseUnit: baseUnit13, priceSource: priceSource13, fixedPrice: fixedPrice13, uniswapMarket: uniswapMarket13, isUniswapReversed: isUniswapReversed13}); + if (i == 14) return TokenConfig({cToken: cToken14, underlying: underlying14, symbolHash: symbolHash14, baseUnit: baseUnit14, priceSource: priceSource14, fixedPrice: fixedPrice14, uniswapMarket: uniswapMarket14, isUniswapReversed: isUniswapReversed14}); + if (i == 15) return TokenConfig({cToken: cToken15, underlying: underlying15, symbolHash: symbolHash15, baseUnit: baseUnit15, priceSource: priceSource15, fixedPrice: fixedPrice15, uniswapMarket: uniswapMarket15, isUniswapReversed: isUniswapReversed15}); + if (i == 16) return TokenConfig({cToken: cToken16, underlying: underlying16, symbolHash: symbolHash16, baseUnit: baseUnit16, priceSource: priceSource16, fixedPrice: fixedPrice16, uniswapMarket: uniswapMarket16, isUniswapReversed: isUniswapReversed16}); + if (i == 17) return TokenConfig({cToken: cToken17, underlying: underlying17, symbolHash: symbolHash17, baseUnit: baseUnit17, priceSource: priceSource17, fixedPrice: fixedPrice17, uniswapMarket: uniswapMarket17, isUniswapReversed: isUniswapReversed17}); + if (i == 18) return TokenConfig({cToken: cToken18, underlying: underlying18, symbolHash: symbolHash18, baseUnit: baseUnit18, priceSource: priceSource18, fixedPrice: fixedPrice18, uniswapMarket: uniswapMarket18, isUniswapReversed: isUniswapReversed18}); + if (i == 19) return TokenConfig({cToken: cToken19, underlying: underlying19, symbolHash: symbolHash19, baseUnit: baseUnit19, priceSource: priceSource19, fixedPrice: fixedPrice19, uniswapMarket: uniswapMarket19, isUniswapReversed: isUniswapReversed19}); + + if (i == 20) return TokenConfig({cToken: cToken20, underlying: underlying20, symbolHash: symbolHash20, baseUnit: baseUnit20, priceSource: priceSource20, fixedPrice: fixedPrice20, uniswapMarket: uniswapMarket20, isUniswapReversed: isUniswapReversed20}); + if (i == 21) return TokenConfig({cToken: cToken21, underlying: underlying21, symbolHash: symbolHash21, baseUnit: baseUnit21, priceSource: priceSource21, fixedPrice: fixedPrice21, uniswapMarket: uniswapMarket21, isUniswapReversed: isUniswapReversed21}); + if (i == 22) return TokenConfig({cToken: cToken22, underlying: underlying22, symbolHash: symbolHash22, baseUnit: baseUnit22, priceSource: priceSource22, fixedPrice: fixedPrice22, uniswapMarket: uniswapMarket22, isUniswapReversed: isUniswapReversed22}); + if (i == 23) return TokenConfig({cToken: cToken23, underlying: underlying23, symbolHash: symbolHash23, baseUnit: baseUnit23, priceSource: priceSource23, fixedPrice: fixedPrice23, uniswapMarket: uniswapMarket23, isUniswapReversed: isUniswapReversed23}); + if (i == 24) return TokenConfig({cToken: cToken24, underlying: underlying24, symbolHash: symbolHash24, baseUnit: baseUnit24, priceSource: priceSource24, fixedPrice: fixedPrice24, uniswapMarket: uniswapMarket24, isUniswapReversed: isUniswapReversed24}); + if (i == 25) return TokenConfig({cToken: cToken25, underlying: underlying25, symbolHash: symbolHash25, baseUnit: baseUnit25, priceSource: priceSource25, fixedPrice: fixedPrice25, uniswapMarket: uniswapMarket25, isUniswapReversed: isUniswapReversed25}); + if (i == 26) return TokenConfig({cToken: cToken26, underlying: underlying26, symbolHash: symbolHash26, baseUnit: baseUnit26, priceSource: priceSource26, fixedPrice: fixedPrice26, uniswapMarket: uniswapMarket26, isUniswapReversed: isUniswapReversed26}); + if (i == 27) return TokenConfig({cToken: cToken27, underlying: underlying27, symbolHash: symbolHash27, baseUnit: baseUnit27, priceSource: priceSource27, fixedPrice: fixedPrice27, uniswapMarket: uniswapMarket27, isUniswapReversed: isUniswapReversed27}); + if (i == 28) return TokenConfig({cToken: cToken28, underlying: underlying28, symbolHash: symbolHash28, baseUnit: baseUnit28, priceSource: priceSource28, fixedPrice: fixedPrice28, uniswapMarket: uniswapMarket28, isUniswapReversed: isUniswapReversed28}); + if (i == 29) return TokenConfig({cToken: cToken29, underlying: underlying29, symbolHash: symbolHash29, baseUnit: baseUnit29, priceSource: priceSource29, fixedPrice: fixedPrice29, uniswapMarket: uniswapMarket29, isUniswapReversed: isUniswapReversed29}); + } + + function getTokenConfigBySymbol(string memory symbol) public view returns (TokenConfig memory) { + return getTokenConfigBySymbolHash(keccak256(abi.encodePacked(symbol))); + } + + function getTokenConfigBySymbolHash(bytes32 symbolHash) public view returns (TokenConfig memory) { + uint index = getSymbolHashIndex(symbolHash); + if (index != uint(-1)) { + return getTokenConfig(index); + } + + revert("token config not found"); + } + + function getTokenConfigByCToken(address cToken) public view returns (TokenConfig memory) { + uint index = getCTokenIndex(cToken); + if (index != uint(-1)) { + return getTokenConfig(index); + } + + return getTokenConfigByUnderlying(CErc20(cToken).underlying()); + } + + function getTokenConfigByUnderlying(address underlying) public view returns (TokenConfig memory) { + uint index = getUnderlyingIndex(underlying); + if (index != uint(-1)) { + return getTokenConfig(index); + } + + revert("token config not found"); + } +} \ No newline at end of file diff --git a/contracts/Uniswap/UniswapLib.sol b/contracts/Uniswap/UniswapLib.sol new file mode 100644 index 00000000..5a5abe47 --- /dev/null +++ b/contracts/Uniswap/UniswapLib.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.6.10; + +// a library for handling binary fixed point numbers (https://en.wikipedia.org/wiki/Q_(number_format)) +library FixedPoint { + // range: [0, 2**112 - 1] + // resolution: 1 / 2**112 + struct uq112x112 { + uint224 _x; + } + + // returns a uq112x112 which represents the ratio of the numerator to the denominator + // equivalent to encode(numerator).div(denominator) + function fraction(uint112 numerator, uint112 denominator) internal pure returns (uq112x112 memory) { + require(denominator > 0, "FixedPoint: DIV_BY_ZERO"); + return uq112x112((uint224(numerator) << 112) / denominator); + } + + // decode a uq112x112 into a uint with 18 decimals of precision + function decode112with18(uq112x112 memory self) internal pure returns (uint) { + // we only have 256 - 224 = 32 bits to spare, so scaling up by ~60 bits is dangerous + // instead, get close to: + // (x * 1e18) >> 112 + // without risk of overflowing, e.g.: + // (x) / 2 ** (112 - lg(1e18)) + return uint(self._x) / 5192296858534816; + } +} + +// library with helper methods for oracles that are concerned with computing average prices +library UniswapV2OracleLibrary { + using FixedPoint for *; + + // helper function that returns the current block timestamp within the range of uint32, i.e. [0, 2**32 - 1] + function currentBlockTimestamp() internal view returns (uint32) { + return uint32(block.timestamp % 2 ** 32); + } + + // produces the cumulative price using counterfactuals to save gas and avoid a call to sync. + function currentCumulativePrices( + address pair + ) internal view returns (uint price0Cumulative, uint price1Cumulative, uint32 blockTimestamp) { + blockTimestamp = currentBlockTimestamp(); + price0Cumulative = IUniswapV2Pair(pair).price0CumulativeLast(); + price1Cumulative = IUniswapV2Pair(pair).price1CumulativeLast(); + + // if time has elapsed since the last update on the pair, mock the accumulated price values + (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast) = IUniswapV2Pair(pair).getReserves(); + if (blockTimestampLast != blockTimestamp) { + // subtraction overflow is desired + uint32 timeElapsed = blockTimestamp - blockTimestampLast; + // addition overflow is desired + // counterfactual + price0Cumulative += uint(FixedPoint.fraction(reserve1, reserve0)._x) * timeElapsed; + // counterfactual + price1Cumulative += uint(FixedPoint.fraction(reserve0, reserve1)._x) * timeElapsed; + } + } +} + +interface IUniswapV2Pair { + function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast); + function price0CumulativeLast() external view returns (uint); + function price1CumulativeLast() external view returns (uint); +} diff --git a/package.json b/package.json index c194750c..a607c661 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "author": "Compound Labs, Inc.", "license": "MIT", "dependencies": { + "bignumber.js": "^9.0.0", "eth-saddle": "^0.1.17", "web3": "^1.2.4", "yargs": "^15.0.2" diff --git a/poster/src/index.ts b/poster/src/index.ts index 559321bc..20f07fd1 100644 --- a/poster/src/index.ts +++ b/poster/src/index.ts @@ -13,7 +13,8 @@ async function run() { .option('web3_provider', {description: 'Web 3 provider', type: 'string', default: 'http://127.0.0.1:8545'}) .option('timeout', {alias: 't', description: 'how many seconds to wait before retrying with more gas', type: 'number', default: 180}) .option('gas_limit', {alias: 'g', description: 'how much gas to send', type: 'number', default: 4000000}) - .option('price_delta', {alias: 'd', description: 'the min required difference between new and previous asset price for price update on blockchain', type: 'number', default: 0.1}) + .option('price_delta', {alias: 'd', description: 'the min required difference between new and previous asset price for price update on blockchain', type: 'number', default: 1}) + .option('supported_assets', {alias: 'sa', description: 'A list of supported token names for posting prices', type: 'string', default: 'BTC,ETH,DAI,REP,ZRX,BAT,KNC,LINK,COMP'}) .help() .alias('help', 'h') .demandOption(['poster_key', 'sources', 'view_function', 'web3_provider', 'view_address'], 'Provide all the arguments') @@ -31,7 +32,7 @@ async function run() { } try { - await main(argv.sources, argv.poster_key, argv.view_address, argv.view_function, argv.gas_limit, argv.price_delta, web3); + await main(argv.sources, argv.poster_key, argv.view_address, argv.view_function, argv.gas_limit, argv.price_delta, argv.supported_assets, web3); console.log("main completed") } catch (e) { console.error(`Poster failed to run`, e); diff --git a/poster/src/poster.ts b/poster/src/poster.ts index bd5ef51e..a09b4faa 100644 --- a/poster/src/poster.ts +++ b/poster/src/poster.ts @@ -3,61 +3,88 @@ import fetch from 'node-fetch'; import Web3 from 'web3'; import { TransactionConfig } from 'web3-core'; import AbiCoder from 'web3-eth-abi'; -import { getDataAddress, getPreviousPrice, getSourceAddress } from './prev_price'; +import helpers from './prev_price'; import { BigNumber as BN } from 'bignumber.js'; -async function main(sources : string, - senderKey : string, - viewAddress : string, - functionSig : string, - gas: number, - delta: number, - web3 : Web3) { +async function main(sources: string, + senderKey: string, + viewAddress: string, + functionSig: string, + gas: number, + delta: number, + assets: string, + web3: Web3) { const payloads = await fetchPayloads(sources.split(",")); - const dataAddress = await getDataAddress(viewAddress, web3); + const filteredPayloads = await filterPayloads(payloads, viewAddress, assets, delta, web3); + if (filteredPayloads.length != 0) { + const gasPrice = await fetchGasPrice(); + const trxData = buildTrxData(filteredPayloads, functionSig); + + const trx = { + data: trxData, + to: viewAddress, + gasPrice: gasPrice, + gas: gas + } + + return await postWithRetries(trx, senderKey, web3); + } +} + +async function filterPayloads(payloads: DelFiReporterPayload[], + viewAddress: string, + assets: string, + delta: number, + web3: Web3): Promise { + const dataAddress = await helpers.getDataAddress(viewAddress, web3); + const supportedAssets = assets.split(","); - let updatePrices = false; await Promise.all(payloads.map(async payload => { - const sourceAddress = await getSourceAddress(dataAddress, payload.messages[0], payload.signatures[0], web3); - + const sourceAddress = await helpers.getSourceAddress(dataAddress, payload.messages[0], payload.signatures[0], web3); + + const filteredPrices = {}; + const filteredMessages: string[] = []; + const filteredSignatures: string[] = []; + let index = 0; for (const [asset, price] of Object.entries(payload.prices)) { - const prev_price = await getPreviousPrice(sourceAddress, asset, dataAddress, web3); + // Post only prices for supported assets, skip prices for unregistered assets + if (!supportedAssets.includes(asset.toUpperCase())) { + index++; + continue; + } + + const prev_price = await helpers.getPreviousPrice(sourceAddress, asset, dataAddress, web3); + console.log(`For asset ${asset}: prev price = ${prev_price}, new price = ${price}`); - // Update price if new price is different by more than delta % from previous price - // Update all asset prices if only 1 asset price is different + // Update price only if new price is different by more than delta % from the previous price if (!inDeltaRange(delta, Number(price), prev_price)) { - updatePrices = true; - break; + console.log(`Not in delta range for asset ${asset}, price = ${Number(price)}, prev price = ${prev_price}`); + filteredPrices[asset] = price; + filteredMessages.push(payload.messages[index]); + filteredSignatures.push(payload.signatures[index]); } + index++; } + payload.prices = filteredPrices; + payload.messages = filteredMessages; + payload.signatures = filteredSignatures; })) - if (updatePrices) { - const gasPrice = await fetchGasPrice(); - const trxData = buildTrxData(payloads, functionSig); - - const trx = { - data: trxData, - to: viewAddress, - gasPrice: gasPrice, - gas: gas - } - - return await postWithRetries(trx, senderKey, web3); - } + // Filter payloads with no updated prices + const filteredPayloads = payloads.filter(payload => payload.messages.length > 0); + return filteredPayloads; } -// if new price is less that delta percent different form the old price, do not post new price -function inDeltaRange(delta:number, price: number, prev_price: number) { +// Checks if new price is less than delta percent different form the old price +function inDeltaRange(delta: number, price: number, prev_price: number) { // Always update prices if delta is set to 0 or delta is not within expected range [0..100]% - if (delta <= 0 || delta > 100) return false; - - const minDifference = new BN(prev_price).multipliedBy(delta).dividedBy(100); - const difference = new BN(prev_price).minus(new BN(price).multipliedBy(1e6)).abs(); - return difference.isLessThanOrEqualTo(minDifference); + if (delta <= 0 || delta > 100) return false; + const minDifference = new BN(prev_price).multipliedBy(delta).dividedBy(100); + const difference = new BN(prev_price).minus(new BN(price).multipliedBy(1e6)).abs(); + return difference.isLessThanOrEqualTo(minDifference); } -async function fetchPayloads(sources : string[], fetchFn=fetch) : Promise { +async function fetchPayloads(sources: string[], fetchFn = fetch): Promise { let sourcePromises = sources.map(async (source) => { try { let response; @@ -110,7 +137,7 @@ async function fetchPayloads(sources : string[], fetchFn=fetch) : Promise x != null); } -async function fetchGasPrice(fetchFn=fetch) : Promise { +async function fetchGasPrice(fetchFn = fetch): Promise { try { let source = "https://api.compound.finance/api/gas_prices/get_gas_price"; let response = await fetchFn(source); @@ -124,7 +151,7 @@ async function fetchGasPrice(fetchFn=fetch) : Promise { } } -function buildTrxData(payloads : DelFiReporterPayload[], functionSig : string) : string { +function buildTrxData(payloads: DelFiReporterPayload[], functionSig: string): string { const types = findTypes(functionSig); let messages = payloads.reduce((a: string[], x) => a.concat(x.messages), []); @@ -135,13 +162,13 @@ function buildTrxData(payloads : DelFiReporterPayload[], functionSig : string) : // see https://github.com/ethereum/web3.js/blob/2.x/packages/web3-eth-abi/src/AbiCoder.js#L112 return (AbiCoder).encodeFunctionSignature(functionSig) + - (AbiCoder) - .encodeParameters(types, [messages, signatures, [...upperCaseDeDuped]]) - .replace('0x', ''); + (AbiCoder) + .encodeParameters(types, [messages, signatures, [...upperCaseDeDuped]]) + .replace('0x', ''); } // e.g. findTypes("postPrices(bytes[],bytes[],string[])")-> ["bytes[]","bytes[]","string[]"] -function findTypes(functionSig : string) : string[] { +function findTypes(functionSig: string): string[] { // this unexported function from ethereumjs-abi is copy pasted from source // see https://github.com/ethereumjs/ethereumjs-abi/blob/master/lib/index.js#L81 let parseSignature = function (sig) { @@ -177,7 +204,7 @@ function findTypes(functionSig : string) : string[] { return parseSignature(functionSig).args; } -function getEnvVar(name : string): string { +function getEnvVar(name: string): string { const result: string | undefined = process.env[name]; if (result) { @@ -193,5 +220,6 @@ export { fetchGasPrice, fetchPayloads, main, - inDeltaRange + inDeltaRange, + filterPayloads } diff --git a/poster/src/prev_price.ts b/poster/src/prev_price.ts index b8b1153e..2ec0c5c5 100644 --- a/poster/src/prev_price.ts +++ b/poster/src/prev_price.ts @@ -46,9 +46,10 @@ async function getPreviousPrice(sourceAddress: string, asset: string, dataAddres return web3.eth.abi.decodeParameter('address', await web3.eth.call(call)).toString(); } - export { - getPreviousPrice, - getSourceAddress, - getDataAddress - } + const exportFunctions = { + getPreviousPrice, + getSourceAddress, + getDataAddress + }; + export default exportFunctions; \ No newline at end of file diff --git a/poster/tests/poster_test.ts b/poster/tests/poster_test.ts index 81b04674..4b7f32ba 100644 --- a/poster/tests/poster_test.ts +++ b/poster/tests/poster_test.ts @@ -1,10 +1,11 @@ -import {buildTrxData, findTypes, fetchGasPrice, fetchPayloads, inDeltaRange} from '../src/poster'; +import {buildTrxData, findTypes, fetchGasPrice, fetchPayloads, inDeltaRange, filterPayloads} from '../src/poster'; +import helpers from '../src/prev_price'; import Web3 from 'web3'; const endpointResponses = { "http://localhost:3000": { "messages": ["0xmessage"], - "prices": { + "prices": { "eth": 260, "zrx": 0.58, }, @@ -12,7 +13,7 @@ const endpointResponses = { }, "http://localhost:3000/prices.json": { "messages": ["0xmessage"], - "prices": { + "prices": { "eth": 250, "zrx": 1.58, }, @@ -64,7 +65,7 @@ describe('loading poster arguments from environment and https', () => { expect(payloads).toEqual([ { "messages": ["0xmessage"], - "prices": { + "prices": { "eth": 260, "zrx": 0.58, }, @@ -72,7 +73,7 @@ describe('loading poster arguments from environment and https', () => { }, { "messages": ["0xmessage"], - "prices": { + "prices": { "eth": 250, "zrx": 1.58, }, @@ -83,7 +84,7 @@ describe('loading poster arguments from environment and https', () => { describe('building a function call', () => { test('findTypes', () => { - let typeString = "writePrices(bytes[],bytes[],string[])"; + let typeString = "writePrices(bytes[],bytes[],string[])"; expect(findTypes(typeString)).toEqual(["bytes[]", "bytes[]", "string[]"]); }); @@ -140,3 +141,143 @@ describe('checking that numbers are within the specified delta range', () => { expect(inDeltaRange(100, 1, 1e6)).toEqual(true); }) }) + +describe('filtering payloads', () => { + let prevPrices = {}; + async function mockPreviosPrice(_sourceAddress, asset, _dataAddress, _web3) { + return prevPrices[asset]; + } + test('Filtering payloads, BAT price is more than delta % different', async () => { + helpers.getSourceAddress = jest.fn(); + helpers.getDataAddress = jest.fn(); + helpers.getPreviousPrice = mockPreviosPrice; + + const payloads = [ + { + timestamp: '1593209100', + messages: ['0x1', '0x2', '0x3', '0x4', '0x5', '0x6', '0x7', '0x8', '0x9'], + signatures: ['0x1', '0x2', '0x3', '0x4', '0x5', '0x6', '0x7', '0x8', '0x9'], + prices: { + BTC: '9192.23', + ETH: '230.585', + XTZ: '2.5029500000000002', + DAI: '1.0035515', + REP: '16.83', + ZRX: '0.3573955', + BAT: '0.26466', + KNC: '1.16535', + LINK: '4.70819' + } + } + ] + prevPrices = { 'BTC': 9149090000, 'ETH': 229435000, 'DAI': 1003372, 'REP': 16884999, 'ZRX': 357704, 'BAT': 260992, 'KNC': 1156300, 'LINK': 4704680 } + + const filteredPayloads = await filterPayloads(payloads, '0x0', 'BTC,ETH,DAI,REP,ZRX,BAT,KNC,LINK,COMP', 1, new Web3()); + expect(filteredPayloads).toEqual([ + { + timestamp: '1593209100', + messages: ['0x7'], + signatures: ['0x7'], + prices: { BAT: '0.26466' } + } + ] + ); + }) + + test('Filtering payloads, ETH, BTC and ZRX prices are more than delta % different, ZRX, XTZ are not supported', async () => { + prevPrices = { 'BTC': 10000000000, 'ETH': 1000000000, 'ZRX': 1011000, 'REP': 16000000, 'DAI': 1000000, 'BAT': 1000000, 'KNC': 2000000, 'LINK': 5000000 } + + helpers.getSourceAddress = jest.fn(); + helpers.getDataAddress = jest.fn(); + helpers.getPreviousPrice = mockPreviosPrice; + + const payloads = [ + { + timestamp: '1593209100', + messages: ['0x1', '0x2', '0x3', '0x4', '0x5', '0x6', '0x7', '0x8', '0x9'], + signatures: ['0x1', '0x2', '0x3', '0x4', '0x5', '0x6', '0x7', '0x8', '0x9'], + prices: { + BTC: '10101', + ETH: '1011', + XTZ: '10', + DAI: '1', + REP: '16', + ZRX: '1.011', + BAT: '1', + KNC: '2', + LINK: '5' + } + } + ] + + const filteredPayloads = await filterPayloads(payloads, '0x0', 'BTC,ETH,DAI,REP,BAT,KNC,LINK,COMP', 1, new Web3()); + expect(filteredPayloads).toEqual([ + { + timestamp: '1593209100', + messages: [ '0x1', '0x2' ], + signatures: [ '0x1', '0x2' ], + prices: { BTC: '10101', ETH: '1011' } + } + ]); + }) + + test('Filtering payloads, ETH, BTC and ZRX prices are more than delta % different, no assets are supported', async () => { + prevPrices = { 'BTC': 10000000000, 'ETH': 1000000000, 'ZRX': 1011000, 'REP': 16000000, 'DAI': 1000000, 'BAT': 1000000, 'KNC': 2000000, 'LINK': 5000000 } + + helpers.getSourceAddress = jest.fn(); + helpers.getDataAddress = jest.fn(); + helpers.getPreviousPrice = mockPreviosPrice; + + const payloads = [ + { + timestamp: '1593209100', + messages: ['0x1', '0x2', '0x3', '0x4', '0x5', '0x6', '0x7', '0x8', '0x9'], + signatures: ['0x1', '0x2', '0x3', '0x4', '0x5', '0x6', '0x7', '0x8', '0x9'], + prices: { + BTC: '10101', + ETH: '1011', + XTZ: '10', + DAI: '1', + REP: '16', + ZRX: '1.011', + BAT: '1', + KNC: '2', + LINK: '5' + } + } + ] + + const filteredPayloads = await filterPayloads(payloads, '0x0', '', 1, new Web3()); + expect(filteredPayloads).toEqual([]); + }) + + test('Filtering payloads, delta is 0% percent, all supported prices should be updated', async () => { + prevPrices = { 'BTC': 10000000000, 'ETH': 1000000000, 'ZRX': 1011000, 'REP': 16000000, 'DAI': 1000000, 'BAT': 1000000, 'KNC': 2000000, 'LINK': 5000000 } + + helpers.getSourceAddress = jest.fn(); + helpers.getDataAddress = jest.fn(); + helpers.getPreviousPrice = mockPreviosPrice; + + const payloads = [ + { + timestamp: '1593209100', + messages: ['0x1', '0x2', '0x3', '0x4', '0x5', '0x6', '0x7', '0x8', '0x9'], + signatures: ['0x1', '0x2', '0x3', '0x4', '0x5', '0x6', '0x7', '0x8', '0x9'], + prices: { + BTC: '10101', + ETH: '1011', + XTZ: '10', + DAI: '1', + REP: '16', + ZRX: '1.011', + BAT: '1', + KNC: '2', + LINK: '5' + } + } + ] + + const filteredPayloads = await filterPayloads(payloads, '0x0', 'BTC,ETH,DAI,REP,ZRX,BAT,KNC,LINK,COMP', 0, new Web3()); + expect(filteredPayloads).toEqual(payloads); + }) +}) diff --git a/tests/AnchoredViewTest.js b/tests/AnchoredViewTest.js index 1e0e51a4..93d28954 100644 --- a/tests/AnchoredViewTest.js +++ b/tests/AnchoredViewTest.js @@ -295,7 +295,7 @@ describe('AnchoredView', () => { ["BAT", 931592500000000, 0.160271e6], ["REP", 56128970000000000, 9.656427e6], ["ZRX", 985525000000000, 0.169549e6], - ["BTC", "399920015996800660000000000000", 6880.223955e6] // 8 decimals underlying -> 10 extra decimals on proxy + ["BTC", "399920015996800660000000000000", 6880.223955e6] // 8 decimals underlying -> 10 extra decimals on proxy ].forEach(([openOracleKey, proxyPrice, expectedOpenOraclePrice]) => { it(`converts ${openOracleKey} price through proxy usdc, with 6 decimals`, async () => { // TODO: fix sai price test after SAI Global Settlement @@ -410,7 +410,7 @@ describe('AnchoredView', () => { ["BAT", 931592500000000], ["REP", 56128970000000000], ["ZRX", 985525000000000], - ["BTC", "399920015996800660000000000000"] // 8 decimals underlying -> 10 extra decimals on proxy + ["BTC", "399920015996800660000000000000"] // 8 decimals underlying -> 10 extra decimals on proxy ].forEach(([openOracleKey, proxyPrice]) => { it(`returns anchor price if reporterBreaker is set for ${openOracleKey}`, async () => { let tokenAddress = await call(delfi, 'getCTokenAddress', [openOracleKey]); @@ -453,7 +453,7 @@ describe('AnchoredView', () => { await expect( send(delfi, 'invalidate', [encoded, signed.signature]) - ).rejects.toRevert("revert invalid message must be 'rotate'"); + ).rejects.toRevert("revert invalidation message must be 'rotate'"); }); it("reverts if given wrong signature", async () => { @@ -479,37 +479,4 @@ describe('AnchoredView', () => { expect(await call(delfi, 'reporterBreaker', [])).toEqual(true); }); }); - - describe("cutAnchor", () => { - beforeEach(async () => { - ({ - delfi, - anchorOracle, - tokens - } = await setup()); - }); - - - it("cuts anchor if anchor is stale", async () => { - await sendRPC(web3, 'evm_mineBlockNumber', 17757); - let blocksPerPeriod = await call(anchorOracle, 'numBlocksPerPeriod', []); - await send(anchorOracle, 'setAnchorPeriod', [tokens.usdc, 12000 / blocksPerPeriod]); - - var cut = await send(delfi, 'cutAnchor', []); - expect(await call(delfi, 'anchorBreaker')).toEqual(false); - - // one more block has passed, so now cuttable - // now on block 17761 aka 5761 blocks past last anchor - cut = await send(delfi, 'cutAnchor', []); - expect(await call(delfi, 'anchorBreaker')).toEqual(true); - expect(cut.events.AnchorCut.returnValues.anchor).toEqual(anchorOracle._address); - }); - - it("no op if anchor is up to date", async () => { - await sendRPC(web3, 'evm_mineBlockNumber', [0]); - let cut = await send(delfi, 'cutAnchor', []); - expect(await call(delfi, 'anchorBreaker')).toEqual(false); - }); - - }); }); diff --git a/tests/Helpers.js b/tests/Helpers.js index 94495961..d47aab61 100644 --- a/tests/Helpers.js +++ b/tests/Helpers.js @@ -2,6 +2,13 @@ const Web3 = require('web3'); const web3 = new Web3(); // no provider, since we won't make any calls +function uint(n) { + return web3.utils.toBN(n).toString(); +} + +function keccak256(str) { + return web3.utils.keccak256(str); +} function address(n) { return `0x${n.toString(16).padStart(40, '0')}`; @@ -52,11 +59,13 @@ function sendRPC(web3, method, params) { } module.exports = { - sendRPC, + sendRPC, address, bytes, time, numToBigNum, numToHex, - uint256 + uint256, + uint, + keccak256 }; diff --git a/tests/IntegrationTest.js b/tests/IntegrationTest.js index 05e22f2b..99ae1786 100644 --- a/tests/IntegrationTest.js +++ b/tests/IntegrationTest.js @@ -63,5 +63,5 @@ describe('Integration', () => { } finally { await compose.down({cwd: root}); } - }, 60000); + }, 180000); }); diff --git a/tests/NonReporterPricesTest.js b/tests/NonReporterPricesTest.js new file mode 100644 index 00000000..32930414 --- /dev/null +++ b/tests/NonReporterPricesTest.js @@ -0,0 +1,72 @@ +const { sendRPC } = require('./Helpers'); + +function address(n) { + return `0x${n.toString(16).padStart(40, '0')}`; +} + +function keccak256(str) { + return web3.utils.keccak256(str); +} + +function uint(n) { + return web3.utils.toBN(n).toString(); +} + +const PriceSource = { + FIXED_ETH: 0, + FIXED_USD: 1, + REPORTER: 2 +}; + +describe('UniswapAnchoredView', () => { + it('handles fixed_usd prices', async () => { + 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}; + 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}; + const priceData = await deploy("OpenOraclePriceData", []); + const oracle = await deploy('UniswapAnchoredView', [priceData._address, address(0), 0, 0, [USDC, USDT]]); + expect(await call(oracle, 'price', ["USDC"])).numEquals(1e6); + expect(await call(oracle, 'price', ["USDT"])).numEquals(1e6); + }); + + it('reverts fixed_eth prices if no ETH price', async () => { + 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}; + const priceData = await deploy("OpenOraclePriceData", []); + const oracle = await deploy('UniswapAnchoredView', [priceData._address, address(0), 0, 0, [SAI]]); + expect(call(oracle, 'price', ["SAI"])).rejects.toRevert('revert ETH price not set, cannot convert to dollars'); + }); + + it('reverts if ETH has no uniswap market', async () => { + const ETH = {cToken: address(5), underlying: address(6), symbolHash: keccak256("ETH"), baseUnit: uint(1e18), priceSource: PriceSource.REPORTER, fixedPrice: 0, uniswapMarket: address(0), isUniswapReversed: true}; + 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}; + const priceData = await deploy("OpenOraclePriceData", []); + expect(deploy('UniswapAnchoredView', [priceData._address, address(0), 0, 0, [ETH, SAI]])).rejects.toRevert('revert reported prices must have an anchor'); + }); + + it('reverts if non-reporter has a uniswap market', async () => { + 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}; + 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}; + const priceData = await deploy("OpenOraclePriceData", []); + expect(deploy('UniswapAnchoredView', [priceData._address, address(0), 0, 0, [ETH, SAI]])).rejects.toRevert('revert only reported prices utilize an anchor'); + }); + + it('handles fixed_eth prices', async () => { + const usdc_eth_pair = await deploy("MockUniswapTokenPair", [ + "1865335786147", + "8202340665419053945756", + "1593755855", + "119785032308978310142960133641565753500432674230537", + "5820053774558372823476814618189", + ]); + const reporter = "0xfCEAdAFab14d46e20144F48824d0C09B1a03F2BC"; + const messages = ["0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000005efebe9800000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000d84ec180000000000000000000000000000000000000000000000000000000000000006707269636573000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000034554480000000000000000000000000000000000000000000000000000000000"]; + const signatures = ["0xb8ba87c37228468f9d107a97eeb92ebd49a50993669cab1737fea77e5b884f2591affbf4058bcfa29e38756021deeafaeeab7a5c4f5ce584c7d1e12346c88d4e000000000000000000000000000000000000000000000000000000000000001b"]; + 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}; + 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}; + const priceData = await deploy("OpenOraclePriceData", []); + const oracle = await deploy('UniswapAnchoredView', [priceData._address, reporter, uint(20e16), 60, [ETH, SAI]]); + await sendRPC(web3, 'evm_increaseTime', [30 * 60]); + await send(oracle, "postPrices", [messages, signatures, ['ETH']]); + expect(await call(oracle, 'price', ["ETH"])).numEquals(226815000); + expect(await call(oracle, 'price', ["SAI"])).numEquals(1198842); + }); +}); \ No newline at end of file diff --git a/tests/PostRealWorldPricesTest.js b/tests/PostRealWorldPricesTest.js new file mode 100644 index 00000000..5676cfad --- /dev/null +++ b/tests/PostRealWorldPricesTest.js @@ -0,0 +1,389 @@ + +// @notice UniswapAnchoredView `postPrices` test +// Based on data from Coinbase oracle https://api.pro.coinbase.com/oracle and Uniswap token pairs at July 2nd 2020. +const { sendRPC } = require('./Helpers'); + +function address(n) { + return `0x${n.toString(16).padStart(40, "0")}`; +} + +function uint(n) { + return web3.utils.toBN(n).toString(); +} + +function keccak256(str) { + return web3.utils.keccak256(str); +} + +function numToHex(num) { + return web3.utils.numberToHex(num); +} + +async function setupTokenPairs() { + // Reversed market for ETH, read value of ETH in USDC + const usdc_eth_pair = await deploy("MockUniswapTokenPair", [ + "1865335786147", + "8202340665419053945756", + "1593755855", + "119785032308978310142960133641565753500432674230537", + "5820053774558372823476814618189", + ]); + + // Initialize DAI_ETH pair with values from mainnet + const dai_eth_pair = await deploy("MockUniswapTokenPair", [ + "3435618131150076101237553", + "15407572689721099289685", + "1593754275", + "100715171900432184428711184053633835098", + "5069668089169215245120760905619375569156736", + ]); + + // Initialize REP_ETH pair with values from mainnet + const rep_eth_pair = await deploy("MockUniswapTokenPair", [ + "40867690797665090689823", + "3089126268851209725535", + "1593751741", + "1326188372862607823298077160955402643895", + "315226499991023307900665225550194785606382", + ]); + + // Initialize BAT_ETH pair with values from mainnet + const bat_eth_pair = await deploy("MockUniswapTokenPair", [ + "2809215824116494014601294", + "3000910749924336260251", + "1593751965", + "22657836903223019490474748660313426663", + "22353658718734403427774753736831427982055589" + ]); + + // Initialize ETH_ZRX pair with values from mainnet + // Reversed market + const eth_zrx_pair = await deploy("MockUniswapTokenPair", [ + "259245497861929182740", + "164221696097447914276729", + "1593752326", + "13610654639402610907794611037761488370001743", + "30665287778536822167996154892216941694", + ]); + + // Initialize WBTC_ETH pair with values from mainnet + const wbtc_eth_pair = await deploy("MockUniswapTokenPair", [ + "4744946699", + "1910114633221652017296", + "1593753186", + "8436575757851690213986884101797344191977744209825804", + "49529064100184996951568929095", + ]); + + // Initialize COMP_ETH pair with values from mainnet + const comp_eth_pair = await deploy("MockUniswapTokenPair", [ + "2726069269242972517844", + "2121223809443892142647", + "1593738503", + "7047295063332907798400663297656723228030", + "10471832919000882624476515664573920963717" + ]) + + // Initialize LINK_ETH pair with values from mainnet + const link_eth_pair = await deploy("MockUniswapTokenPair", [ + "115522168522463195428450", + "2448717634007234031730", + "1593750856", + "379784304220418702903383781057063011507", + "1098123734917468235191126600400328121343356", + ]) + + // Initialize ETH_KNC pair with values from mainnet + // Reversed market + const eth_knc_pair = await deploy("MockUniswapTokenPair", [ + "2071741256888346573966", + "283551022700267758624550", + "1593751102", + "5224005871622835504950986888037007421616163", + "84792274943467211540214022183090944437", + ]) + + return { + USDC_ETH: usdc_eth_pair._address, + DAI_ETH: dai_eth_pair._address, + REP_ETH: rep_eth_pair._address, + BAT_ETH: bat_eth_pair._address, + ETH_ZRX: eth_zrx_pair._address, + WBTC_ETH: wbtc_eth_pair._address, + COMP_ETH: comp_eth_pair._address, + ETH_KNC: eth_knc_pair._address, + LINK_ETH: link_eth_pair._address, + } +} + +async function setupUniswapAnchoredView() { + const reporter = "0xfCEAdAFab14d46e20144F48824d0C09B1a03F2BC"; + const anchorMantissa = numToHex(1e17); //1e17 equates to 10% tolerance for source price to be above or below anchor + const priceData = await deploy("OpenOraclePriceData", []); + const anchorPeriod = 30 * 60; + + const pairs = await setupTokenPairs(); + const tokenConfigs = [ + {cToken: address(1), underlying: address(1), symbolHash: keccak256("ETH"), baseUnit: uint(1e18), priceSource: PriceSource.REPORTER, fixedPrice: 0, uniswapMarket: pairs.USDC_ETH, isUniswapReversed: true}, + {cToken: address(2), underlying: address(2), symbolHash: keccak256("DAI"), baseUnit: uint(1e18), priceSource: PriceSource.REPORTER, fixedPrice: 0, uniswapMarket: pairs.DAI_ETH, isUniswapReversed: false}, + {cToken: address(3), underlying: address(3), symbolHash: keccak256("REP"), baseUnit: uint(1e18), priceSource: PriceSource.REPORTER, fixedPrice: 0, uniswapMarket: pairs.REP_ETH, isUniswapReversed: false}, + {cToken: address(4), underlying: address(4), symbolHash: keccak256("BAT"), baseUnit: uint(1e18), priceSource: PriceSource.REPORTER, fixedPrice: 0, uniswapMarket: pairs.BAT_ETH, isUniswapReversed: false}, + {cToken: address(5), underlying: address(5), symbolHash: keccak256("ZRX"), baseUnit: uint(1e18), priceSource: PriceSource.REPORTER, fixedPrice: 0, uniswapMarket: pairs.ETH_ZRX, isUniswapReversed: true}, + {cToken: address(6), underlying: address(6), symbolHash: keccak256("BTC"), baseUnit: uint(1e8), priceSource: PriceSource.REPORTER, fixedPrice: 0, uniswapMarket: pairs.WBTC_ETH, isUniswapReversed: false}, + {cToken: address(7), underlying: address(7), symbolHash: keccak256("COMP"), baseUnit: uint(1e18), priceSource: PriceSource.REPORTER, fixedPrice: 0, uniswapMarket: pairs.COMP_ETH, isUniswapReversed: false}, + {cToken: address(8), underlying: address(8), symbolHash: keccak256("KNC"), baseUnit: uint(1e18), priceSource: PriceSource.REPORTER, fixedPrice: 0, uniswapMarket: pairs.ETH_KNC, isUniswapReversed: true}, + {cToken: address(9), underlying: address(9), symbolHash: keccak256("LINK"), baseUnit: uint(1e18), priceSource: PriceSource.REPORTER, fixedPrice: 0, uniswapMarket: pairs.LINK_ETH, isUniswapReversed: false}, + ]; + + const uniswapAnchoredView = await deploy("UniswapAnchoredView", [priceData._address, reporter, anchorMantissa, anchorPeriod, tokenConfigs]); + return [uniswapAnchoredView, pairs]; +} + +const PriceSource = { + FIXED_ETH: 0, + FIXED_USD: 1, + REPORTER: 2 +}; + +describe("UniswapAnchoredView, postPrices test", () => { + + it("basic scenario, use real world data", async () => { + const [uniswapAnchoredView, _] = await setupUniswapAnchoredView(); + await sendRPC(web3, "evm_increaseTime", [31 * 60]); + + const messages = [ + "0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000005efebe9800000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000021e69e1300000000000000000000000000000000000000000000000000000000000000006707269636573000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000034254430000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000005efebe9800000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000d84ec180000000000000000000000000000000000000000000000000000000000000006707269636573000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000034554480000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000005efebe2000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000f81f90000000000000000000000000000000000000000000000000000000000000006707269636573000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000034441490000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000005efebe9800000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000010798780000000000000000000000000000000000000000000000000000000000000006707269636573000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000035245500000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000005efebe9800000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000005707f0000000000000000000000000000000000000000000000000000000000000006707269636573000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000035a52580000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000005efebe9800000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000003b8920000000000000000000000000000000000000000000000000000000000000006707269636573000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000034241540000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000005efebe9800000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000018f18c0000000000000000000000000000000000000000000000000000000000000006707269636573000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000034b4e430000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000005efebe9800000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000049208c0000000000000000000000000000000000000000000000000000000000000006707269636573000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044c494e4b00000000000000000000000000000000000000000000000000000000", + ]; + + const signatures = [ + "0xe64be3c6153c0f450062ebb3bb93b48d8e2bec11030dbe7b639ab6c78a5edf688f4fd9ba51c7589c15e384b952a2dc4943d402871163aca12197fd08568c4b9f000000000000000000000000000000000000000000000000000000000000001c", + "0xb8ba87c37228468f9d107a97eeb92ebd49a50993669cab1737fea77e5b884f2591affbf4058bcfa29e38756021deeafaeeab7a5c4f5ce584c7d1e12346c88d4e000000000000000000000000000000000000000000000000000000000000001b", + "0xac0731a325943a92f3745be5853f54ae110d889a408805e076ad9b5bc0bb1f4c1a994aebfb27a09156234fd1b27abf0fc19f667ea48c27a0c2a5d58c0243b99b000000000000000000000000000000000000000000000000000000000000001c", + "0x77058eaa98c77df280e069fb3c751b95aff57827a730458b48d773721432c42293f3b5d21b475169a59140e31196d333ca0f2b5935d2d86563df7a4e5cbc1e24000000000000000000000000000000000000000000000000000000000000001c", + "0x4758081209589c08db900e84d32393b3a37d9041a8e722e977dc3fff8affe0847d1dcb5c80496ea2f9314b3d6fcde03f8e7a896ba28dde6e2a003972da9e2b25000000000000000000000000000000000000000000000000000000000000001b", + "0x8f69f85dc792d657238a9a766b3cdb6c17c789d24a2b4c0d59eec079602aecfaf3d79d87a4ed430ef6889c46e03c82b5c616f9b968c4b757a89c1792a02dee13000000000000000000000000000000000000000000000000000000000000001b", + "0x25005922d67f6f446667de7e3052a2e97cf6b74bd01be62b478e16e3d72a3ecc5582fb44a3501fa359c2d6b5794844713c584740938e4012a1b0e3371e61a8a6000000000000000000000000000000000000000000000000000000000000001b", + "0xe393df120a0d95b8dea2ab693e4c89b4faf867c66636305bb1199e53cff95f43a22889e148a5bd85105173a102c3167f7716d4eca6a89a0120bef6479e812011000000000000000000000000000000000000000000000000000000000000001b", + ]; + + // No data for COMP from Coinbase so far, it is not added to the oracle yet + const symbols = ["BTC", "ETH", "DAI", "REP", "ZRX", "BAT", "KNC", "LINK"]; + + await send(uniswapAnchoredView, "postPrices", [ + messages, + signatures, + symbols, + ]); + + const btc_price = await call(uniswapAnchoredView, "price", ["BTC"]); + expect(btc_price).toBe("9100190000"); + + const eth_price = await call(uniswapAnchoredView, "price", ["ETH"]); + expect(eth_price).toBe("226815000"); + + const dai_price = await call(uniswapAnchoredView, "price", ["DAI"]); + expect(dai_price).toBe("1016313"); + + const rep_price = await call(uniswapAnchoredView, "price", ["REP"]); + expect(rep_price).toBe("17275000"); + + const zrx_price = await call(uniswapAnchoredView, "price", ["ZRX"]); + expect(zrx_price).toBe("356479"); + + const bat_price = await call(uniswapAnchoredView, "price", ["BAT"]); + expect(bat_price).toBe("243858"); + + const knc_price = await call(uniswapAnchoredView, "price", ["KNC"]); + expect(knc_price).toBe("1634700"); + + const link_price = await call(uniswapAnchoredView, "price", ["LINK"]); + expect(link_price).toBe("4792460"); + }); + + it("test price events", async () => { + const [uniswapAnchoredView, pairs] = await setupUniswapAnchoredView(); + await sendRPC(web3, "evm_increaseTime", [31 * 60]); + + const messages = [ + "0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000005efebe9800000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000021e69e1300000000000000000000000000000000000000000000000000000000000000006707269636573000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000034254430000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000005efebe9800000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000d84ec180000000000000000000000000000000000000000000000000000000000000006707269636573000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000034554480000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000005efebe2000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000f81f90000000000000000000000000000000000000000000000000000000000000006707269636573000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000034441490000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000005efebe9800000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000010798780000000000000000000000000000000000000000000000000000000000000006707269636573000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000035245500000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000005efebe9800000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000005707f0000000000000000000000000000000000000000000000000000000000000006707269636573000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000035a52580000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000005efebe9800000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000003b8920000000000000000000000000000000000000000000000000000000000000006707269636573000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000034241540000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000005efebe9800000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000018f18c0000000000000000000000000000000000000000000000000000000000000006707269636573000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000034b4e430000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000005efebe9800000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000049208c0000000000000000000000000000000000000000000000000000000000000006707269636573000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044c494e4b00000000000000000000000000000000000000000000000000000000", + ]; + + const signatures = [ + "0xe64be3c6153c0f450062ebb3bb93b48d8e2bec11030dbe7b639ab6c78a5edf688f4fd9ba51c7589c15e384b952a2dc4943d402871163aca12197fd08568c4b9f000000000000000000000000000000000000000000000000000000000000001c", + "0xb8ba87c37228468f9d107a97eeb92ebd49a50993669cab1737fea77e5b884f2591affbf4058bcfa29e38756021deeafaeeab7a5c4f5ce584c7d1e12346c88d4e000000000000000000000000000000000000000000000000000000000000001b", + "0xac0731a325943a92f3745be5853f54ae110d889a408805e076ad9b5bc0bb1f4c1a994aebfb27a09156234fd1b27abf0fc19f667ea48c27a0c2a5d58c0243b99b000000000000000000000000000000000000000000000000000000000000001c", + "0x77058eaa98c77df280e069fb3c751b95aff57827a730458b48d773721432c42293f3b5d21b475169a59140e31196d333ca0f2b5935d2d86563df7a4e5cbc1e24000000000000000000000000000000000000000000000000000000000000001c", + "0x4758081209589c08db900e84d32393b3a37d9041a8e722e977dc3fff8affe0847d1dcb5c80496ea2f9314b3d6fcde03f8e7a896ba28dde6e2a003972da9e2b25000000000000000000000000000000000000000000000000000000000000001b", + "0x8f69f85dc792d657238a9a766b3cdb6c17c789d24a2b4c0d59eec079602aecfaf3d79d87a4ed430ef6889c46e03c82b5c616f9b968c4b757a89c1792a02dee13000000000000000000000000000000000000000000000000000000000000001b", + "0x25005922d67f6f446667de7e3052a2e97cf6b74bd01be62b478e16e3d72a3ecc5582fb44a3501fa359c2d6b5794844713c584740938e4012a1b0e3371e61a8a6000000000000000000000000000000000000000000000000000000000000001b", + "0xe393df120a0d95b8dea2ab693e4c89b4faf867c66636305bb1199e53cff95f43a22889e148a5bd85105173a102c3167f7716d4eca6a89a0120bef6479e812011000000000000000000000000000000000000000000000000000000000000001b", + ]; + + // No data for COMP from Coinbase so far, it is not added to the oracle yet + const symbols = ["BTC", "ETH", "DAI", "REP", "ZRX", "BAT", "KNC", "LINK"]; + + const postRes = await send(uniswapAnchoredView, "postPrices", [ + messages, + signatures, + symbols, + ]); + + const anchorEvents = postRes.events.AnchorPriceUpdate; + const priceUpdatedEvents = postRes.events.PriceUpdated; + const priceGuardedEvents = postRes.events.PriceGuarded; + + // All prices were updated + expect(priceGuardedEvents).toBe(undefined); + + // Check price updates + priceUpdatedEvents.forEach((updateEvent) => { + if (updateEvent.returnValues.symbol == "BTC") { + expect(updateEvent.returnValues.price).toBe("9100190000"); + } + if (updateEvent.returnValues.symbol == "ETH") { + expect(updateEvent.returnValues.price).toBe("226815000"); + } + if (updateEvent.returnValues.symbol == "DAI") { + expect(updateEvent.returnValues.price).toBe("1016313"); + } + if (updateEvent.returnValues.symbol == "ZRX") { + expect(updateEvent.returnValues.price).toBe("356479"); + } + if (updateEvent.returnValues.symbol == "REP") { + expect(updateEvent.returnValues.price).toBe("17275000"); + } + if (updateEvent.returnValues.symbol == "BAT") { + expect(updateEvent.returnValues.price).toBe("243858"); + } + if (updateEvent.returnValues.symbol == "KNC") { + expect(updateEvent.returnValues.price).toBe("1634700"); + } + if (updateEvent.returnValues.symbol == "LINK") { + expect(updateEvent.returnValues.price).toBe("4792460"); + } + }); + + // Check anchor prices + anchorEvents.forEach((anchorEvent) => { + if (anchorEvent.returnValues.uniswapMarket == pairs.USDC_ETH) { + expect(anchorEvent.returnValues.anchorPrice).toBe("227415058"); + } + if (anchorEvent.returnValues.uniswapMarket == pairs.DAI_ETH) { + expect(anchorEvent.returnValues.anchorPrice).toBe("1019878"); + } + if (anchorEvent.returnValues.uniswapMarket == pairs.REP_ETH) { + expect(anchorEvent.returnValues.anchorPrice).toBe("17189956"); + } + if (anchorEvent.returnValues.uniswapMarket == pairs.BAT_ETH) { + expect(anchorEvent.returnValues.anchorPrice).toBe("242933"); + } + if (anchorEvent.returnValues.uniswapMarket == pairs.ETH_ZRX) { + expect(anchorEvent.returnValues.anchorPrice).toBe("359004"); + } + if (anchorEvent.returnValues.uniswapMarket == pairs.WBTC_ETH) { + expect(anchorEvent.returnValues.anchorPrice).toBe("9154767327"); + } + if (anchorEvent.returnValues.uniswapMarket == pairs.ETH_KNC) { + expect(anchorEvent.returnValues.anchorPrice).toBe("1661588"); + } + if (anchorEvent.returnValues.uniswapMarket == pairs.LINK_ETH) { + expect(anchorEvent.returnValues.anchorPrice).toBe("4820505"); + } + }); + }); + + it("test uniswap window events", async () => { + const [uniswapAnchoredView, _] = await setupUniswapAnchoredView(); + await sendRPC(web3, "evm_increaseTime", [31 * 60]); + + const messages1 = [ + "0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000005efebe9800000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000021e69e1300000000000000000000000000000000000000000000000000000000000000006707269636573000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000034254430000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000005efebe9800000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000d84ec180000000000000000000000000000000000000000000000000000000000000006707269636573000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000034554480000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000005efebe2000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000f81f90000000000000000000000000000000000000000000000000000000000000006707269636573000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000034441490000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000005efebe9800000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000010798780000000000000000000000000000000000000000000000000000000000000006707269636573000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000035245500000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000005efebe9800000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000005707f0000000000000000000000000000000000000000000000000000000000000006707269636573000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000035a52580000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000005efebe9800000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000003b8920000000000000000000000000000000000000000000000000000000000000006707269636573000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000034241540000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000005efebe9800000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000018f18c0000000000000000000000000000000000000000000000000000000000000006707269636573000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000034b4e430000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000005efebe9800000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000049208c0000000000000000000000000000000000000000000000000000000000000006707269636573000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044c494e4b00000000000000000000000000000000000000000000000000000000", + ]; + + const signatures1 = [ + "0xe64be3c6153c0f450062ebb3bb93b48d8e2bec11030dbe7b639ab6c78a5edf688f4fd9ba51c7589c15e384b952a2dc4943d402871163aca12197fd08568c4b9f000000000000000000000000000000000000000000000000000000000000001c", + "0xb8ba87c37228468f9d107a97eeb92ebd49a50993669cab1737fea77e5b884f2591affbf4058bcfa29e38756021deeafaeeab7a5c4f5ce584c7d1e12346c88d4e000000000000000000000000000000000000000000000000000000000000001b", + "0xac0731a325943a92f3745be5853f54ae110d889a408805e076ad9b5bc0bb1f4c1a994aebfb27a09156234fd1b27abf0fc19f667ea48c27a0c2a5d58c0243b99b000000000000000000000000000000000000000000000000000000000000001c", + "0x77058eaa98c77df280e069fb3c751b95aff57827a730458b48d773721432c42293f3b5d21b475169a59140e31196d333ca0f2b5935d2d86563df7a4e5cbc1e24000000000000000000000000000000000000000000000000000000000000001c", + "0x4758081209589c08db900e84d32393b3a37d9041a8e722e977dc3fff8affe0847d1dcb5c80496ea2f9314b3d6fcde03f8e7a896ba28dde6e2a003972da9e2b25000000000000000000000000000000000000000000000000000000000000001b", + "0x8f69f85dc792d657238a9a766b3cdb6c17c789d24a2b4c0d59eec079602aecfaf3d79d87a4ed430ef6889c46e03c82b5c616f9b968c4b757a89c1792a02dee13000000000000000000000000000000000000000000000000000000000000001b", + "0x25005922d67f6f446667de7e3052a2e97cf6b74bd01be62b478e16e3d72a3ecc5582fb44a3501fa359c2d6b5794844713c584740938e4012a1b0e3371e61a8a6000000000000000000000000000000000000000000000000000000000000001b", + "0xe393df120a0d95b8dea2ab693e4c89b4faf867c66636305bb1199e53cff95f43a22889e148a5bd85105173a102c3167f7716d4eca6a89a0120bef6479e812011000000000000000000000000000000000000000000000000000000000000001b", + ]; + + const messages2 = [ + "0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000005effbf7800000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000021cd92f100000000000000000000000000000000000000000000000000000000000000006707269636573000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000034254430000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000005effbf7800000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000d6e56d80000000000000000000000000000000000000000000000000000000000000006707269636573000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000034554480000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000005effbf7800000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000f7f660000000000000000000000000000000000000000000000000000000000000006707269636573000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000034441490000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000005effbf7800000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000116ee400000000000000000000000000000000000000000000000000000000000000006707269636573000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000035245500000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000005effbf7800000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000005cd4e0000000000000000000000000000000000000000000000000000000000000006707269636573000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000035a52580000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000005effbf0000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000003aff50000000000000000000000000000000000000000000000000000000000000006707269636573000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000034241540000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000005effbf7800000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000001c6a6a0000000000000000000000000000000000000000000000000000000000000006707269636573000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000034b4e430000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000005effbf7800000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000004895e00000000000000000000000000000000000000000000000000000000000000006707269636573000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044c494e4b00000000000000000000000000000000000000000000000000000000", + ]; + + const signatures2 = [ + "0xbd6866cafc46a9f55ad102830a57e807d797ed54d2cf1a689e527b054f1103d860a712d1e6e7f3e1f0e57a263f8a195c8484afdade8e23984d23d8c023bc9dd8000000000000000000000000000000000000000000000000000000000000001c", + "0xadbca52dc0ecc378d65b540f69e42f6d4879907b8713371758e8dffa02ba4e8eae2a10459600ad3784fe0aac34c0adda164a649cc8b5e713524d349bdddf4b64000000000000000000000000000000000000000000000000000000000000001b", + "0xc7b7b4b9411a06f623ed6549c3f314b6bf1d39af7d42a3131fbe1e99ddcb4bb0f494788fd01a58a33e567b52345d8889e0ae5eeffeba91e57b718ae7b6d77485000000000000000000000000000000000000000000000000000000000000001c", + "0x94dfe8cab9eb31f68e926863da610a7764f7153d5d599dc2382cbb0e1343452e5fa0f9da9f812470b54ce806e596688cc2b9c3c25c300060fdb06bca92a6e668000000000000000000000000000000000000000000000000000000000000001b", + "0x0c4c504ff54a157548c81d96369bd7f3245e7a4fe66cd142cdc9d54a5361eef16d19b925af77c85e49c8389410afa6864c210ed6440939fc4190dec0d49b4ad7000000000000000000000000000000000000000000000000000000000000001c", + "0x6eea7c84b145877f1c133062a3bc718d2c6ea5806e16ec179b5336ddd96bd47c4cf64cd8475cec87237d5a3715d99409d18d05375ad1fb1996b47d356e59b8ff000000000000000000000000000000000000000000000000000000000000001b", + "0xafa764b8e63866b81853c8d74e380a8cc7cd14cf2aed22df306f6c4931801a1986ea34f54d4de25f4f3f6a4e968abf42371a6ad3e72b90b2027dc63212fededb000000000000000000000000000000000000000000000000000000000000001b", + "0x039f30fb49b2f2badad1e3c5df00f2c5c2124c2a1bd06da56467aea45ebf89a027525cc7bfa776452171cb5865e74ef0c04ea6ef18d6ca2e556a0686af658803000000000000000000000000000000000000000000000000000000000000001b", + ]; + + // No data for COMP from Coinbase so far, it is not added to the oracle yet + const symbols = ["BTC", "ETH", "DAI", "REP", "ZRX", "BAT", "KNC", "LINK"]; + + const postRes1 = await send(uniswapAnchoredView, "postPrices", [ + messages1, + signatures1, + symbols, + ]); + const uniswapWindowEvents1 = postRes1.events.UniswapWindowUpdate; + + uniswapWindowEvents1.forEach((windowUpdate) => { + expect(windowUpdate.returnValues.newTimestamp).toBe( + windowUpdate.returnValues.oldTimestamp + ); + }); + + await sendRPC(web3, "evm_increaseTime", [31 * 60]); + const postRes2 = await send(uniswapAnchoredView, "postPrices", [ + messages2, + signatures2, + symbols, + ]); + const uniswapWindowEvents2 = postRes2.events.UniswapWindowUpdate; + + uniswapWindowEvents2.forEach((windowUpdate) => { + const elapsedTime = + windowUpdate.returnValues.newTimestamp - + windowUpdate.returnValues.oldTimestamp; + // Give an extra 5 seconds safety delay, but time difference should be around 31 minutes + 0/1 second + expect(elapsedTime >= 31 * 60 && elapsedTime < 31 * 60 + 5).toBe(true); + }); + }); +}); diff --git a/tests/UniswapAnchoredViewTest.js b/tests/UniswapAnchoredViewTest.js new file mode 100644 index 00000000..43dc9ecc --- /dev/null +++ b/tests/UniswapAnchoredViewTest.js @@ -0,0 +1,171 @@ +const { encode, sign } = require('../sdk/javascript/.tsbuilt/reporter'); +const { uint, keccak256, time, numToHex, address } = require('./Helpers'); +const BigNumber = require('bignumber.js'); + +async function setup(opts) { + ({isMockedView} = opts); + const reporter = web3.eth.accounts.privateKeyToAccount('0x177ee777e72b8c042e05ef41d1db0f17f1fcb0e8150b37cfad6993e4373bdf10'); + const anchorMantissa = numToHex(1e17); + const priceData = await deploy('OpenOraclePriceData', []); + const anchorPeriod = 60; + + const FIXED_ETH_AMOUNT = 0.005e18; + + const dummyPair = await deploy("MockUniswapTokenPair", [ + "157323115357569242624896", + "10627310410144389510631", + "1592425041", + "819360504021542874838907395123146706291", + "221435982014902761159721791828816348231935", + ]); + + const priceSource = {FIXED_ETH: 0, FIXED_USD: 1, REPORTER: 2}; + const cToken = {ETH: address(1), DAI: address(2), REP: address(3), USDT: address(4), SAI: address(5), WBTC: address(6)}; + const dummyAddress = address(0); + const tokenConfigs = [ + {cToken: cToken.ETH, underlying: dummyAddress, symbolHash: keccak256('ETH'), baseUnit: uint(1e18), priceSource: priceSource.REPORTER, fixedPrice: 0, uniswapMarket: dummyPair._address, isUniswapReversed: true}, + {cToken: cToken.DAI, underlying: dummyAddress, symbolHash: keccak256('DAI'), baseUnit: uint(1e18), priceSource: priceSource.REPORTER, fixedPrice: 0, uniswapMarket: dummyPair._address, isUniswapReversed: false}, + {cToken: cToken.REP, underlying: dummyAddress, symbolHash: keccak256('REP'), baseUnit: uint(1e18), priceSource: priceSource.REPORTER, fixedPrice: 0, uniswapMarket: dummyPair._address, isUniswapReversed: false}, + {cToken: cToken.USDT, underlying: dummyAddress, symbolHash: keccak256('USDT'), baseUnit: uint(1e6), priceSource: priceSource.FIXED_USD, fixedPrice: uint(1e6), uniswapMarket: address(0), isUniswapReversed: false}, + {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}, + {cToken: cToken.WBTC, underlying: dummyAddress, symbolHash: keccak256('WBTC'), baseUnit: uint(1e8), priceSource: priceSource.REPORTER, fixedPrice: 0, uniswapMarket: dummyPair._address, isUniswapReversed: false}, + ]; + + let uniswapAnchoredView; + if (isMockedView) { + uniswapAnchoredView = await deploy('MockUniswapAnchoredView', [priceData._address, reporter.address, anchorMantissa, anchorPeriod, tokenConfigs]); + } else { + uniswapAnchoredView = await deploy('UniswapAnchoredView', [priceData._address, reporter.address, anchorMantissa, anchorPeriod, tokenConfigs]); + } + + async function postPrices(timestamp, prices2dArr, symbols, signer = reporter) { + const messages = [], + signatures = []; + + prices2dArr.forEach((prices, i) => { + const signed = sign( + encode( + 'prices', + timestamp, + prices + ), + signer.privateKey + ); + for (let { message, signature } of signed) { + messages.push(message); + signatures.push(signature); + } + }); + return send(uniswapAnchoredView, 'postPrices', [messages, signatures, symbols]); + } + return {reporter, anchorMantissa, priceData, anchorPeriod, uniswapAnchoredView, tokenConfigs, postPrices, cToken}; +} + +describe('UniswapAnchoredView', () => { + let cToken, reporter, anchorMantissa, priceData, anchorPeriod, uniswapAnchoredView, tokenConfigs, postPrices; + + describe('postPrices Unit Test', () => { + beforeEach(async done => { + ({reporter, anchorMantissa, priceData, uniswapAnchoredView, postPrices} = await setup({isMockedView: true})); + done(); + }) + + it('should not update view if sender is not reporter', async () => { + const timestamp = time() - 5; + const nonSource = web3.eth.accounts.privateKeyToAccount('0x666ee777e72b8c042e05ef41d1db0f17f1fcb0e8150b37cfad6993e4373bdf10'); + const tx = await postPrices(timestamp, [[['ETH', 91]]], ['ETH'], nonSource); + expect(tx.events.PriceGuarded).toBe(undefined); + expect(tx.events.PricePosted).toBe(undefined); + expect(await call(uniswapAnchoredView, 'prices', [keccak256('ETH')])).numEquals(0); + }); + + it('should update view if ETH price is within anchor bounds', async () => { + const timestamp = time() - 5; + await send(uniswapAnchoredView, 'setAnchorPrice', ['ETH', 91e6]); + const tx = await postPrices(timestamp, [[['ETH', 91]]], ['ETH']); + + expect(tx.events.PriceGuarded).toBe(undefined); + expect(tx.events.PriceUpdated).not.toBe(undefined); + expect(await call(uniswapAnchoredView, 'prices', [keccak256('ETH')])).numEquals(91e6); + expect(await call(priceData, 'getPrice', [reporter.address, 'ETH'])).numEquals(91e6); + }); + + it('should not update view if ETH price is below anchor bounds', async () => { + // anchorMantissa is 1e17, so 10% tolerance + const timestamp = time() - 5; + await send(uniswapAnchoredView, 'setAnchorPrice', ['ETH', 89.9e6]); + const tx = await postPrices(timestamp, [[['ETH', 100]]], ['ETH']); + + expect(tx.events.PriceGuarded).not.toBe(undefined); + expect(tx.events.PriceUpdated).toBe(undefined); + expect(await call(uniswapAnchoredView, 'prices', [keccak256('ETH')])).numEquals(0); + expect(await call(priceData, 'getPrice', [reporter.address, 'ETH'])).numEquals(100e6); + }); + + it('should not update view if ETH price is above anchor bounds', async () => { + // anchorMantissa is 1e17, so 10% tolerance + const timestamp = time() - 5; + await send(uniswapAnchoredView, 'setAnchorPrice', ['ETH', 110.1e6]); + const tx = await postPrices(timestamp, [[['ETH', 100]]], ['ETH']); + + expect(tx.events.PriceGuarded).not.toBe(undefined); + expect(tx.events.PriceUpdated).toBe(undefined); + expect(await call(uniswapAnchoredView, 'prices', [keccak256('ETH')])).numEquals(0); + expect(await call(priceData, 'getPrice', [reporter.address, 'ETH'])).numEquals(100e6); + }); + + it.todo('test anchor with non-eth prices') + + it.todo('should invalidate reporter'); + + }); + + describe('getUnderlyingPrice', () => { + // everything must return 1e36 - underlying units + + beforeEach(async done => { + ({cToken, uniswapAnchoredView, postPrices} = await setup({isMockedView: true})); + done(); + }) + + it('should work correctly for USDT fixed USD price source', async () => { + // 1 * (1e(36 - 6)) = 1e30 + let expected = new BigNumber('1e30'); + expect(await call(uniswapAnchoredView, 'getUnderlyingPrice', [cToken.USDT])).numEquals(expected.toFixed()); + }); + + it('should return fixed ETH amount if SAI', async () => { + const timestamp = time() - 5; + await send(uniswapAnchoredView, 'setAnchorPrice', ['ETH', 200e6]); + const tx = await postPrices(timestamp, [[['ETH', 200]]], ['ETH']); + // priceInternal: returns 200e6 * 0.005e18 / 1e18 = 1e6 + // getUnderlyingPrice: 1e30 * 1e6 / 1e18 = 1e18 + expect(await call(uniswapAnchoredView, 'getUnderlyingPrice', [cToken.SAI])).numEquals(1e18); + }); + + it('should return reported ETH price', async () => { + const timestamp = time() - 5; + await send(uniswapAnchoredView, 'setAnchorPrice', ['ETH', 200e6]); + const tx = await postPrices(timestamp, [[['ETH', 200]]], ['ETH']); + // priceInternal: returns 200e6 + // getUnderlyingPrice: 1e30 * 200e6 / 1e18 = 200e18 + expect(await call(uniswapAnchoredView, 'getUnderlyingPrice', [cToken.ETH])).numEquals(200e18); + }); + + it('should return reported WBTC price', async () => { + const timestamp = time() - 5; + await send(uniswapAnchoredView, 'setAnchorPrice', ['ETH', 200e6]); + await send(uniswapAnchoredView, 'setAnchorPrice', ['WBTC', 10000e6]); + + const tx = await postPrices(timestamp, [[['ETH', 200], ['WBTC', 10000]]], ['ETH', 'WBTC']); + const wbtcPrice = await call(uniswapAnchoredView, 'prices', [keccak256('WBTC')]); + + expect(wbtcPrice).numEquals(10000e6); + // priceInternal: returns 10000e6 + // getUnderlyingPrice: 1e30 * 10000e6 / 1e8 = 1e32 + let expected = new BigNumber('1e32'); + expect(await call(uniswapAnchoredView, 'getUnderlyingPrice', [cToken.WBTC])).numEquals(expected.toFixed()); + }); + + }); +}); diff --git a/tests/UniswapConfigTest.js b/tests/UniswapConfigTest.js new file mode 100644 index 00000000..d51834ba --- /dev/null +++ b/tests/UniswapConfigTest.js @@ -0,0 +1,99 @@ +function address(n) { + return `0x${n.toString(16).padStart(40, '0')}`; +} + +function keccak256(str) { + return web3.utils.keccak256(str); +} + +function uint(n) { + return web3.utils.toBN(n).toString(); +} + +describe('UniswapConfig', () => { + it('basically works', async () => { + const contract = await deploy('UniswapConfig', [[ + {cToken: address(1), underlying: address(0), symbolHash: keccak256('ETH'), baseUnit: uint(1e18), priceSource: 0, fixedPrice: 0, uniswapMarket: address(6), isUniswapReversed: false}, + {cToken: address(2), underlying: address(3), symbolHash: keccak256('BTC'), baseUnit: uint(1e18), priceSource: 1, fixedPrice: 1, uniswapMarket: address(7), isUniswapReversed: true} + ]]); + + const cfg0 = await call(contract, 'getTokenConfig', [0]); + const cfg1 = await call(contract, 'getTokenConfig', [1]); + const cfgETH = await call(contract, 'getTokenConfigBySymbol', ['ETH']); + const cfgBTC = await call(contract, 'getTokenConfigBySymbol', ['BTC']); + const cfgCT0 = await call(contract, 'getTokenConfigByCToken', [address(1)]); + const cfgCT1 = await call(contract, 'getTokenConfigByCToken', [address(2)]); + expect(cfg0).toEqual(cfgETH); + expect(cfgETH).toEqual(cfgCT0); + expect(cfg1).toEqual(cfgBTC); + expect(cfgBTC).toEqual(cfgCT1); + expect(cfg0).not.toEqual(cfg1); + + await expect(call(contract, 'getTokenConfig', [2])).rejects.toRevert('revert token config not found'); + await expect(call(contract, 'getTokenConfigBySymbol', ['COMP'])).rejects.toRevert('revert token config not found'); + await expect(call(contract, 'getTokenConfigByCToken', [address(3)])).rejects.toRevert('revert'); // not a ctoken + }); + + it('returns configs exactly as specified', async () => { + const symbols = Array(30).fill(0).map((_, i) => String.fromCharCode('a'.charCodeAt(0) + i)); + const configs = symbols.map((symbol, i) => { + 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} + }); + const contract = await deploy('UniswapConfig', [configs]); + + await Promise.all(configs.map(async (config, i) => { + const cfgByIndex = await call(contract, 'getTokenConfig', [i]); + const cfgBySymbol = await call(contract, 'getTokenConfigBySymbol', [symbols[i]]); + const cfgByCToken = await call(contract, 'getTokenConfigByCToken', [address(i + 1)]); + expect({ + cToken: cfgByIndex.cToken.toLowerCase(), + underlying: cfgByIndex.underlying.toLowerCase(), + symbolHash: cfgByIndex.symbolHash, + baseUnit: cfgByIndex.baseUnit, + priceSource: cfgByIndex.priceSource, + fixedPrice: cfgByIndex.fixedPrice, + uniswapMarket: cfgByIndex.uniswapMarket.toLowerCase(), + isUniswapReversed: cfgByIndex.isUniswapReversed + }).toEqual({ + cToken: config.cToken, + underlying: config.underlying, + symbolHash: config.symbolHash, + baseUnit: `${config.baseUnit}`, + priceSource: `${config.priceSource}`, + fixedPrice: `${config.fixedPrice}`, + uniswapMarket: config.uniswapMarket, + isUniswapReversed: config.isUniswapReversed + }); + expect(cfgByIndex).toEqual(cfgBySymbol); + expect(cfgBySymbol).toEqual(cfgByCToken); + })); + }); + + it('checks gas', async () => { + const configs = Array(26).fill(0).map((_, i) => { + const symbol = String.fromCharCode('a'.charCodeAt(0) + i); + 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} + }); + const contract = await deploy('UniswapConfig', [configs]); + + const cfg9 = await call(contract, 'getTokenConfig', [9]); + const tx9 = await send(contract, 'getTokenConfig', [9]); + expect(cfg9.underlying).toEqual(address(9)); + expect(tx9.gasUsed).toEqual(22619); + + const cfg25 = await call(contract, 'getTokenConfig', [25]); + const tx25 = await send(contract, 'getTokenConfig', [25]); + expect(cfg25.underlying).toEqual(address(25)); + expect(tx25.gasUsed).toEqual(23035); + + const cfgZ = await call(contract, 'getTokenConfigBySymbol', ['z']); + const txZ = await send(contract, 'getTokenConfigBySymbol', ['z']); + expect(cfgZ.underlying).toEqual(address(25)); + expect(txZ.gasUsed).toEqual(25273); + + const cfgCT26 = await call(contract, 'getTokenConfigByCToken', [address(26)]); + const txCT26 = await send(contract, 'getTokenConfigByCToken', [address(26)]); + expect(cfgCT26.underlying).toEqual(address(25)); + expect(txCT26.gasUsed).toEqual(25136); + }); +}); \ No newline at end of file diff --git a/tests/contracts/MockUniswapAnchoredView.sol b/tests/contracts/MockUniswapAnchoredView.sol new file mode 100644 index 00000000..43d4127c --- /dev/null +++ b/tests/contracts/MockUniswapAnchoredView.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.6.10; +pragma experimental ABIEncoderV2; + +import "../../contracts/Uniswap/UniswapAnchoredView.sol"; + +contract MockUniswapAnchoredView is UniswapAnchoredView { + mapping(bytes32 => uint) public anchorPrices; + + constructor(OpenOraclePriceData priceData_, + address reporter_, + uint anchorToleranceMantissa_, + uint anchorPeriod_, + TokenConfig[] memory configs) UniswapAnchoredView(priceData_, reporter_, anchorToleranceMantissa_, anchorPeriod_, configs) public {} + + function setAnchorPrice(string memory symbol, uint price) external { + anchorPrices[keccak256(abi.encodePacked(symbol))] = price; + } + + function fetchAnchorPrice(TokenConfig memory config, uint _conversionFactor) internal override returns (uint) { + _conversionFactor; // Shh + return anchorPrices[config.symbolHash]; + } +} diff --git a/tests/contracts/MockUniswapTokenPair.sol b/tests/contracts/MockUniswapTokenPair.sol new file mode 100644 index 00000000..e66fc094 --- /dev/null +++ b/tests/contracts/MockUniswapTokenPair.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.6.10; + +contract MockUniswapTokenPair { + uint112 public reserve0; + uint112 public reserve1; + uint32 public blockTimestampLast; + + uint256 public price0CumulativeLast; + uint256 public price1CumulativeLast; + + constructor( + uint112 reserve0_, + uint112 reserve1_, + uint32 blockTimestampLast_, + uint256 price0CumulativeLast_, + uint256 price1CumulativeLast_ + ) public { + reserve0 = reserve0_; + reserve1 = reserve1_; + blockTimestampLast = blockTimestampLast_; + price0CumulativeLast = price0CumulativeLast_; + price1CumulativeLast = price1CumulativeLast_; + } + + function update( + uint112 reserve0_, + uint112 reserve1_, + uint32 blockTimestampLast_, + uint256 price0CumulativeLast_, + uint256 price1CumulativeLast_ + ) public { + reserve0 = reserve0_; + reserve1 = reserve1_; + blockTimestampLast = blockTimestampLast_; + price0CumulativeLast = price0CumulativeLast_; + price1CumulativeLast = price1CumulativeLast_; + } + + function getReserves() external view returns(uint112, uint112, uint32) { + return (reserve0, reserve1, blockTimestampLast); + } +} diff --git a/tests/contracts/ProxyPriceOracle.sol b/tests/contracts/ProxyPriceOracle.sol index fa4b4f95..aaf57e5d 100644 --- a/tests/contracts/ProxyPriceOracle.sol +++ b/tests/contracts/ProxyPriceOracle.sol @@ -1,4 +1,6 @@ -pragma solidity ^0.6.6; +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.6.10; // @dev mock version of v1 price oracle, allowing manually setting return values contract ProxyPriceOracle { diff --git a/tests/contracts/Test.sol b/tests/contracts/Test.sol index 3ad551d9..ca27430f 100644 --- a/tests/contracts/Test.sol +++ b/tests/contracts/Test.sol @@ -1,4 +1,6 @@ -pragma solidity ^0.6.6; +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.6.10; pragma experimental ABIEncoderV2; contract TestOverflow {