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 (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();
if (s.sys.twaReserves[well].reserve0 == 0 || s.sys.twaReserves[well].reserve1 == 0) {
price = 0;
} else {
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();
if (deltaB > 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) {
return caseId = 6;
}
}
caseId = 3;
}
}
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
);
}
}
}