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

Report #32085

Report Date
June 7, 2024
Status
Closed
Payout

Upon mowing, user's `plenty` gets overwritten instead of increased

‣
Report Info

Report ID

#32085

Report type

Smart Contract

Has PoC?

Yes

Target

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

Impacts

Permanent freezing of unclaimed yield

Description

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);
    }