Skip to content

Commit

Permalink
Oracle updates, use million USD for low precision tokens (#1079)
Browse files Browse the repository at this point in the history
  • Loading branch information
pizzaman1337 authored Sep 10, 2024
2 parents cc9c330 + 85346fe commit a49290a
Show file tree
Hide file tree
Showing 17 changed files with 964 additions and 93 deletions.
11 changes: 10 additions & 1 deletion protocol/contracts/beanstalk/sun/OracleFacet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
4 changes: 4 additions & 0 deletions protocol/contracts/interfaces/IMockFBeanstalk.sol
Original file line number Diff line number Diff line change
Expand Up @@ -1530,6 +1530,8 @@ interface IMockFBeanstalk {
GerminationSide side
) external;

function mockInitState() external;

function mockLiquidityWeight() external pure returns (uint256);

function mockSetAverageGrownStalkPerBdvPerSeason(
Expand Down Expand Up @@ -1715,6 +1717,8 @@ interface IMockFBeanstalk {

function poolCurrentDeltaB(address pool) external view returns (int256 deltaB);

function poolCurrentDeltaBMock(address pool) external view returns (int256 deltaB);

function poolDeltaB(address pool) external view returns (int256);

function publishRequisition(Requisition memory requisition) external;
Expand Down
40 changes: 31 additions & 9 deletions protocol/contracts/libraries/Oracle/LibChainlinkOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand All @@ -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:
Expand Down Expand Up @@ -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.
Expand All @@ -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 {
Expand All @@ -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 */,
Expand All @@ -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) {
Expand All @@ -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,
Expand Down
3 changes: 0 additions & 3 deletions protocol/contracts/libraries/Oracle/LibUniswapOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
56 changes: 41 additions & 15 deletions protocol/contracts/libraries/Oracle/LibUsdOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -119,22 +140,27 @@ 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
// thus the price needs to be inverted.
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 = (1e12 * (10 ** tokenDecimals)) / tokenPrice;
} else {
tokenPrice = (1e6 * (10 ** tokenDecimals)) / tokenPrice;
}
// return the USD/TOKEN price.
// 1e6 * 1e`n` / 1e`n` = 1e6
return (tokenPrice * chainlinkTokenPrice) / (10 ** chainlinkTokenDecimals);
} else {
// return the TOKEN/USD price.
return (tokenPrice * chainlinkTokenPrice) / UNISWAP_DENOMINATOR;
}

return (tokenPrice * chainlinkTokenPrice) / UNISWAP_DENOMINATOR;
}

// If the oracle implementation address is not set, use the current contract.
Expand Down
2 changes: 0 additions & 2 deletions protocol/contracts/libraries/Silo/LibWhitelist.sol
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,6 @@ library LibWhitelist {
s.sys.silo.assetSettings[token].stalkIssuedPerBdv = stalkIssuedPerBdv;
s.sys.silo.assetSettings[token].milestoneSeason = uint32(s.sys.season.current);
s.sys.silo.assetSettings[token].encodeType = encodeType;
s.sys.silo.assetSettings[token].gaugePointImplementation.selector = bytes4(0);
s.sys.silo.assetSettings[token].liquidityWeightImplementation.selector = bytes4(0);
s.sys.silo.assetSettings[token].gaugePoints = gaugePoints;
s.sys.silo.assetSettings[token].optimalPercentDepositedBdv = optimalPercentDepositedBdv;
s.sys.silo.assetSettings[token].gaugePointImplementation = gpImplementation;
Expand Down
40 changes: 35 additions & 5 deletions protocol/contracts/libraries/Well/LibWell.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
}

Expand Down Expand Up @@ -168,7 +191,9 @@ library LibWell {

/**
* @dev Sets the price in {AppStorage.usdTokenPrice} given a set of ratios.
* It assumes that the ratios correspond to the Constant Product Well indexes.
* Assumes
* 1) Ratios correspond to the Constant Product Well indexes.
* 2) the Well is a 2 token Well.
*/
function setUsdTokenPriceForWell(address well, uint256[] memory ratios) internal {
AppStorage storage s = LibAppStorage.diamondStorage();
Expand All @@ -180,7 +205,12 @@ library LibWell {
s.sys.usdTokenPrice[well] = 0;
} else {
(, uint256 j) = getNonBeanTokenAndIndexFromWell(well);
s.sys.usdTokenPrice[well] = ratios[j];
uint256 i = j == 0 ? 1 : 0;
// usdTokenPrice is scaled down to USD/TOKEN, in the cases where
// Beanstalk calculated the MILLION_USD/TOKEN price instead of USD/TOKEN price.
// Beanstalk accepts the loss of precision here, as `usdTokenPrice[well]` is used for
// calculating the liquidity and excessive price.
s.sys.usdTokenPrice[well] = (ratios[j] * 1e6) / ratios[i];
}
}

Expand Down
28 changes: 28 additions & 0 deletions protocol/contracts/mocks/mockFacets/MockSeasonGettersFacet.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
SPDX-License-Identifier: MIT
*/

pragma solidity ^0.8.20;

import {SeasonGettersFacet} from "../../beanstalk/sun/SeasonFacet/SeasonGettersFacet.sol";
import {LibDeltaB} from "../../libraries/Oracle/LibDeltaB.sol";
import {LibAppStorage, AppStorage} from "../../libraries/LibAppStorage.sol";
import "forge-std/console.sol";

/**
* @author pizzaman1337
* @title Mock Season Getters Facet
**/
contract MockSeasonGettersFacet is SeasonGettersFacet {
// this mock removes the isWell check, so that a well's deltaB can be checked without requiring whitelisting
function poolCurrentDeltaBMock(address pool) public view returns (int256 deltaB) {
console.log("poolCurrentDeltaB");
(deltaB) = LibDeltaB.currentDeltaB(pool);
return deltaB;
}

function mockInitState() public {
AppStorage storage s = LibAppStorage.diamondStorage();
s.sys.tokens.bean = 0xBEA0000029AD1c77D3d5D23Ba2D8893dB9d1Efab;
}
}
Loading

0 comments on commit a49290a

Please sign in to comment.