From 8a3b6a771b3b205a103205f96071d5b556b61610 Mon Sep 17 00:00:00 2001 From: Brean0 Date: Tue, 10 Sep 2024 11:46:25 +0200 Subject: [PATCH] Add million dollar option for fetching usd prices. --- .../contracts/beanstalk/sun/OracleFacet.sol | 11 +++- .../libraries/Oracle/LibChainlinkOracle.sol | 40 +++++++++++---- .../libraries/Oracle/LibUniswapOracle.sol | 3 -- .../libraries/Oracle/LibUsdOracle.sol | 51 ++++++++++++++----- protocol/contracts/libraries/Well/LibWell.sol | 29 +++++++++-- .../test/foundry/Migration/L1Reciever.t.sol | 4 +- 6 files changed, 108 insertions(+), 30 deletions(-) diff --git a/protocol/contracts/beanstalk/sun/OracleFacet.sol b/protocol/contracts/beanstalk/sun/OracleFacet.sol index c5e70886bc..ef84026c05 100644 --- a/protocol/contracts/beanstalk/sun/OracleFacet.sol +++ b/protocol/contracts/beanstalk/sun/OracleFacet.sol @@ -87,7 +87,16 @@ contract OracleFacet is Invariable, ReentrancyGuard { function getRatiosAndBeanIndex( IERC20[] memory tokens, uint256 lookback - ) internal view returns (uint[] memory ratios, uint beanIndex, bool success) { + ) external view returns (uint[] memory ratios, uint beanIndex, bool success) { (ratios, beanIndex, success) = LibWell.getRatiosAndBeanIndex(tokens, lookback); } + + /** + * @notice Fetches the amount of tokens equal to 1 Million USD for a given token. + * @param token address of the token to get the amount for. + * @param lookback the amount of time to look back in seconds. + */ + function getMillionUsdPrice(address token, uint256 lookback) external view returns (uint256) { + return LibUsdOracle.getMillionUsdPrice(token, lookback); + } } diff --git a/protocol/contracts/libraries/Oracle/LibChainlinkOracle.sol b/protocol/contracts/libraries/Oracle/LibChainlinkOracle.sol index 686f4e9a66..5594f14a10 100644 --- a/protocol/contracts/libraries/Oracle/LibChainlinkOracle.sol +++ b/protocol/contracts/libraries/Oracle/LibChainlinkOracle.sol @@ -41,12 +41,22 @@ library LibChainlinkOracle { address priceAggregatorAddress, uint256 maxTimeout, uint256 tokenDecimals, - uint256 lookback + uint256 lookback, + bool isMillion ) internal view returns (uint256 price) { return lookback > 0 - ? getTwap(priceAggregatorAddress, maxTimeout, tokenDecimals, lookback) - : getPrice(priceAggregatorAddress, maxTimeout, tokenDecimals); + ? getTwap(priceAggregatorAddress, maxTimeout, tokenDecimals, lookback, isMillion) + : getPrice(priceAggregatorAddress, maxTimeout, tokenDecimals, isMillion); + } + + function getTokenPrice( + address priceAggregatorAddress, + uint256 maxTimeout, + uint256 tokenDecimals, + uint256 lookback + ) internal view returns (uint256 price) { + return getTokenPrice(priceAggregatorAddress, maxTimeout, tokenDecimals, lookback, false); } /** @@ -59,7 +69,8 @@ library LibChainlinkOracle { function getPrice( address priceAggregatorAddress, uint256 maxTimeout, - uint256 tokenDecimals + uint256 tokenDecimals, + bool isMillion ) internal view returns (uint256 price) { IChainlinkAggregator priceAggregator = IChainlinkAggregator(priceAggregatorAddress); // First, try to get current decimal precision: @@ -88,6 +99,9 @@ library LibChainlinkOracle { // if token decimals is greater than 0, return the TOKEN2/TOKEN1 price instead (i.e invert the price). if (tokenDecimals > 0) { + // if `isMillion` is set, return `MillionTOKEN2/TOKEN1` Price instead + // (i.e, the amount of TOKEN1 equal to a million of TOKEN2) + if (isMillion) tokenDecimals = tokenDecimals + 6; price = uint256(10 ** (tokenDecimals + decimals)).div(uint256(answer)); } else { // Adjust to 6 decimal precision. @@ -109,12 +123,12 @@ library LibChainlinkOracle { address priceAggregatorAddress, uint256 maxTimeout, uint256 tokenDecimals, - uint256 lookback + uint256 lookback, + bool isMillion ) internal view returns (uint256 price) { - IChainlinkAggregator priceAggregator = IChainlinkAggregator(priceAggregatorAddress); // First, try to get current decimal precision: uint8 decimals; - try priceAggregator.decimals() returns (uint8 _decimals) { + try IChainlinkAggregator(priceAggregatorAddress).decimals() returns (uint8 _decimals) { // If call to Chainlink succeeds, record the current decimal precision decimals = _decimals; } catch { @@ -123,7 +137,7 @@ library LibChainlinkOracle { } // Secondly, try to get latest price data: - try priceAggregator.latestRoundData() returns ( + try IChainlinkAggregator(priceAggregatorAddress).latestRoundData() returns ( uint80 roundId, int256 answer, uint256 /* startedAt */, @@ -139,6 +153,11 @@ library LibChainlinkOracle { TwapVariables memory t; t.endTimestamp = block.timestamp.sub(lookback); + + if (isMillion) { + // if `isMillion` flag is enabled, + tokenDecimals = tokenDecimals + 6; + } // Check if last round was more than `lookback` ago. if (timestamp <= t.endTimestamp) { if (tokenDecimals > 0) { @@ -161,7 +180,10 @@ library LibChainlinkOracle { ); roundId -= 1; t.lastTimestamp = timestamp; - (answer, timestamp) = getRoundData(priceAggregator, roundId); + (answer, timestamp) = getRoundData( + IChainlinkAggregator(priceAggregatorAddress), + roundId + ); if ( checkForInvalidTimestampOrAnswer( timestamp, diff --git a/protocol/contracts/libraries/Oracle/LibUniswapOracle.sol b/protocol/contracts/libraries/Oracle/LibUniswapOracle.sol index 6c0d71733b..0286a9e3b9 100644 --- a/protocol/contracts/libraries/Oracle/LibUniswapOracle.sol +++ b/protocol/contracts/libraries/Oracle/LibUniswapOracle.sol @@ -15,9 +15,6 @@ interface IERC20Decimals { /** * @title Uniswap Oracle Library * @notice Contains functionalty to read prices from Uniswap V3 pools. - * @dev currently supports: - * - ETH:USDC price from the ETH:USDC 0.05% pool - * - ETH:USDT price from the ETH:USDT 0.05% pool **/ library LibUniswapOracle { // All instantaneous queries of Uniswap Oracles should use a 15 minute lookback. diff --git a/protocol/contracts/libraries/Oracle/LibUsdOracle.sol b/protocol/contracts/libraries/Oracle/LibUsdOracle.sol index 5116b1fbd7..5616a319e1 100644 --- a/protocol/contracts/libraries/Oracle/LibUsdOracle.sol +++ b/protocol/contracts/libraries/Oracle/LibUsdOracle.sol @@ -31,7 +31,7 @@ library LibUsdOracle { } /** - * @dev Returns the price of a given token in in USD with the option of using a lookback. (Usd:token Price) + * @dev Returns the price of 1 USD in terms of `token` with the option of using a lookback. (Usd:token Price) * `lookback` should be 0 if the instantaneous price is desired. Otherwise, it should be the * TWAP lookback in seconds. * If using a non-zero lookback, it is recommended to use a substantially large `lookback` @@ -68,23 +68,44 @@ library LibUsdOracle { uint256 tokenDecimals, uint256 lookback ) internal view returns (uint256 tokenPrice) { + return getTokenPriceFromExternal(token, tokenDecimals, lookback, false); + } + + /** + * @notice returns the price of 1 Million USD in terms of `token` with the option of using a lookback. + * @dev `LibWell.getRatiosAndBeanIndex` attempts to calculate the target ratios by fetching the usdPrice of each token. + * For tokens with low decimal precision and high prices (ex. WBTC), using the usd:token price would result in a + * large amount of precision loss. For this reason, tokens with less than 8 decimals use the 1 Million USD price instead.. + */ + function getMillionUsdPrice(address token, uint256 lookback) internal view returns (uint256) { + return getTokenPriceFromExternal(token, IERC20Decimals(token).decimals(), lookback, true); + } + + /** + * @notice internal helper function for `getTokenPriceFromExternal`. + * @dev the `isMillion` flag is used in `LibChainlinkOracle.getTokenPrice` to + * return the MILLION_TOKEN2/TOKEN1 price, in cases where the price of TOKEN1 is extremely high (relative to token 2), + * and when the decimals is very low. + */ + function getTokenPriceFromExternal( + address token, + uint256 tokenDecimals, + uint256 lookback, + bool isMillion + ) private view returns (uint256 tokenPrice) { AppStorage storage s = LibAppStorage.diamondStorage(); Implementation memory oracleImpl = s.sys.oracleImplementation[token]; // If the encode type is type 1, use the default chainlink implementation instead. // `target` refers to the address of the price aggergator implmenation if (oracleImpl.encodeType == bytes1(0x01)) { - // if the address in the oracle implementation is 0, use the chainlink registry to lookup address - address chainlinkOraclePriceAddress = oracleImpl.target; - - // decode data timeout to uint256 - uint256 timeout = abi.decode(oracleImpl.data, (uint256)); return LibChainlinkOracle.getTokenPrice( - chainlinkOraclePriceAddress, - timeout, - tokenDecimals, - lookback + oracleImpl.target, // chainlink Aggergator Address + abi.decode(oracleImpl.data, (uint256)), // timeout + tokenDecimals, // token decimals + lookback, + isMillion ); } else if (oracleImpl.encodeType == bytes1(0x02)) { // if the encodeType is type 2, use a uniswap oracle implementation. @@ -119,7 +140,8 @@ library LibUsdOracle { chainlinkOracle.target, abi.decode(chainlinkOracle.data, (uint256)), // timeout tokenDecimals == 0 ? tokenDecimals : chainlinkTokenDecimals, - lookback + lookback, + false ); // if token decimals != 0, Beanstalk is attempting to query the USD/TOKEN price, and @@ -127,7 +149,12 @@ library LibUsdOracle { if (tokenDecimals != 0) { // invert tokenPrice (to get CL_TOKEN/TOKEN). // `tokenPrice` has 6 decimal precision (see {LibUniswapOracle.getTwap}). - tokenPrice = 1e12 / tokenPrice; + // `tokenPrice` is scaled up to 1 million units, if the `isMillion` flag is enabled. + if (isMillion) { + tokenPrice = 1e18 / tokenPrice; + } else { + tokenPrice = 1e12 / tokenPrice; + } // return the USD/TOKEN price. // 1e6 * 1e`n` / 1e`n` = 1e6 return (tokenPrice * chainlinkTokenPrice) / (10 ** chainlinkTokenDecimals); diff --git a/protocol/contracts/libraries/Well/LibWell.sol b/protocol/contracts/libraries/Well/LibWell.sol index b6b87c4544..a52e76f723 100644 --- a/protocol/contracts/libraries/Well/LibWell.sol +++ b/protocol/contracts/libraries/Well/LibWell.sol @@ -51,17 +51,40 @@ library LibWell { success = true; ratios = new uint[](tokens.length); beanIndex = type(uint256).max; + bool isMillion; + address bean = s.sys.tokens.bean; + + // fetch the bean index and check whether the ratios precision needs to be increased. for (uint i; i < tokens.length; ++i) { - if (s.sys.tokens.bean == address(tokens[i])) { + if (address(tokens[i]) == bean) { beanIndex = i; - ratios[i] = 1e6; + } else if (IERC20Decimals(address(tokens[i])).decimals() < 8) { + // if the nonBean token in the well has a low decimal precision, + // set `isMillion` such that the ratio is set to be on a million basis. + isMillion = true; + } + } + + // get the target ratios. + for (uint i; i < tokens.length; ++i) { + if (address(tokens[i]) == bean) { + if (isMillion) { + ratios[i] = 1e12; + } else { + ratios[i] = 1e6; + } } else { - ratios[i] = LibUsdOracle.getUsdPrice(address(tokens[i]), lookback); + if (isMillion) { + ratios[i] = LibUsdOracle.getMillionUsdPrice(address(tokens[i]), lookback); + } else { + ratios[i] = LibUsdOracle.getUsdPrice(address(tokens[i]), lookback); + } if (ratios[i] == 0) { success = false; } } } + require(beanIndex != type(uint256).max, "Bean not in Well."); } diff --git a/protocol/test/foundry/Migration/L1Reciever.t.sol b/protocol/test/foundry/Migration/L1Reciever.t.sol index 9b5907ce6e..728faffd8e 100644 --- a/protocol/test/foundry/Migration/L1Reciever.t.sol +++ b/protocol/test/foundry/Migration/L1Reciever.t.sol @@ -224,13 +224,12 @@ contract L1RecieverFacetTest is Order, TestHelper { } function test_L2MigrateInvalidPodOrder() public { - bs.setRecieverForL1Migration(OWNER, RECIEVER); - ( address owner, L1RecieverFacet.L1PodOrder[] memory podOrders, bytes32[] memory proof ) = getMockPodOrder(); + bs.setRecieverForL1Migration(owner, RECIEVER); // update pod orderer podOrders[0].podOrder.orderer = RECIEVER; @@ -244,6 +243,7 @@ contract L1RecieverFacetTest is Order, TestHelper { // test helpers function getMockDepositData() internal + pure returns (address, uint256[] memory, uint256[] memory, uint256[] memory, bytes32[] memory) { address account = address(0x000000009d3a9e5C7c620514e1f36905C4Eb91e1);