-
Notifications
You must be signed in to change notification settings - Fork 10
/
Copy pathPythOracle.sol
139 lines (127 loc) · 6.62 KB
/
PythOracle.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
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;
import {IPyth} from "@pyth/IPyth.sol";
import {PythStructs} from "@pyth/PythStructs.sol";
import {BaseAdapter, Errors, IPriceOracle} from "../BaseAdapter.sol";
import {ScaleUtils, Scale} from "../../lib/ScaleUtils.sol";
/// @title PythOracle
/// @custom:security-contact [email protected]
/// @author Euler Labs (https://www.eulerlabs.com/)
/// @notice PriceOracle adapter for Pyth pull-based price feeds.
/// @dev Integration Note: Pyth is a pull-based oracle which requires price updates to be pushed by the user.
/// Before calling `getQuote*` dispatch a call `updatePriceFeeds` on the Pyth oracle proxy to refresh the price.
/// This is best done atomically via a multicall contract such as the Ethereum Vault Connector (EVC).
contract PythOracle is BaseAdapter {
/// @notice The maximum length of time that a price can be in the future.
uint256 internal constant MAX_AHEADNESS = 1 minutes;
/// @notice The maximum permitted value for `maxStaleness`.
uint256 internal constant MAX_STALENESS_UPPER_BOUND = 15 minutes;
/// @notice The minimum permitted value for `maxConfWidth`.
/// @dev Equal to 0.1%.
uint256 internal constant MAX_CONF_WIDTH_LOWER_BOUND = 10;
/// @notice The maximum permitted value for `maxConfWidth`.
/// @dev Equal to 5%.
uint256 internal constant MAX_CONF_WIDTH_UPPER_BOUND = 500;
/// @dev The smallest PythStruct exponent that the oracle can handle.
int256 internal constant MIN_EXPONENT = -20;
/// @dev The largest PythStruct exponent that the oracle can handle.
int256 internal constant MAX_EXPONENT = 12;
/// @dev The denominator for basis points values (maxConfWidth).
uint256 internal constant BASIS_POINTS = 10_000;
/// @inheritdoc IPriceOracle
string public constant name = "PythOracle";
/// @notice The address of the Pyth oracle proxy.
address public immutable pyth;
/// @notice The address of the base asset corresponding to the feed.
address public immutable base;
/// @notice The address of the quote asset corresponding to the feed.
address public immutable quote;
/// @notice The id of the feed in the Pyth network.
/// @dev See https://pyth.network/developers/price-feed-ids.
bytes32 public immutable feedId;
/// @notice The maximum allowed age of the price.
uint256 public immutable maxStaleness;
/// @notice The maximum allowed width of the confidence interval.
/// @dev Note: this value is in basis points i.e. 500 = 5%.
uint256 public immutable maxConfWidth;
/// @dev Used for correcting for the decimals of base and quote.
uint8 internal immutable baseDecimals;
/// @dev Used for correcting for the decimals of base and quote.
uint8 internal immutable quoteDecimals;
/// @notice Deploy a PythOracle.
/// @param _pyth The address of the Pyth oracle proxy.
/// @param _base The address of the base asset corresponding to the feed.
/// @param _quote The address of the quote asset corresponding to the feed.
/// @param _feedId The id of the feed in the Pyth network.
/// @param _maxStaleness The maximum allowed age of the price.
/// @param _maxConfWidth The maximum width of the confidence interval in basis points.
/// @dev Note: A high confidence interval indicates market volatility or Pyth consensus instability.
/// Consider a lower `_maxConfWidth` for highly-correlated pairs and a higher value for uncorrelated pairs.
/// Pairs with few data sources and low liquidity are more prone to volatility spikes and consensus instability.
constructor(
address _pyth,
address _base,
address _quote,
bytes32 _feedId,
uint256 _maxStaleness,
uint256 _maxConfWidth
) {
if (_maxStaleness > MAX_STALENESS_UPPER_BOUND) {
revert Errors.PriceOracle_InvalidConfiguration();
}
if (_maxConfWidth < MAX_CONF_WIDTH_LOWER_BOUND || _maxConfWidth > MAX_CONF_WIDTH_UPPER_BOUND) {
revert Errors.PriceOracle_InvalidConfiguration();
}
pyth = _pyth;
base = _base;
quote = _quote;
feedId = _feedId;
maxStaleness = _maxStaleness;
maxConfWidth = _maxConfWidth;
baseDecimals = _getDecimals(base);
quoteDecimals = _getDecimals(quote);
}
/// @notice Fetch the latest Pyth price and transform it to a quote.
/// @param inAmount The amount of `base` to convert.
/// @param _base The token that is being priced.
/// @param _quote The token that is the unit of account.
/// @return The converted amount.
function _getQuote(uint256 inAmount, address _base, address _quote) internal view override returns (uint256) {
bool inverse = ScaleUtils.getDirectionOrRevert(_base, base, _quote, quote);
PythStructs.Price memory priceStruct = _fetchPriceStruct();
uint256 price = uint256(uint64(priceStruct.price));
int8 feedExponent = int8(baseDecimals) - int8(priceStruct.expo);
Scale scale;
if (feedExponent > 0) {
scale = ScaleUtils.from(quoteDecimals, uint8(feedExponent));
} else {
scale = ScaleUtils.from(quoteDecimals + uint8(-feedExponent), 0);
}
return ScaleUtils.calcOutAmount(inAmount, price, scale, inverse);
}
/// @notice Get the latest Pyth price and perform sanity checks.
/// @dev Revert conditions: update timestamp is too stale or too ahead, price is negative or zero,
/// confidence interval is too wide, exponent is too large or too small.
/// @return The Pyth price struct without modification.
function _fetchPriceStruct() internal view returns (PythStructs.Price memory) {
PythStructs.Price memory p = IPyth(pyth).getPriceUnsafe(feedId);
if (p.publishTime < block.timestamp) {
// Verify that the price is not too stale
uint256 staleness = block.timestamp - p.publishTime;
if (staleness > maxStaleness) revert Errors.PriceOracle_InvalidAnswer();
} else {
// Verify that the price is not too ahead
uint256 aheadness = p.publishTime - block.timestamp;
if (aheadness > MAX_AHEADNESS) revert Errors.PriceOracle_InvalidAnswer();
}
// Verify that the price is positive and within the confidence width.
if (p.price <= 0 || p.conf > uint64(p.price) * maxConfWidth / BASIS_POINTS) {
revert Errors.PriceOracle_InvalidAnswer();
}
// Verify that the price exponent is within bounds.
if (p.expo < MIN_EXPONENT || p.expo > MAX_EXPONENT) {
revert Errors.PriceOracle_InvalidAnswer();
}
return p;
}
}