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

Overestimated Soil Demand Leads to Misaligned Bean Sowing Incentives

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 problem is that calcDeltaPodDemand generates a wrong value whenever the change in soil demand should be set to steady. i.e: Decimal.from(1e18):

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); // @audit it will return 1e36
} else if (
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); // @audit it will return 1e36
} else {
deltaPodDemand = Decimal.ratio(dsoil, lastDeltaSoil);
}
}
lastSowTime = s.sys.weather.thisSowTime; // Overwrite last Season
thisSowTime = type(uint32).max; // Reset for next Season
}

Notice that two paths can assign the Decimal.from(1e18); to deltaPodDemand.

The value returned by Decimal.from(1e18); is 1e36. This occurs because the function from multiplies the value passed as a parameter by 1e18:

Decimal -> from

uint256 constant BASE = 10 ** 18;
function from(uint256 a) internal pure returns (D256 memory) {
return D256({value: a.mul(BASE)});
}

With the wrong value returned for the delta pod demand, the function evalDeltaPodDemand will assign caseId = 2, indicating increasing demand for soil.

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)
}

But in this case, the correct value for the caseId should be 1.

Further, this value is used to update caseId, temperature and the gauge system.

Weather -> calcCaseIdandUpdate

function calcCaseIdandUpdate(int256 deltaB) internal returns (uint256) {
uint256 beanSupply = C.bean().totalSupply();
// prevents infinite L2SR and podrate
if (beanSupply == 0) {
s.sys.weather.temp = 1;
return 9; // Reasonably low
}
// Calculate Case Id
@> (uint256 caseId, bool oracleFailure) = LibEvaluate.evaluateBeanstalk(deltaB, beanSupply);
@> updateTemperatureAndBeanToMaxLpGpPerBdvRatio(caseId, oracleFailure);
@> LibFlood.handleRain(caseId);
return caseId;
}

The wrong caseId will also impact the demand for soil.

PoC

  1. On MockSeasonFacet add a function to retrieve the delta pod demand:

import {Decimal} from "contracts/libraries/Decimal.sol";
function getPodDemand(uint256 soil) external view returns (Decimal.D256 memory deltaPodDemand) {
(deltaPodDemand, ,) = LibEvaluate.calcDeltaPodDemand(soil);
}
  1. On Gauge.t.sol add the proper imports and the test:

import {Decimal} from "contracts/libraries/Decimal.sol";
import "forge-std/console.sol";
function test_whenPodDemandIsSteady_shouldReturnCorrectValue() public {
uint256 dsoil = 1;
Decimal.D256 memory deltaPodDemand = season.getPodDemand(dsoil);
console.log("deltaPodDemand: %e", deltaPodDemand.value);
assertEq(deltaPodDemand.value, 1e18, "deltaPodDemand should be 1e18");
}

Run: forge test --match-test test_whenPodDemandIsSteady_shouldReturnCorrectValue -vv

Output:

@> [FAIL. Reason: deltaPodDemand should be 1e18: 1000000000000000000000000000000000000 != 1000000000000000000] test_whenPodDemandIsSteady_shouldReturnCorrectValue() (gas: 20840)
Logs:
@> deltaPodDemand: 1e36
Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 249.53ms (1.02ms CPU time

Impact

  • Beanstalk will adjust caseId and temperature as if there is a increasing demand for pods, even though the demand is actually steady.

  • Beanstalk will update the gauge points per BDV for LP with the incorrect value.

  • The demand for soil will be incorrectly calculated.

  • The logic for the handleRain(caseId) will also be impacted.

Tools Used

Manual Review & Foundry

Recommendations

Replace Decimal.from(1e18) with Decimal.one()

Updates

Lead Judging Commences

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

Overestimated Soil Demand Leads to Misaligned Bean Sowing Incentives (deltaPodDemand = 1e36 not 1e18)

Support

FAQs

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