Skip to content

Commit

Permalink
Anchored View/PriceOracleProxy for a single (Coinbase) reporter
Browse files Browse the repository at this point in the history
  • Loading branch information
Coburn Berry authored and jflatow committed Jun 23, 2020
1 parent cb2d092 commit c4c4683
Showing 38 changed files with 5,043 additions and 2,732 deletions.
5 changes: 5 additions & 0 deletions .build/mainnet.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"AnchoredPriceView": "0xDECee5C06B8CC6c4D85dfd8518C246fd1497b2AE",
"AnchoredView": "0x4345d279E3aeA247Ad7865cC95BeE3088eaBc433",
"OpenOraclePriceData": "0x541e90aadfbb653843ea9b5dec43fe2cceca0dd6"
}
7 changes: 5 additions & 2 deletions .build/ropsten.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"OpenOraclePriceData": "0x63189F27CeB083D88d83A2C70BEcD427e4adF972",
"DelFiPrice": "0xA7637AD217af1DD781b7DDeD452CE339cfb4a312"
"OpenOraclePriceData": "0xda019b064F451657F2c117Ad79a56F7abDbF1201",
"DelFiPrice": "0xA7637AD217af1DD781b7DDeD452CE339cfb4a312",
"AnchoredPriceView": "0x4745ed21F933C445F6021E1949817c6C281329d2",
"MockAnchorOracle": "0xF054B68676900ECB13b7107019b062662B50d3d7",
"AnchoredView": "0xcaF366e452f9613Cb3fabe094619cc1e1e2aC149"
}
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ jobs:
docker_layer_caching: true
- run:
|
sudo wget https://github.com/ethereum/solidity/releases/download/v0.5.12/solc-static-linux -O /usr/local/bin/solc
sudo wget https://github.com/ethereum/solidity/releases/download/v0.6.6/solc-static-linux -O /usr/local/bin/solc
sudo chmod +x /usr/local/bin/solc
- checkout
- restore_cache:
3 changes: 3 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -11,3 +11,6 @@ poster/.tsbuilt
tests
yarn-error.log
junit.xml

# Allow files and directories
!tests/contracts
7 changes: 4 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
FROM node:13.6.0-alpine3.10
WORKDIR /open-oracle
RUN wget https://github.com/ethereum/solidity/releases/download/v0.5.12/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.6/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 \
@@ -15,8 +15,9 @@ COPY package.json /open-oracle/package.json
RUN yarn install

ENV PROVIDER PROVIDER
ADD contracts contracts
ADD saddle.config.js saddle.config.js
COPY contracts contracts
COPY tests tests
COPY saddle.config.js saddle.config.js
RUN npx saddle compile

ENTRYPOINT []
23 changes: 14 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -5,25 +5,25 @@ The Open Oracle is a standard and SDK allowing reporters to sign key-value pairs

## Contracts

First, you will need solc 0.5.10 installed.
Additionally, you will need TypeScript installed and will need to build the project by running `tsc`.
First, you will need solc 0.6.6 installed.
Additionally for testing, you will need TypeScript installed and will need to build the open-oracle-reporter project by running `cd sdk/javascript && yarn`.

To compile everything run:
To fetch dependencies run:

```
yarn run compile
yarn install
```

To compile just the contracts run:
To compile everything run:

```
yarn run saddle:compile
yarn run compile
```

To deploy the Open Oracle locally, you can run:
To deploy contracts locally, you can run:

```
yarn run saddle:deploy --network development Oracle
yarn run deploy --network development OpenOraclePriceData
```

