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 {