Summary
On the Sunrise process, there is the call to stepOracle. If one of the oracle fails when fetching the price, the storage for reserves/liquidity in USD is set to zero.
function updateOracle(
address well,
bytes memory lastSnapshot
) internal returns (int256 deltaB) {
AppStorage storage s = LibAppStorage.diamondStorage();
uint256[] memory twaReserves;
uint256[] memory ratios;
(deltaB, s.wellOracleSnapshots[well], twaReserves, ratios) = twaDeltaB(
well,
lastSnapshot
);
@> LibWell.setTwaReservesForWell(well, twaReserves);
@> LibWell.setUsdTokenPriceForWell(well, ratios);
emit WellOracle(
s.season.current,
well,
deltaB,
s.wellOracleSnapshots[well]
);
}
The problem here is that when calculating the caseId the function calcLPToSupplyRatiofrom LibEvalulatewill determine the lpToSupplyRatiobased using the totalUsdLiquiditywhich is the sum of the liquidity.
'liquidity' is definied as the non-bean value in a pool that trades beans.
But when one oracle or more fails, it will impact heavily the lpToSupplyRatiowhich is used to determine the value of the caseId.
function calcLPToSupplyRatio(
uint256 beanSupply
)
internal
view
returns (Decimal.D256 memory lpToSupplyRatio, address largestLiqWell, bool oracleFailure)
{
if (beanSupply == 0) return (Decimal.zero(), address(0), true);
address[] memory pools = LibWhitelistedTokens.getWhitelistedLpTokens();
uint256[] memory twaReserves;
uint256 totalUsdLiquidity;
uint256 largestLiq;
uint256 wellLiquidity;
for (uint256 i; i < pools.length; i++) {
twaReserves = LibWell.getTwaReservesFromStorageOrBeanstalkPump(pools[i]);
@> uint256 usdLiquidity = LibWell.getWellTwaUsdLiquidityFromReserves(
pools[i],
twaReserves
);
if (usdLiquidity == 0) {
oracleFailure = true;
}
wellLiquidity = getLiquidityWeight(pools[i]).mul(usdLiquidity).div(1e18);
if (wellLiquidity > largestLiq) {
largestLiq = wellLiquidity;
largestLiqWell = pools[i];
}
@> totalUsdLiquidity = totalUsdLiquidity.add(wellLiquidity);
if (pools[i] == LibBarnRaise.getBarnRaiseWell()) {
if (LibAppStorage.diamondStorage().sys.season.fertilizing == true) {
beanSupply = beanSupply.sub(LibUnripe.getLockedBeans(twaReserves));
}
}
}
if (totalUsdLiquidity == 0) return (Decimal.zero(), address(0), true);
@> lpToSupplyRatio = Decimal.ratio(totalUsdLiquidity.div(LIQUIDITY_PRECISION), beanSupply);
}
Scenario: When calling Sunrise, the following occurs:
✅ WETH/BEAN - $20k in WETH liquidity - oracle succeeds, therefore twaReserves and usdLiquidity are stored. Then those values are used to set the totalUsdLiquidityon calcLPToSupplyRatio.
❌ WSTETH/BEAN - 500k in WSTETH liquidity - oracle failed, therefore twaReserves and usdLiquidityare zero.
The lpToSupplyRatiothat should have the value Decimal.ratio(520k.div(1e12), beanSupply); in case both oracles' succeed, will be miscalculated as:
Decimal.ratio(20k.div(1e12), beanSupply);
Notice that the beanSupplydoesn't change, so here we have a result that differs a lot from what is the current non-bean liquidity. Beanstalk will miscalculate the caseId:
-
Let's say that the correct lpToSupplyLiquidity should be greater than the lpToSupplyRatioUpperBound but as only the well with low liquidity has succeeded in fetching the price of the non-bean asset, the lpToSupplyLiquidity will be considered lpToSupplyRatioLowerBound
-
In a nutshell, the caseIdthat should be 108is now set to 36.
function evalLpToSupplyRatio(
Decimal.D256 memory lpToSupplyRatio
) internal view returns (uint256 caseId) {
AppStorage storage s = LibAppStorage.diamondStorage();
if (
lpToSupplyRatio.greaterThanOrEqualTo(
s.sys.seedGaugeSettings.lpToSupplyRatioUpperBound.toDecimal()
)
) {
@> caseId = 108;
} else if (
lpToSupplyRatio.greaterThanOrEqualTo(
s.sys.seedGaugeSettings.lpToSupplyRatioOptimal.toDecimal()
)
) {
@> caseId = 72;
} else if (
lpToSupplyRatio.greaterThanOrEqualTo(
s.sys.seedGaugeSettings.lpToSupplyRatioLowerBound.toDecimal()
)
) {
@> caseId = 36;
}
}
This is just one scenario, as there will be several wells with different liquidity, the caseIdwill be often at risk of being set with an inappropriate value.
See that the calculation of the caseIdis 100% dependent on evalLpToSupplyRatio
@> * @notice Evaluates beanstalk based on deltaB, podRate, deltaPodDemand and lpToSupplyRatio.
@> * and returns the associated caseId.
*/
function evaluateBeanstalk(int256 deltaB, uint256 beanSupply) internal returns (uint256, bool) {
BeanstalkState memory bs = updateAndGetBeanstalkState(beanSupply);
uint256 caseId = evalPodRate(bs.podRate)
.add(evalPrice(deltaB, bs.largestLiqWell))
.add(evalDeltaPodDemand(bs.deltaPodDemand))
@> .add(evalLpToSupplyRatio(bs.lpToSupplyRatio));
return (caseId, bs.oracleFailure);
}
Also, there is an assumption on the updateTemperatureAndBeanToMaxLpGpPerBdvRatio function that the liquidity level doesn't affect the temperature.
* @notice updates the temperature and BeanToMaxLpGpPerBdvRatio, based on the caseId.
* @param caseId the state beanstalk is in, based on the current season.
@> * @dev currently, an oracle failure does not affect the temperature, as
@> * the temperature is not affected by liquidity levels. The function will
@> * need to be updated if the temperature is affected by liquidity levels.
* This is implemented such that liveliness in change in temperature is retained.
*/
function updateTemperatureAndBeanToMaxLpGpPerBdvRatio(
uint256 caseId,
bool oracleFailure
) internal {
@> LibCases.CaseData memory cd = LibCases.decodeCaseData(caseId);
@> updateTemperature(cd.bT, caseId);
if (oracleFailure) return;
updateBeanToMaxLPRatio(cd.bL, caseId);
}
See that the oracleFailuredoesn't prevent the temperature to be updated using the miscalculated caseId.
In the last line tagged on the NatSpec: The function will need to be updated if the temperature is affected by liquidity levels.
Because of this issue, the peg mechanism will frequently be affected, resulting in:
Impact
Miscalculation of the temperature, impacting on how Beanstalk issues debt/rewards.
Miscalculation for the Flood/Sop. If P > 1 and the * Pod Rate is less than 5%, the Farm is Oversaturated. If it is Oversaturated * for a Season, each Season in which it continues to be Oversaturated, it Floods.Impact
With all the above, the peg mechanism is compromised.
Tools Used
Manual Review
Recommendations
(Recommended) When an oracle fails, the protocol should adopt a conservative strategy when setting the caseId on evalLpToSupplyRatioto avoid unintended changes in the temperature.
(Additional) In one of my previous reports, I suggested the latest 'safe' price for a similar case, but I think we can use it here as well.