If user has previous unclaimed plenty, they'll lose it when accruing new plenty.
Vulnerability Details
When a user mows, if there has been a flood, LibFlood.handleRainAndSops is called.
Then, if a sop has occurred since lastUpdate, the user is calculated plenty that they're eligible to claim.
The problem is that plenty is not increased, but actually overwritten. If there has been any prior unclaimed plenty it will be lost and the user will be able to only claim the newly accrued one.
Impact Details
User will lose portion of their yield
References
Add any relevant links to documentation or code
Proof of concept
For the Proof of Concept, we'll use the provided Beanstalk Foundry test suite.
add the following function to MockSiloFacet.sol
function setPlenty(address account, address well, uint256 amount) public {
s.accts[account].sop.perWellPlenty[well].plenty = amount;
}
then add this function in the beginning of the provided testOneSop function in Flood.t.sol
In the end, even that the user had previous plenty balance, they test results will be the exact same as in the case where the user had no previous plenty balance (because it is overwritten).
BIC Response
We have been unable to reproduce the issue described in the report and the submitter has also determined that the report is invalid.
Thus, we are closing this report and no reward will be issued.
// if the user has not migrated from siloV2, revert.
uint32 lastUpdate = _lastUpdate(account);
// sop data only needs to be updated once per season,
// if it started raining and it's still raining, or there was a sop
uint32 currentSeason = s.sys.season.current;
if (s.sys.season.rainStart > s.sys.season.stemStartSeason) {
if (lastUpdate <= s.sys.season.rainStart && lastUpdate <= currentSeason) {
// Increments `plenty` for `account` if a Flood has occured.
// Saves Rain Roots for `account` if it is Raining.
LibFlood.handleRainAndSops(account, lastUpdate);
}
if (s.sys.season.lastSopSeason > lastUpdate) {
address[] memory tokens = LibWhitelistedTokens.getWhitelistedWellLpTokens();
for (uint i; i < tokens.length; i++) {
s.accts[account].sop.perWellPlenty[tokens[i]].plenty = balanceOfPlenty(
account,
tokens[i]
);
}
s.accts[account].lastSop = s.sys.season.lastSop;
}
function testOneSop() public {
uint256 userCalcPlenty = 25595575914848452999;
uint256 userCalcPlentyPerRoot = 2558534177813719812;
address sopWell = C.BEAN_ETH_WELL;
bs.setPlenty(users[1], sopWell, 1000); // @audit - even though that the user will have prior `plenty` balance, the rest of the test will execute as if they didn't
setReserves(sopWell, 1000000e6, 1100e18);
season.rainSunrise();
bs.mow(users[1], C.BEAN);
vm.expectEmit();
emit SeasonOfPlentyField(0); // zero in this test since no beans in podline
vm.expectEmit();
emit SeasonOfPlentyWell(
seasonGetters.time().current + 1, // flood will happen next season
sopWell,
C.WETH,
51191151829696906017
);
season.rainSunrise();
Season memory s = seasonGetters.time();
assertEq(s.lastSop, s.rainStart);
assertEq(s.lastSopSeason, s.current);
// check weth balance of beanstalk
assertEq(IERC20(C.WETH).balanceOf(BEANSTALK), 51191151829696906017);
// after the swap, the composition of the pools are
uint256[] memory balances = IWell(sopWell).getReserves();
assertEq(balances[0], 1048808848170);
assertEq(balances[1], 1048808848170303093983);
// tracks user plenty before update
uint256 userPlenty = bs.balanceOfPlenty(users[1], sopWell);
assertEq(userPlenty, userCalcPlenty);
// tracks user plenty after update
bs.mow(users[1], C.BEAN);
SiloGettersFacet.AccountSeasonOfPlenty memory userSop = siloGetters.balanceOfSop(users[1]);
assertEq(userSop.lastRain, 6);
assertEq(userSop.lastSop, 6);
assertEq(userSop.roots, 10004000e18);
assertGt(userSop.farmerSops.length, 0);
assertEq(userSop.farmerSops[0].well, sopWell);
assertEq(userSop.farmerSops[0].wellsPlenty.plenty, userCalcPlenty);
assertEq(userSop.farmerSops[0].wellsPlenty.plentyPerRoot, userCalcPlentyPerRoot);
// each user should get half of the eth gained
assertEq(bs.balanceOfPlenty(users[2], sopWell), userCalcPlenty);
// tracks user2 plenty after update
bs.mow(users[2], C.BEAN);
userSop = siloGetters.balanceOfSop(users[2]);
assertEq(userSop.lastRain, 6);
assertEq(userSop.lastSop, 6);
assertEq(userSop.roots, 10004000e18);
assertEq(userSop.farmerSops[0].well, sopWell);
assertEq(userSop.farmerSops[0].wellsPlenty.plenty, userCalcPlenty);
assertEq(userSop.farmerSops[0].wellsPlenty.plentyPerRoot, userCalcPlentyPerRoot);
// claims user plenty
bs.mow(users[2], C.BEAN);
vm.prank(users[2]);
bs.claimPlenty(sopWell, IMockFBeanstalk.To.EXTERNAL);
assertEq(bs.balanceOfPlenty(users[2], sopWell), 0);
assertEq(IERC20(C.WETH).balanceOf(users[2]), userCalcPlenty);
}