Skip to content

Commit 98fe5cd

Browse files
authored
Merge pull request #1 from Gearbox-protocol/new-oracles
feat: erc4626 and redstone oracles
2 parents 84d02d0 + 27b6885 commit 98fe5cd

File tree

6 files changed

+714
-8
lines changed

6 files changed

+714
-8
lines changed
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
// Gearbox Protocol. Generalized leverage for DeFi protocols
3+
// (c) Gearbox Holdings, 2023
4+
pragma solidity ^0.8.17;
5+
6+
import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
7+
import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol";
8+
import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
9+
import {PriceFeedType} from "../../interfaces/IPriceFeedType.sol";
10+
11+
import {LPPriceFeed} from "../LPPriceFeed.sol";
12+
13+
// EXCEPTIONS
14+
import {ZeroAddressException} from "@gearbox-protocol/core-v2/contracts/interfaces/IErrors.sol";
15+
16+
uint256 constant RANGE_WIDTH = 200; // 2%
17+
18+
/// @title ERC4626 vault shares price feed
19+
contract ERC4626PriceFeed is LPPriceFeed {
20+
/// @dev Chainlink price feed for the vault's underlying
21+
AggregatorV3Interface public immutable priceFeed;
22+
23+
/// @dev Address of the vault to compute prices for
24+
IERC4626 public immutable vault;
25+
26+
/// @dev Amount of shares comprising a single unit (accounting for decimals)
27+
uint256 public immutable vaultShareUnit;
28+
29+
/// @dev Amount of underlying comprising a single unit (accounting for decimals)
30+
uint256 public immutable underlyingUnit;
31+
32+
PriceFeedType public constant override priceFeedType = PriceFeedType.ERC4626_VAULT_ORACLE;
33+
uint256 public constant override version = 1;
34+
35+
/// @dev Whether to skip price sanity checks.
36+
/// @notice Always set to true for LP price feeds,
37+
/// since they perform their own sanity checks
38+
bool public constant override skipPriceCheck = true;
39+
40+
constructor(address addressProvider, address _vault, address _priceFeed)
41+
LPPriceFeed(
42+
addressProvider,
43+
RANGE_WIDTH,
44+
_vault != address(0) ? string(abi.encodePacked(IERC20Metadata(_vault).name(), " priceFeed")) : ""
45+
)
46+
{
47+
if (_vault == address(0) || _priceFeed == address(0)) {
48+
revert ZeroAddressException();
49+
}
50+
51+
vault = IERC4626(_vault);
52+
priceFeed = AggregatorV3Interface(_priceFeed);
53+
54+
vaultShareUnit = 10 ** vault.decimals();
55+
underlyingUnit = 10 ** IERC20Metadata(vault.asset()).decimals();
56+
57+
uint256 assetsPerShare = vault.convertToAssets(vaultShareUnit);
58+
_setLimiter(assetsPerShare);
59+
}
60+
61+
/// @dev Returns the USD price of the pool's share
62+
function latestRoundData()
63+
external
64+
view
65+
override
66+
returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)
67+
{
68+
(roundId, answer, startedAt, updatedAt, answeredInRound) = priceFeed.latestRoundData();
69+
70+
// Sanity check for chainlink pricefeed
71+
_checkAnswer(roundId, answer, updatedAt, answeredInRound);
72+
73+
uint256 assetsPerShare = vault.convertToAssets(vaultShareUnit);
74+
75+
assetsPerShare = _checkAndUpperBoundValue(assetsPerShare);
76+
77+
answer = int256((assetsPerShare * uint256(answer)) / underlyingUnit);
78+
}
79+
80+
function _checkCurrentValueInBounds(uint256 _lowerBound, uint256 _uBound) internal view override returns (bool) {
81+
uint256 assetsPerShare = vault.convertToAssets(vaultShareUnit);
82+
if (assetsPerShare < _lowerBound || assetsPerShare > _uBound) {
83+
return false;
84+
}
85+
return true;
86+
}
87+
}
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
// Gearbox Protocol. Generalized leverage for DeFi protocols
3+
// (c) Gearbox Holdings, 2023
4+
pragma solidity ^0.8.17;
5+
6+
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";
7+
import {RedstoneConsumerNumericBase} from
8+
"@redstone-finance/evm-connector/contracts/core/RedstoneConsumerNumericBase.sol";
9+
import {PriceFeedType, IPriceFeedType} from "../../interfaces/IPriceFeedType.sol";
10+
import {NotImplementedException} from "@gearbox-protocol/core-v2/contracts/interfaces/IErrors.sol";
11+
import {IUpdatablePriceFeed} from "@gearbox-protocol/core-v3/contracts/interfaces/ICreditFacadeV3Multicall.sol";
12+
import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
13+
14+
interface IRedstonePriceFeedExceptions {
15+
/// @dev Thrown no non-zero signers are passed
16+
/// or the signer set is smaller than required threshold
17+
error InvalidSignerSetException();
18+
19+
/// @dev Thrown when the validated price value is zero
20+
error ZeroPriceException();
21+
22+
/// @dev Thrown when attempting to access a price value
23+
/// that wasn't submitted in the current block
24+
error RedstonePriceStaleException();
25+
26+
/// @dev Thrown when attempting to push an update
27+
/// with the payload that is older than the last
28+
/// update payload, or too far from the current block
29+
/// timestamp
30+
error RedstonePayloadTimestampIncorrect();
31+
32+
/// @dev Thrown when data package timestamp is not equal to expected
33+
/// payload timestamp
34+
error DataPackageTimestampIncorrect();
35+
}
36+
37+
interface IRedstonePriceFeedEvents {
38+
/// @dev Emitted when a successful price update is pushed
39+
event PriceUpdated(uint256 price);
40+
}
41+
42+
contract RedstonePriceFeed is
43+
RedstoneConsumerNumericBase,
44+
IUpdatablePriceFeed,
45+
IPriceFeedType,
46+
AggregatorV3Interface,
47+
IRedstonePriceFeedExceptions,
48+
IRedstonePriceFeedEvents
49+
{
50+
using SafeCast for uint256;
51+
52+
/// @notice Max period that the payload can be backward in time relative to the block
53+
uint256 public constant DEFAULT_MAX_DATA_TIMESTAMP_DELAY_SECONDS = 3 minutes;
54+
55+
/// @notice Max period that the payload can be forward in time relative to the block
56+
uint256 public constant DEFAULT_MAX_DATA_TIMESTAMP_AHEAD_SECONDS = 1 minutes;
57+
58+
/// @notice Max time that the price feed will return the last update's price for
59+
/// If more time elapse since last update, the price feed will revert
60+
uint256 public constant DEFAULT_PRICE_EXPIRATION_TIME = 4 minutes;
61+
62+
/// @notice Price feed description
63+
string public override description;
64+
65+
/// @notice Decimals of the price feed's returned value (always 8 for USD price feeds)
66+
uint8 public constant override decimals = 8;
67+
68+
/// @notice ID of the asset in Redstone's payload
69+
bytes32 public immutable dataFeedId;
70+
71+
/// @notice Authorized payload signer at index 0
72+
address public immutable signerAddress0;
73+
74+
/// @notice Authorized payload signer at index 1
75+
address public immutable signerAddress1;
76+
77+
/// @notice Authorized payload signer at index 2
78+
address public immutable signerAddress2;
79+
80+
/// @notice Authorized payload signer at index 3
81+
address public immutable signerAddress3;
82+
83+
/// @notice Authorized payload signer at index 4
84+
address public immutable signerAddress4;
85+
86+
/// @notice Authorized payload signer at index 5
87+
address public immutable signerAddress5;
88+
89+
/// @notice Authorized payload signer at index 6
90+
address public immutable signerAddress6;
91+
92+
/// @notice Authorized payload signer at index 7
93+
address public immutable signerAddress7;
94+
95+
/// @notice Authorized payload signer at index 8
96+
address public immutable signerAddress8;
97+
98+
/// @notice Authorized payload signer at index 9
99+
address public immutable signerAddress9;
100+
101+
/// @notice Minimal number of unique signatures from authorized signers
102+
/// required to validate a payload
103+
uint8 public immutable signersThreshold;
104+
105+
/// @notice The last stored price value
106+
uint128 public lastPrice;
107+
108+
/// @notice The timestamp of the last update's payload
109+
uint40 public lastPayloadTimestamp;
110+
111+
/// @dev Contract version
112+
uint256 public constant override version = 3_00;
113+
114+
/// @dev Whether to skip price sanity checks.
115+
/// @notice Always set to true for Redstone oracle,
116+
/// since price updates always check price value to be zero
117+
/// and extra metadata from `latestRoundData()` is returned as zero
118+
bool public constant override skipPriceCheck = true;
119+
120+
PriceFeedType public constant override priceFeedType = PriceFeedType.REDSTONE_ORACLE;
121+
122+
constructor(string memory tokenSymbol, bytes32 _dataFeedId, address[10] memory _signers, uint8 _signersThreshold) {
123+
if (_signersThreshold > 10) revert InvalidSignerSetException();
124+
for (uint256 i = 0; i < _signersThreshold; ++i) {
125+
if (_signers[i] == address(0)) revert InvalidSignerSetException();
126+
}
127+
128+
dataFeedId = _dataFeedId;
129+
130+
signerAddress0 = _signers[0];
131+
signerAddress1 = _signers[1];
132+
signerAddress2 = _signers[2];
133+
signerAddress3 = _signers[3];
134+
signerAddress4 = _signers[4];
135+
signerAddress5 = _signers[5];
136+
signerAddress6 = _signers[6];
137+
signerAddress7 = _signers[7];
138+
signerAddress8 = _signers[8];
139+
signerAddress9 = _signers[9];
140+
141+
signersThreshold = _signersThreshold;
142+
143+
description = string(abi.encodePacked(tokenSymbol, " Redstone Price Feed"));
144+
}
145+
146+
/// @notice Returns the number of unique signatures required to validate a payload
147+
function getUniqueSignersThreshold() public view virtual override returns (uint8) {
148+
return signersThreshold;
149+
}
150+
151+
/// @notice Returns the index of the provided signer or reverts if the address is not a signer
152+
function getAuthorisedSignerIndex(address signerAddress) public view virtual override returns (uint8) {
153+
if (signerAddress == signerAddress0) return 0;
154+
if (signerAddress == signerAddress1) return 1;
155+
if (signerAddress == signerAddress2) return 2;
156+
if (signerAddress == signerAddress3) return 3;
157+
if (signerAddress == signerAddress4) return 4;
158+
if (signerAddress == signerAddress5) return 5;
159+
if (signerAddress == signerAddress6) return 6;
160+
if (signerAddress == signerAddress7) return 7;
161+
if (signerAddress == signerAddress8) return 8;
162+
if (signerAddress == signerAddress9) return 9;
163+
164+
revert SignerNotAuthorised(signerAddress);
165+
}
166+
167+
/// @notice Validates that a timestamp in a data package is valid
168+
/// @dev Sanity checks on the timestamp are performed earlier in the update,
169+
/// when the lastPayloadTimestamp is being set
170+
/// @param receivedTimestampMilliseconds Timestamp in the data package, in milliseconds
171+
function validateTimestamp(uint256 receivedTimestampMilliseconds) public view override {
172+
uint256 receivedTimestampSeconds = receivedTimestampMilliseconds / 1000;
173+
174+
if (receivedTimestampSeconds != lastPayloadTimestamp) {
175+
revert DataPackageTimestampIncorrect();
176+
}
177+
}
178+
179+
/// @dev Validates that the expected payload timestamp is not older than the last payload's,
180+
/// and not too far from the current block's
181+
/// @param expectedPayloadTimestamp Timestamp expected to be in all of the incoming payload's
182+
/// packages
183+
function _validateExpectedPayloadTimestamp(uint256 expectedPayloadTimestamp) internal view {
184+
if ((block.timestamp < expectedPayloadTimestamp)) {
185+
if ((expectedPayloadTimestamp - block.timestamp) > DEFAULT_MAX_DATA_TIMESTAMP_AHEAD_SECONDS) {
186+
revert RedstonePayloadTimestampIncorrect();
187+
}
188+
} else if ((block.timestamp - expectedPayloadTimestamp) > DEFAULT_MAX_DATA_TIMESTAMP_DELAY_SECONDS) {
189+
revert RedstonePayloadTimestampIncorrect();
190+
}
191+
}
192+
193+
/// @notice Saves validated price retrieved from the passed Redstone payload
194+
/// @param data A data blob with with 2 parts:
195+
/// - A timestamp expected to be in all Redstone data packages
196+
/// - Redstone payload with price update
197+
function updatePrice(bytes calldata data) external {
198+
(uint256 expectedPayloadTimestamp, bytes memory payload) = abi.decode(data, (uint256, bytes));
199+
200+
// We want to minimize price update execution, in case, e.g., when several users submit
201+
// the same price update in a short span of time. So only updates with a larger payload timestamp
202+
// are fully validated and applied
203+
if (expectedPayloadTimestamp <= lastPayloadTimestamp) return;
204+
205+
// We validate and set the payload timestamp here. Data packages' timestamps being equal
206+
// to the expected timestamp is checked in `validateTimestamp()`, which is called
207+
// from inside `getOracleNumericValueFromTxMsg`
208+
_validateExpectedPayloadTimestamp(expectedPayloadTimestamp);
209+
lastPayloadTimestamp = uint40(expectedPayloadTimestamp);
210+
211+
uint256 priceValue = getOracleNumericValueFromTxMsg(dataFeedId);
212+
213+
if (priceValue == 0) revert ZeroPriceException();
214+
215+
if (priceValue != lastPrice) {
216+
lastPrice = priceValue.toUint128();
217+
emit PriceUpdated(priceValue);
218+
}
219+
}
220+
221+
/// @notice Returns the USD price of the token (as the second returned value)
222+
/// @dev Since Redstone oracles do not adhere to Chainlink's interface, extra metadata is returned as 0
223+
function latestRoundData() external view override returns (uint80, int256, uint256, uint256, uint80) {
224+
if (lastPayloadTimestamp + DEFAULT_PRICE_EXPIRATION_TIME < block.timestamp) {
225+
revert RedstonePriceStaleException();
226+
}
227+
228+
int256 answer = int256(uint256(lastPrice));
229+
230+
return (0, answer, 0, 0, 0);
231+
}
232+
233+
/// @dev Not implemented, since Gearbox does not use historical data
234+
function getRoundData(uint80) external pure override returns (uint80, int256, uint256, uint256, uint80) {
235+
revert NotImplementedException();
236+
}
237+
}

0 commit comments

Comments
 (0)