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 (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 (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 (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 (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();
bs.mow(users[1], C.BEAN);
uint256[] memory depositIds = bs.getTokenDepositIdsForAccount(users[1], C.BEAN);
(, int96 stem) = LibBytes.unpackAddressAndStem(depositIds[0]);
LibTransfer.To mode;
season.rainSunrise();
bs.mow(users[1], C.BEAN);
setReserves(sopWell, 1000000e6, 1100e18);
season.rainSunrise();
bs.mow(users[1], C.BEAN);
season.rainSunrise();
season.rainSunrise();
season.droughtSunrise();
season.droughtSunrise();
vm.prank(users[1]);
bs.withdrawDeposit(C.BEAN, stem, 1000e6, 0);
bs.mow(users[1], C.BEAN);
season.rainSunrise();
bs.mow(users[1], C.BEAN);
uint256 userPlenty = bs.balanceOfPlenty(users[1], sopWell);
console.log(userPlenty);
assertEq(userPlenty, 0);
}