/
UniswapAnchoredView.sol
287 lines (241 loc) · 12.8 KB
/
UniswapAnchoredView.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
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;
}
}