DeFiHardhatFoundry
250,000 USDC
View results
Submission Details
Severity: low
Valid

`LibWell.getBeanTokenPriceFromTwaReserves()` incorrectly assumes that token has 18 decimals

Summary

Any Well LP can be whitelisted according to docs:
https://docs.bean.money/almanac/farm/sun#minting-whitelist

In case newly whitelisted Well has token with non 18 decimals, inner mechanism in core sunrise() function will break. That's because price calculation inside LibWell.getBeanTokenPriceFromTwaReserves() has hardcoded 1e18 for 18 decimals, however actual should be 10 ** ERC20(nonBeanToken).decimals().

Vulnerability Details

There is long following callback chain:

SeasonFacet.gm()
Weather.calcCaseIdandUpdate()
LibEvaluate.evaluateBeanstalk()
LibEvaluate.updateAndGetBeanstalkState()
LibEvaluate.calcLPToSupplyRatio()
LibEvaluate.evalPrice()
LibWell.getBeanTokenPriceFromTwaReserves()

Firstly, LibEvaluate.calcLPToSupplyRatio() calculates largestLiqWell which has 1e18 precision (because it's calculates via LibWell.getWellTwaUsdLiquidityFromReserves which has specifically 1e18 precision). Remember that it has 1e18 precision.

function calcLPToSupplyRatio(
uint256 beanSupply
)
internal
view
returns (Decimal.D256 memory lpToSupplyRatio, address largestLiqWell, bool oracleFailure)
{
...
// if the liquidity is the largest, update `largestLiqWell`,
// and add the liquidity to the total.
// `largestLiqWell` is only used to initialize `s.sopWell` upon a sop,
// but a hot storage load to skip the block below
// is significantly more expensive than performing the logic on every sunrise.
if (wellLiquidity > largestLiq) {
largestLiq = wellLiquidity;
largestLiqWell = pools[i];
}
...
}

Secondly, LibWell.getBeanTokenPriceFromTwaReserves() calculates Bean price according to reserves, returned price should be with 1e6 precision. However for example with reserves 500e6 USDC and 500e6 Bean it will return price 1e18 (instead of expected 1e6). Remember it for future steps.

function getBeanTokenPriceFromTwaReserves(address well) internal view returns (uint256 price) {
AppStorage storage s = LibAppStorage.diamondStorage();
// s.sys.twaReserve[well] should be set prior to this function being called.
// 'price' is in terms of reserve0:reserve1.
if (s.sys.twaReserves[well].reserve0 == 0 || s.sys.twaReserves[well].reserve1 == 0) {
price = 0;
} else {
// fetch the bean index from the well in order to properly return the bean price.
if (getBeanIndexFromWell(well) == 0) {
@> price = uint256(s.sys.twaReserves[well].reserve0).mul(1e18).div(
s.sys.twaReserves[well].reserve1
);
} else {
@> price = uint256(s.sys.twaReserves[well].reserve1).mul(1e18).div(
s.sys.twaReserves[well].reserve0
);
}
}
}

Finally, evalPrice() merges calculation from previous 2 steps to calculate beanUsdPrice and based on this performs important decisions regarding Bean peg mechanism further in flow. That is the place when miscalculation has impact:

function evalPrice(int256 deltaB, address well) internal view returns (uint256 caseId) {
AppStorage storage s = LibAppStorage.diamondStorage();
// p > 1
if (deltaB > 0) {
// Beanstalk will only use the largest liquidity well to compute the Bean price,
// and thus will skip the p > EXCESSIVE_PRICE_THRESHOLD check if the well oracle fails to
// compute a valid price this Season.
// deltaB > 0 implies that address(well) != address(0).
uint256 beanTknPrice = LibWell.getBeanTokenPriceFromTwaReserves(well);
if (beanTknPrice > 1) {
@> uint256 beanUsdPrice = uint256(1e30).div(
LibWell.getUsdTokenPriceForWell(well).mul(beanTknPrice)
);
if (beanUsdPrice > s.sys.seedGaugeSettings.excessivePriceThreshold) {
// p > excessivePriceThreshold
return caseId = 6;
}
}
caseId = 3;
}
// p < 1
}

It fetches cached price Token/Usd (it has 18 decimals), so overall calculation is 1e30 / (1e18 * 1e18) = 0.

function getUsdTokenPriceForWell(address well) internal view returns (uint tokenUsd) {
tokenUsd = LibAppStorage.diamondStorage().sys.usdTokenPrice[well];
}

Impact

Core peg mechanism becomes broken as soon as Well with non-18 decimals token is whitelisted. According to docs any Well can be whitelisted by governance, restrictions regarding decimals are not mentioned.

Tools Used

Manual Review

Recommendations

Do not harcode 1e18:

function getBeanTokenPriceFromTwaReserves(address well) internal view returns (uint256 price) {
AppStorage storage s = LibAppStorage.diamondStorage();
// s.sys.twaReserve[well] should be set prior to this function being called.
// 'price' is in terms of reserve0:reserve1.
if (s.sys.twaReserves[well].reserve0 == 0 || s.sys.twaReserves[well].reserve1 == 0) {
price = 0;
} else {
// fetch the bean index from the well in order to properly return the bean price.
if (getBeanIndexFromWell(well) == 0) {
- price = uint256(s.sys.twaReserves[well].reserve0).mul(1e18).div(
+ price = uint256(s.sys.twaReserves[well].reserve0).mul(10 ** ERC20(well.tokens[1]).decimals()).div(
s.sys.twaReserves[well].reserve1
);
} else {
- price = uint256(s.sys.twaReserves[well].reserve1).mul(1e18).div(
+ price = uint256(s.sys.twaReserves[well].reserve1).mul(10 ** ERC20(well.tokens[0]).decimals()).div(
s.sys.twaReserves[well].reserve0
);
}
}
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 12 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Appeal created

T1MOH Submitter
12 months ago
T1MOH Submitter
12 months ago
inallhonesty Lead Judge
12 months ago
inallhonesty Lead Judge 11 months ago
Submission Judgement Published
Validated
Assigned finding tags:

`LibWell.getBeanTokenPriceFromTwaReserves()` incorrectly assumes that token has 18 decimals

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.