DeFiHardhatFoundry
250,000 USDC
View results
Submission Details
Severity: medium
Invalid

Miscalculation for soil demand will compromise Beans' peg

Summary

The LibEvaluate calculates the caseId based on the state of Beanstalk.

This library has the following functions

  • calcDeltaPodDemand - Calculates the change in soil demand from the previous season.

  • evalDeltaPodDemand - updates the caseId based on the change in Soil demand.

The issue is that calcDeltaPodDemand generates an incorrect value whenever there is a change in soil demand by considering a scenario where no beans were sown in the last season as a "high increasing change for pod demand."

function calcDeltaPodDemand(
uint256 dsoil
)
internal
view
returns (Decimal.D256 memory deltaPodDemand, uint32 lastSowTime, uint32 thisSowTime)
{
AppStorage storage s = LibAppStorage.diamondStorage();
// `s.weather.thisSowTime` is set to the number of seconds in it took for
// Soil to sell out during the current Season. If Soil didn't sell out,
// it remains `type(uint32).max`.
if (s.sys.weather.thisSowTime < type(uint32).max) {
if (
@> s.sys.weather.lastSowTime == type(uint32).max || // Didn't Sow all last Season
s.sys.weather.thisSowTime < SOW_TIME_DEMAND_INCR || // Sow'd all instantly this Season
(s.sys.weather.lastSowTime > SOW_TIME_STEADY &&
s.sys.weather.thisSowTime < s.sys.weather.lastSowTime.sub(SOW_TIME_STEADY)) // Sow'd all faster
) {
deltaPodDemand = Decimal.from(1e18);

lastSowTime == type(uint32).max Indicates no soil was sown last season, but if this condition is met, the value returned by this function is Decimal.from(1e18) which results in 1e36. This is higher than the deltaPodDemandUpperBound which is 1.05e18, therefore evalPodDemand will consider that Beanstalk has an increasing podRate:

function evalDeltaPodDemand(
Decimal.D256 memory deltaPodDemand
) internal view returns (uint256 caseId) {
AppStorage storage s = LibAppStorage.diamondStorage();
// increasing
if (
@> deltaPodDemand.greaterThanOrEqualTo( // @audit deltaPodDemand is 1e36, thus > 1.05e18
s.sys.seedGaugeSettings.deltaPodDemandUpperBound.toDecimal()
)
) {
caseId = 2;
// steady
} else if (
deltaPodDemand.greaterThanOrEqualTo(
s.sys.seedGaugeSettings.deltaPodDemandLowerBound.toDecimal()
)
) {
caseId = 1;
}
// decreasing (caseId = 0)
}

If Beanstalk didn't sow beans last season but has sown beans this season, it means that Beanstalk has recently (this season) entered a state that requires issuing soil.

When this happens, Beanstalk should be able to issue debt and burn beans, a.k.a allow sowing. But as we can see above, we are increasing the value of the caseId which will impact how much soil will be issued.

A higher caseId will increase the chances of reaching the high pod rate threshold and limit the issuance of soil.

if (caseId.mod(36) >= 24) {
@> newSoil = newSoil.mul(SOIL_COEFFICIENT_HIGH).div(C.PRECISION); // high podrate
}

When in fact, the scenario described should be treated as a "steady" increase, not a high increase. This is because Beanstalk still needs to issue debt to maintain the peg in a steady manner.

Due to the current logic, Beanstalk will issue less soil than necessary, making it harder to maintain the peg.

Impact

  • The caseId will be incorrect, compromising the Beanstalk peg by not issuing enough soil when needed, causing it to remain under the peg.

  • This will cause a cascade effect in the system as temperature, the gauge system, soil issuance, and the generalized flood all rely on the caseId.

Tools Used

Manual Review

Recommendations

Handle the following case as a steady change on the soil demand:
Check if Soil Sold Out This Season (thisSowTime < type(uint32).max):
- Condition 1: lastSowTime == type(uint32).max - Indicates no soil was sown last season.

function calcDeltaPodDemand(
uint256 dsoil
)
internal
view
returns (Decimal.D256 memory deltaPodDemand, uint32 lastSowTime, uint32 thisSowTime)
{
AppStorage storage s = LibAppStorage.diamondStorage();
// `s.weather.thisSowTime` is set to the number of seconds in it took for
// Soil to sell out during the current Season. If Soil didn't sell out,
// it remains `type(uint32).max`.
if (s.sys.weather.thisSowTime < type(uint32).max) {
if (
- s.sys.weather.lastSowTime == type(uint32).max || // Didn't Sow all last Season
s.sys.weather.thisSowTime < SOW_TIME_DEMAND_INCR || // Sow'd all instantly this Season
(s.sys.weather.lastSowTime > SOW_TIME_STEADY &&
s.sys.weather.thisSowTime < s.sys.weather.lastSowTime.sub(SOW_TIME_STEADY)) // Sow'd all faster
) {
deltaPodDemand = Decimal.from(1e18);
} else if (
+ s.sys.weather.lastSowTime == type(uint32).max || // Didn't Sow all last Season
s.sys.weather.thisSowTime <= s.sys.weather.lastSowTime.add(SOW_TIME_STEADY)
) {
- // Sow'd all in same time
deltaPodDemand = Decimal.one();
} else {
deltaPodDemand = Decimal.zero();
}
} else {
// Soil didn't sell out
uint256 lastDeltaSoil = s.sys.weather.lastDeltaSoil;
if (dsoil == 0) {
deltaPodDemand = Decimal.zero(); // If no one Sow'd
} else if (lastDeltaSoil == 0) {
deltaPodDemand = Decimal.from(1e18); // If no one Sow'd last Season
} else {
deltaPodDemand = Decimal.ratio(dsoil, lastDeltaSoil);
}
}
lastSowTime = s.sys.weather.thisSowTime; // Overwrite last Season
thisSowTime = type(uint32).max; // Reset for next Season
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 12 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

Bad calculation for caseId will lead to an inaccurate soil demand that will compromise Beans' peg

Support

FAQs

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