Beanstalk Notion
Beanstalk Notion
/
🪲
Bug Reports
/
BIC Notes
/
📄
Report #32092
📄

Report #32092

Report Date
June 8, 2024
Status
Confirmed
Payout
10,000

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

‣
Report Info

Report ID

#32092

Report type

Smart Contract

Has PoC?

Yes

Target

https://etherscan.io/address/0xC1E088fC1323b20BCBee9bd1B9fC9546db5624C5

Impacts

Permanent freezing of unclaimed yield

Description

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

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.

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

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:

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

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

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 Details

If user hasn't mown since germination, they'll lose all of the sop rewards that they should be getting. It's very likely that users will not mow immediately upon germination, so losses are extremely likely for new depositors.

References

Add any relevant links to documentation or code

Proof of concept

I've used the provided Foundry test suite for the currently audited code. Although there are some differences in the code, the mentioned part is the exact same, hence the issue is present in both the live code and the undeployed one.

The two test cases demonstrate that depending on whether the user has mown since germination they receive plenty or not

Add the tests to Flood.t.sol

BIR-17: Plenty for Unmown Germinating Deposits

BIC Response

We agree with the selected Impact of "Permanent freezing of unclaimed yield", and thus High severity. Given that the practicable economic damage is effectively zero due to the low likelihood of Flood occurring in the near future, the BIC has determined that this bug report be rewarded the minimum reward for High severity reports of 10k Beans.

    function mintGerminatingStalk(
        address account,
        uint128 stalk,
        LibGerminate.Germinate germ
    ) internal {
        AppStorage storage s = LibAppStorage.diamondStorage();

        if (germ == LibGerminate.Germinate.ODD) {
            s.a[account].farmerGerminating.odd = s.a[account].farmerGerminating.odd.add(stalk);
        } else {
            s.a[account].farmerGerminating.even = s.a[account].farmerGerminating.even.add(stalk);
        }

        // germinating stalk are either newly germinating, or partially germinated.
        // Thus they can only be incremented in the latest or previous season.
        uint32 germinationSeason = s.season.current;
        if (LibGerminate.getSeasonGerminationState() == germ) {
            s.unclaimedGerminating[germinationSeason].stalk =
                s.unclaimedGerminating[germinationSeason].stalk.add(stalk);
        } else {
            germinationSeason = germinationSeason.sub(1);
            s.unclaimedGerminating[germinationSeason].stalk =
                s.unclaimedGerminating[germinationSeason].stalk
                .add(stalk);
        }

        // emit events.
        emit LibGerminate.FarmerGerminatingStalkBalanceChanged(account, stalk, germ);
        emit LibGerminate.TotalGerminatingStalkChanged(germinationSeason, stalk);
    }
    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.unclaimedGerminating[germinationSeason].stalk;
        uint128 rootsFromGerminatingStalk;
        if (s.s.roots == 0) {
            rootsFromGerminatingStalk = finishedGerminatingStalk.mul(uint128(C.getRootsBase()));
        } else if (s.unclaimedGerminating[germinationSeason].stalk > 0) {
            rootsFromGerminatingStalk = s.s.roots.mul(finishedGerminatingStalk).div(s.s.stalk).toUint128();
        }
        s.unclaimedGerminating[germinationSeason].roots = rootsFromGerminatingStalk;
        // increment total stalk and roots based on unclaimed values.
        s.s.stalk = s.s.stalk.add(finishedGerminatingStalk);
        s.s.roots = s.s.roots.add(rootsFromGerminatingStalk);
    function handleRain(uint256 caseId, address well) internal {
        // 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.season.raining) {
                s.season.raining = false;
            }
            return;
        } else if (!s.season.raining) {
            s.season.raining = true;
            // Set the plenty per root equal to previous rain start.
            s.sops[s.season.current] = s.sops[s.season.rainStart];
            s.season.rainStart = s.season.current;
            s.r.pods = s.f.pods;
            s.r.roots = s.s.roots;
        } else {
            if (s.r.roots > 0) {
                // initialize sopWell if it is not already set.
                if (s.sopWell == address(0)) s.sopWell = well;
                sop();
            }
        }
    }
    function _mow(address account, address token) internal {
        AppStorage storage s = LibAppStorage.diamondStorage();

        // if the user has not migrated from siloV2, revert.
        (bool needsMigration, uint32 lastUpdate) = migrationNeeded(account);
        require(!needsMigration, "Silo: Migration needed");

        // if the user hasn't updated prior to the seedGauge/siloV3.1 update,
        // perform a one time `lastStem` scale.
        if (
            (lastUpdate < s.season.stemScaleSeason && lastUpdate > 0) ||
            (lastUpdate == s.season.stemScaleSeason && checkStemEdgeCase(account))
        ) {
            migrateStems(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.season.current;
        if (s.season.rainStart > s.season.stemStartSeason) {
            if (lastUpdate <= s.season.rainStart && lastUpdate <= currentSeason) {
                // Increments `plenty` for `account` if a Flood has occured.
                // Saves Rain Roots for `account` if it is Raining.
                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.a[account].lastUpdate = currentSeason;
    }
    function test_notGerminated() public { 
        address customUser = address(1337);

        C.bean().mint(customUser, 10_000e6);
        vm.prank(customUser);
        C.bean().approve(BEANSTALK, type(uint256).max);
        vm.prank(customUser);
        bs.deposit(C.BEAN, 1000e6, 0);

        season.siloSunrise(0);
        season.siloSunrise(0);
        season.siloSunrise(0);      // should be germinated by now, not mown though 

        address sopWell = C.BEAN_ETH_WELL;
        setReserves(sopWell, 1000000e6, 1100e18);

        
        season.rainSunrise();
        season.rainSunrise();

        bs.mow(customUser, C.BEAN);

        uint256 balanceOfPlenty =  bs.balanceOfPlenty(customUser, sopWell);
        assertEq(0, balanceOfPlenty);
    }

    function test_Germinated() public { 
        address customUser = address(1337);

        C.bean().mint(customUser, 10_000e6);
        vm.prank(customUser);
        C.bean().approve(BEANSTALK, type(uint256).max);
        vm.prank(customUser);
        bs.deposit(C.BEAN, 1000e6, 0);

        season.siloSunrise(0);
        season.siloSunrise(0);
        season.siloSunrise(0);      // should be germinated by now, not mown though 

        address sopWell = C.BEAN_ETH_WELL;
        setReserves(sopWell, 1000000e6, 1100e18);

        bs.mow(customUser, C.BEAN);  
        season.rainSunrise();
        season.rainSunrise();

        bs.mow(customUser, C.BEAN);

        uint256 balanceOfPlenty =  bs.balanceOfPlenty(customUser, sopWell);
        assertEq(12799706762155710963, balanceOfPlenty);
    }