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

The `SeasonFacet::incentivize` function is likely to revert due to an incorrect calculation of the `secondsLate` value

Summary

SeasonFacet contract holds the sunrise function and handles all logic for season changes. When Beanstalk advances to a new season, a reward is sent to the address that successfully called the sunrise function. The reward comprises the total gas spent on the transaction and uses other parameters such as the seconds elapsed after gm was ready to be called, stored in the secondsLate variable. However, the calculation of the secondsLate value is very likely to fail due to the season counting mechanism.

Vulnerability Details

When the sunrise function is called to start a new season, the function gm is executed. The gm function transitions Beanstalk to the next season. After some checks, the season number is updated, as well as the deltaB and caseId values. Calculations of the new gauge points for LP assets, soil issuance and germination process are also carried out. After the transition is performed, the incentivize function is finally called.

Calculation of the secondsLate value is given in the incentivize function:

function incentivize(address account, LibTransfer.To mode) private returns (uint256) {
uint256 secondsLate = block.timestamp.sub( //<@ FINDING
s.sys.season.start.add(s.sys.season.period.mul(s.sys.season.current))
);
// reset USD Token prices and TWA reserves in storage for all whitelisted Well LP Tokens.
address[] memory whitelistedWells = LibWhitelistedTokens.getWhitelistedWellLpTokens();
for (uint256 i; i < whitelistedWells.length; i++) {
LibWell.resetUsdTokenPriceForWell(whitelistedWells[i]);
LibWell.resetTwaReservesForWell(whitelistedWells[i]);
}
uint256 incentiveAmount = LibIncentive.determineReward(secondsLate);
LibTransfer.mintToken(C.bean(), incentiveAmount, account, mode);
emit LibIncentive.Incentivization(account, incentiveAmount);
return incentiveAmount;
}

Nevertheless, since the system update has already been performed at this point, the calculation of the seconsdLate value takes the new season number into account. This leads to an incorrect result since the product of s.sys.season.period.mul(s.sys.season.current) implies that all seasons, including the current one, have already ended.

Proof of concept

The following code is an ultra-minimalistic foundry test to illustrate the founded problem. Most of the elements and processes of the SeasonFacet contract have been ommited for simplicity. This code works as a standalone foundry contract test as it does not depend on external files or additional information. Executing it with a verbosity level of two (-vv) is enough to see the logged explanation of the outputs.

// SPDX-License-Identifier: MIT
pragma solidity >=0.6.0 <0.9.0;
import {Test, console} from "forge-std/Test.sol";
struct Season {
uint32 current;
uint32 sunriseBlock;
uint256 start;
uint256 period;
}
contract incentivizeTest is Test {
Season public season;
SeasonFacet public seasonFacet;
function setUp() public {
season.current = 1; // The first season of Beanstalk ever
season.start = block.timestamp;
season.period = 3600; // 1 hour
seasonFacet = new SeasonFacet();
}
function testIncentivize() public {
uint256 secondsLate;
uint256 expectedSecondsLate = 26; // an arbitrary amount of seconds
console.log("Initial season state:");
console.log("Current season: %i", season.current);
console.log("Season start: %i", season.start);
console.log("block timestamp: %i", block.timestamp);
vm.warp(block.timestamp + season.period + expectedSecondsLate); // 1 hour advance + secondsLate
vm.roll(block.number + 1);
// stepSeason
season.current += 1;
season.sunriseBlock = uint32(block.number);
console.log("\nFinal season state:");
console.log("Current season: %i", season.current);
console.log("Season start: %i", season.start);
console.log("block timestamp: %i", block.timestamp);
vm.expectRevert();
secondsLate = seasonFacet.incentivize(season); // The original secondsLate calculation fails
secondsLate = seasonFacet.incentivize2(season); // The corrected secondsLate calculation
assert(secondsLate == expectedSecondsLate);
}
}
contract SeasonFacet {
function incentivize(Season memory _season) public view returns (uint256) {
uint256 secondsLate = block.timestamp -
(_season.start + (_season.period * (_season.current)));
return secondsLate;
}
function incentivize2(Season memory _season) public view returns (uint256) {
uint256 secondsLate = block.timestamp -
(_season.start + (_season.period * (_season.current - 1)));
return secondsLate;
}
}

Impact

Impact : High

This issue causes that the incentivize function reverts due to the assignment of a negative value to a uint256 variable.

Likelihood: High

Tools Used

Manual Review

Recommended Mitigation

Considering only the past seasons as finished solves the issue:

function incentivize(address account, LibTransfer.To mode) private returns (uint256) {
uint256 secondsLate = block.timestamp.sub(
- s.sys.season.start.add(s.sys.season.period.mul(s.sys.season.current))
+ s.sys.season.start.add(s.sys.season.period.mul(s.sys.season.current - 1))
);
// reset USD Token prices and TWA reserves in storage for all whitelisted Well LP Tokens.
address[] memory whitelistedWells = LibWhitelistedTokens.getWhitelistedWellLpTokens();
for (uint256 i; i < whitelistedWells.length; i++) {
LibWell.resetUsdTokenPriceForWell(whitelistedWells[i]);
LibWell.resetTwaReservesForWell(whitelistedWells[i]);
}
uint256 incentiveAmount = LibIncentive.determineReward(secondsLate);
LibTransfer.mintToken(C.bean(), incentiveAmount, account, mode);
emit LibIncentive.Incentivization(account, incentiveAmount);
return incentiveAmount;
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 11 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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