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

If user has not mown since germination, they'll lose their portion of `plenty`

Summary

If user has not mown since germination, they'll lose their portion of plenty

Vulnerability Details

After a user deposits, they have to wait 2 seasons until their deposit germinates (until they receive their stalk and roots). Currently, if the user hasn't mown since germination, their roots will be included in the sum of all roots that should receive plenty, although that's only on paper and they won't realistically be able to claim any of these funds.

When a user deposits, LibSilo.mintGerminatingStalk is called which adds the necessary stalk to the global accounting of unclaimedGerminating.

function mintGerminatingStalk(address account, uint128 stalk, GerminationSide side) internal {
AppStorage storage s = LibAppStorage.diamondStorage();
s.accts[account].germinatingStalk[side] += stalk;
// germinating stalk are either newly germinating, or partially germinated.
// Thus they can only be incremented in the latest or previous season.
uint32 season = s.sys.season.current;
if (LibGerminate.getSeasonGerminationSide() == side) {
s.sys.silo.unclaimedGerminating[season].stalk += stalk;
} else {
s.sys.silo.unclaimedGerminating[season - 1].stalk += stalk;
}
// emit events.
emit LibGerminate.FarmerGerminatingStalkBalanceChanged(
account,
int256(uint256(stalk)),
side
);
emit LibGerminate.TotalGerminatingStalkChanged(season, int256(uint256(stalk)));
}

Then, when the actual germinating season comes, the global stalk and roots accounting is increased by these values

function endTotalGermination(uint32 season, address[] memory tokens) external {
AppStorage storage s = LibAppStorage.diamondStorage();
// germination can only occur after season 3.
if (season < 2) return;
uint32 germinationSeason = season.sub(2);
// base roots are used if there are no roots in the silo.
// root calculation is skipped if no deposits have been made
// in the season.
uint128 finishedGerminatingStalk = s.sys.silo.unclaimedGerminating[germinationSeason].stalk;
uint128 rootsFromGerminatingStalk;
if (s.sys.silo.roots == 0) {
rootsFromGerminatingStalk = finishedGerminatingStalk.mul(uint128(C.getRootsBase()));
} else if (s.sys.silo.unclaimedGerminating[germinationSeason].stalk > 0) {
rootsFromGerminatingStalk = s
.sys
.silo
.roots
.mul(finishedGerminatingStalk)
.div(s.sys.silo.stalk)
.toUint128();
}
s.sys.silo.unclaimedGerminating[germinationSeason].roots = rootsFromGerminatingStalk;
// increment total stalk and roots based on unclaimed values.
s.sys.silo.stalk = s.sys.silo.stalk.add(finishedGerminatingStalk);
s.sys.silo.roots = s.sys.silo.roots.add(rootsFromGerminatingStalk);

Then, when a new raining season starts, this global accounting value is used to determine the total amount of roots to distribute the rewards to:

function handleRain(uint256 caseId) external {
AppStorage storage s = LibAppStorage.diamondStorage();
// cases % 36 3-8 represent the case where the pod rate is less than 5% and P > 1.
if (caseId.mod(36) < 3 || caseId.mod(36) > 8) {
if (s.sys.season.raining) {
s.sys.season.raining = false;
}
return;
} else if (!s.sys.season.raining) {
s.sys.season.raining = true;
address[] memory wells = LibWhitelistedTokens.getCurrentlySoppableWellLpTokens();
// Set the plenty per root equal to previous rain start.
uint32 season = s.sys.season.current;
uint32 rainstartSeason = s.sys.season.rainStart;
for (uint i; i < wells.length; i++) {
s.sys.sop.sops[season][wells[i]] = s.sys.sop.sops[rainstartSeason][wells[i]];
}
s.sys.season.rainStart = s.sys.season.current;
s.sys.rain.pods = s.sys.fields[s.sys.activeField].pods;
s.sys.rain.roots = s.sys.silo.roots;

Hence, we've just shown that although these roots are not claimed to the user, they're included in the plenty allocation.

Now, if we look at the code of _mow, we'll see that first handleRainAndSops is called and then LibGerminate.endAccountGermination.

function _mow(address account, address token) external {
AppStorage storage s = LibAppStorage.diamondStorage();
// 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);
}
}
// End account germination.
if (lastUpdate < currentSeason) {
LibGerminate.endAccountGermination(account, lastUpdate, currentSeason);
}
// Calculate the amount of Grown Stalk claimable by `account`.
// Increase the account's balance of Stalk and Roots.
__mow(account, token);
// update lastUpdate for sop and germination calculations.
s.accts[account].lastUpdate = currentSeason;
}

Since handleRainAndSops calculates the plenty user should get based on their roots and LibGerminate.endAccountGermination is the function which actually gives the user their roots, because of the order of the functions, in case the user hasn't mown since germination, user will not get the plenty they should get.

Impact

Loss of funds

Tools Used

Manual review

Recommendations

Simply reversing the order would introduce new issue. Fix is non-trivial.

Updates

Lead Judging Commences

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

If user has not mown since germination, they'll lose their portion of `plenty`

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:

If user has not mown since germination, they'll lose their portion of `plenty`

Support

FAQs

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