Skip to content

Commit

Permalink
Add million dollar option for fetching usd prices.
Browse files Browse the repository at this point in the history
  • Loading branch information
Brean0 committed Sep 10, 2024
1 parent cc9c330 commit 8a3b6a7
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 30 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);
}
}
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
51 changes: 39 additions & 12 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,15 +140,21 @@ 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 = 1e18 / tokenPrice;
} else {
tokenPrice = 1e12 / tokenPrice;
}
// return the USD/TOKEN price.
// 1e6 * 1e`n` / 1e`n` = 1e6
return (tokenPrice * chainlinkTokenPrice) / (10 ** chainlinkTokenDecimals);
Expand Down
29 changes: 26 additions & 3 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
4 changes: 2 additions & 2 deletions protocol/test/foundry/Migration/L1Reciever.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down

0 comments on commit 8a3b6a7

Please sign in to comment.