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

User will lose the rest of their `plenty` if they fully withdraw during/after `rain`.

Summary

User will lose their share of plenty

Vulnerability Details

When a user mows during a raining season, they're set rainRoots. If a sop (Season of plenty) occurs following the rain season, these rain roots are later used to calculate user's plenty.

if (s.sys.season.raining) {
// If rain started after update, set account variables to track rain.
if (s.sys.season.rainStart > lastUpdate) {
s.accts[account].lastRain = s.sys.season.rainStart;
s.accts[account].sop.rainRoots = s.accts[account].roots;
}
if (s.accts[account].lastRain > 0) {
// if the last processed SOP = the lastRain processed season,
// then we use the stored roots to get the delta.
if (a.lastSop == a.lastRain) {
previousPPR = a.sop.perWellPlenty[well].plentyPerRoot;
} else {
previousPPR = s.sys.sop.sops[a.lastSop][well];
}
uint256 lastRainPPR = s.sys.sop.sops[s.accts[account].lastRain][well];
// If there has been a SOP duing the rain sesssion since last update, process SOP.
if (lastRainPPR > previousPPR) {
uint256 plentyPerRoot = lastRainPPR - previousPPR;
previousPPR = lastRainPPR;
plenty = plenty.add(
plentyPerRoot.mul(s.accts[account].sop.rainRoots).div(C.SOP_PRECISION)
);
}

The problem is that in the beginning of the handleRainAndSops function, if the user has 0 roots, it does a early return and does not allocate the user the necessary plenty for the user's rainRoots.

function handleRainAndSops(address account, uint32 lastUpdate) private {
AppStorage storage s = LibAppStorage.diamondStorage();
// If no roots, reset Sop counters variables
if (s.a[account].roots == 0) {
s.a[account].lastSop = s.season.rainStart;
s.a[account].lastRain = 0;
return;
}

Considering that if user's lastUpdated > rainStart user would have to wait until next rainSeason to claim the plenty from the previous, this issue also means that full withdraws inbetween rains will result in losing user's allocated plenty.

Impact

Loss of funds

Tools Used

Manual review

Recommendations

Do not make an early return

PoC

Adding two PoCs, first one is a withdraw during the rainSeason and the other one is after the rainSeason has finished. Add them to Flood.t.sol

function test_lostPlenty() public {
address sopWell = C.BEAN_ETH_WELL;
setReserves(sopWell, 1000000e6, 1100e18);
season.rainSunrise();
bs.mow(users[1], C.BEAN);
uint256[] memory depositIds = bs.getTokenDepositIdsForAccount(users[1], C.BEAN);
(, int96 stem) = LibBytes.unpackAddressAndStem(depositIds[0]);
LibTransfer.To mode;
vm.prank(users[1]);
bs.withdrawDeposit(C.BEAN, stem, 1000e6, 0);
assertEq(siloGetters.balanceOfRainRoots(users[1]), 10004000000000000000000000);
assertEq(siloGetters.balanceOfRoots(users[1]), 0);
season.rainSunrise();
season.droughtSunrise();
season.droughtSunrise();
bs.mow(users[1], C.BEAN);
uint256 userPlenty = bs.balanceOfPlenty(users[1], sopWell);
assertEq(userPlenty, 0);
}
function test_lostPlenty2() public {
address sopWell = C.BEAN_ETH_WELL;
season.rainSunrise(); // rainStart
bs.mow(users[1], C.BEAN); // lastUpdated = rainStart
uint256[] memory depositIds = bs.getTokenDepositIdsForAccount(users[1], C.BEAN);
(, int96 stem) = LibBytes.unpackAddressAndStem(depositIds[0]);
LibTransfer.To mode;
season.rainSunrise(); // plenty not yet accrued here.
bs.mow(users[1], C.BEAN); // lastUpdated = rainStart + 1
setReserves(sopWell, 1000000e6, 1100e18); // setting reserves so next season plenty is accrued
season.rainSunrise(); // plenty accrued.
bs.mow(users[1], C.BEAN); // this will do nothing as lastUpdated > rainStart
season.rainSunrise();
season.rainSunrise(); // 2nd sop
season.droughtSunrise();
season.droughtSunrise();
vm.prank(users[1]);
bs.withdrawDeposit(C.BEAN, stem, 1000e6, 0); // adjust this to 1 wei less and test will fail
bs.mow(users[1], C.BEAN);
season.rainSunrise(); // must be raining to enter handleRainAndSops
bs.mow(users[1], C.BEAN);
uint256 userPlenty = bs.balanceOfPlenty(users[1], sopWell);
console.log(userPlenty);
assertEq(userPlenty, 0);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 11 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Loss/lock of rewards in case of withdrawing fully before a flood

Appeal created

deadrosesxyz Submitter
11 months ago
inallhonesty Lead Judge
11 months ago
inallhonesty Lead Judge 11 months ago
Submission Judgement Published
Validated
Assigned finding tags:

User will lose the rest of their `plenty` if they fully withdraw during/after `rain`

Support

FAQs

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