Note: you will need to be running an Ethereum node locally in order for this to work.
@@ -32,7 +32,7 @@ E.g., start [ganache-cli](https://github.com/trufflesuite/ganache-cli) in anothe
You can add a view in `MyView.sol` and run (default is `network=development`):

```
yarn run saddle:deploy MyView arg1 arg2 ...
yarn run deploy MyView arg1 arg2 ...
```

To run tests:
@@ -41,6 +41,11 @@ To run tests:
yarn run test
```

To track deployed contracts in a saddle console:

```
yarn run console
```
## Reporter SDK

This repository contains a set of SDKs for reporters to easily sign "reporter" data in any supported languages. We currently support the following languages:
254 changes: 254 additions & 0 deletions contracts/AnchoredView/AnchoredView.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
pragma solidity ^0.6.6;
pragma experimental ABIEncoderV2;

import "./SymbolConfiguration.sol";
import "../OpenOraclePriceData.sol";

interface AnchorOracle {
function numBlocksPerPeriod() external view returns (uint); // approximately 1 hour: 60 seconds/minute * 60 minutes/hour * 1 block/15 seconds

function assetPrices(address asset) external view returns (uint);

struct Anchor {
// floor(block.number / numBlocksPerPeriod) + 1
uint period;

// Price in ETH, scaled by 10**18
uint priceMantissa;
}
function anchors(address asset) external view returns (Anchor memory);
}


/**
* @notice Price feed conforming to Price Oracle Proxy interface.
* @dev Use a single open oracle reporter and anchored to and falling back to the Compound v2 oracle system.
* @dev The reporter must report at a minimum the USD/ETH price, so that anchor ETH/TOKEN prices can be converted to USD/TOKEN
* @author Compound Labs, Inc.
*/
contract AnchoredView is SymbolConfiguration {
/// @notice The mapping of anchored reporter prices by symbol
mapping(string => uint) public _prices;

/// @notice Circuit breaker for using anchor price oracle directly, ignoring reporter
bool public reporterBreaker;

/// @notice Circuit breaker for using reporter price without anchor
bool public anchorBreaker;

/// @notice the Open Oracle Reporter price reporter
address public immutable reporter;

/// @notice The anchor oracle ( Compound Oracle V1 )
AnchorOracle public immutable anchor;

/// @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
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
uint immutable lowerBoundAnchorRatio;

/// @notice Average blocks per day, for checking anchor staleness
/// @dev 1 day / 15
uint constant blocksInADay = 5760;

/// @notice The event emitted when the median 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 anchor is cut for staleness
event AnchorCut(address anchor);

/**
* @param data_ Address of the Oracle Data contract
* @param reporter_ The reporter address whose price will be used if it matches the anchor
* @param anchor_ The PriceOracleProxy that will be used to verify reporter price, or serve prices not given by the reporter
* @param anchorToleranceMantissa_ The tolerance allowed between the anchor and median. A tolerance of 10e16 means a new median that is 10% off from the anchor will still be saved
* @param tokens_ The CTokens struct that contains addresses for CToken contracts
*/
constructor(OpenOraclePriceData data_,
address reporter_,
AnchorOracle anchor_,
uint anchorToleranceMantissa_,
CTokens memory tokens_) SymbolConfiguration(tokens_) public {
reporter = reporter_;
anchor = anchor_;
priceData = data_;

require(anchorToleranceMantissa_ < 100e16, "Anchor Tolerance is too high");
upperBoundAnchorRatio = 100e16 + anchorToleranceMantissa_;
lowerBoundAnchorRatio = 100e16 - anchorToleranceMantissa_;
}

/**
* @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]);
}

// load usdc for using in loop to convert anchor prices to dollars
uint usdcPrice = readAnchor(cUsdcAddress);

// Try to update the view storage
for (uint i = 0; i < symbols.length; i++) {
CTokenMetadata memory tokenConfig = getCTokenConfig(symbols[i]);
// symbol is not supported in the view, but allow writing to data
if (tokenConfig.cTokenAddress == address(0)) continue;

uint reporterPrice = priceData.getPrice(reporter, tokenConfig.openOracleKey);
uint anchorPrice = getAnchorInUsd(tokenConfig, usdcPrice);

uint anchorRatio = mul(anchorPrice, 100e16) / reporterPrice;
bool withinAnchor = anchorRatio <= upperBoundAnchorRatio && anchorRatio >= lowerBoundAnchorRatio;

if (withinAnchor || anchorBreaker) {
// only update and emit event if value changes
if (_prices[tokenConfig.openOracleKey] != reporterPrice) {
_prices[tokenConfig.openOracleKey] = reporterPrice;
emit PriceUpdated(tokenConfig.openOracleKey, reporterPrice);
}
} else {
emit PriceGuarded(tokenConfig.openOracleKey, reporterPrice, anchorPrice);
}
}
}
/**
* @notice Returns price denominated in USD, with 6 decimals
* @dev If price was posted by reporter, return it. Otherwise, return anchor price converted through reporter ETH price.
*/
function prices(string calldata symbol) external view returns (uint) {
CTokenMetadata memory tokenConfig = getCTokenConfig(symbol);

if (tokenConfig.priceSource == PriceSource.REPORTER) return _prices[symbol];
if (tokenConfig.priceSource == PriceSource.FIXED_USD) return tokenConfig.fixedReporterPrice;
if (tokenConfig.priceSource == PriceSource.ANCHOR) {
uint usdPerEth = _prices["ETH"];
require(usdPerEth > 0, "eth price not set, cannot convert eth to dollars");

uint ethPerToken = readAnchor(tokenConfig);
return mul(usdPerEth, ethPerToken) / tokenConfig.baseUnit;
}
}

/**
* @dev fetch price in eth from proxy and convert to usd price using anchor usdc price.
* @dev Anchor price has 36 - underlying decimals, so scale back up to 36 decimals before dividing by by usdc price (30 decimals), yielding 6 decimal usd price
*/
function getAnchorInUsd(address cToken, uint ethPerUsdc) public view returns (uint) {
CTokenMetadata memory tokenConfig = getCTokenConfig(cToken);
return getAnchorInUsd(tokenConfig, ethPerUsdc);
}

function getAnchorInUsd(CTokenMetadata memory tokenConfig, uint ethPerUsdc) internal view returns (uint) {
if (tokenConfig.anchorSource == AnchorSource.FIXED_USD) {
return tokenConfig.fixedAnchorPrice;
}

uint ethPerToken = readAnchor(tokenConfig);

return mul(ethPerToken, tokenConfig.baseUnit) / ethPerUsdc;
}

/**
* @notice Implements the method of the PriceOracle interface of Compound v2 and returns returns the Eth price for an asset.
* @dev converts from 1e6 decimals of Open Oracle to 1e(36 - underlyingDecimals) of PriceOracleProxy
* @param cToken The cToken address for price retrieval
* @return The price for the given cToken address
*/
function getUnderlyingPrice(address cToken) public view returns (uint) {
CTokenMetadata memory tokenConfig = getCTokenConfig(cToken);
if (reporterBreaker == true) {
return readAnchor(tokenConfig);
}

if (tokenConfig.priceSource == PriceSource.FIXED_USD) {
uint usdPerToken = tokenConfig.fixedReporterPrice;
return mul(usdPerToken, 1e30) / tokenConfig.baseUnit;
}

if (tokenConfig.priceSource == PriceSource.REPORTER) {
uint usdPerToken = _prices[tokenConfig.openOracleKey];
return mul(usdPerToken, 1e30) / tokenConfig.baseUnit;
}

if (tokenConfig.priceSource == PriceSource.ANCHOR) {
// convert anchor price to usd, via reporter eth price
uint usdPerEth = _prices["ETH"];
require(usdPerEth != 0, "no reporter price for usd/eth exists, cannot convert anchor price to usd terms");

// factoring out extra 6 decimals from reporter eth price brings us back to decimals given by anchor
uint ethPerToken = readAnchor(tokenConfig);
return mul(usdPerEth, ethPerToken) / 1e6;
}
}

/**
* @notice Get the underlying price of a listed cToken asset
* @param cToken The cToken to get the underlying price of
* @return The underlying asset price mantissa (scaled by 1e18)
*/
function readAnchor(address cToken) public view returns (uint) {
return readAnchor(getCTokenConfig(cToken));
}

function readAnchor(CTokenMetadata memory tokenConfig) internal view returns (uint) {
if (tokenConfig.anchorSource == AnchorSource.FIXED_ETH) return tokenConfig.fixedAnchorPrice;

return anchor.assetPrices(tokenConfig.anchorOracleKey);
}

/// @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(priceData.source(message, signature) == reporter, "invalidation message must come from the reporter");

reporterBreaker = true;
emit ReporterInvalidated(reporter);
}

/// @notice invalidate the anchor, and fall back to using reporter without anchor

/// @dev determine if anchor is stale by checking when usdc was last updated
/// @dev all anchor prices are converted through usdc price, so if it is stale they are all stale
function cutAnchor() external {
AnchorOracle.Anchor memory latestUsdcAnchor = anchor.anchors(cUsdcAnchorKey);

uint usdcAnchorBlockNumber = mul(latestUsdcAnchor.period, anchor.numBlocksPerPeriod());
uint blocksSinceUpdate = block.number - usdcAnchorBlockNumber;

// one day in 15 second blocks without an update
if (blocksSinceUpdate > blocksInADay) {
anchorBreaker = true;
emit AnchorCut(address(anchor));
}
}


// @notice 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;
}
}
Loading

0 comments on commit c4c4683

Please sign in to comment.