more contest details

Results Summary

Number of findings:

  • High: 0
  • Medium: 6
  • Low: 2

High Risk Findings

Medium Risk Findings

M-01. [M-2] Failure in Maintaining Gauge Points

Submitted by Zealynx, LordOfTerra. Selected submission by: Zealynx.

Relevant GitHub Links

https://github.com/Cyfrin/2024-02-Beanstalk-1/blob/a3658861af8f5126224718af494d02352fbb3ea5/protocol/contracts/beanstalk/sun/GaugePointFacet.sol#L36-L57

Summary

The defaultGaugePointFunction in the smart contract does not explicitly handle the scenario where the percentage of the Base Deposited Value (BDV) equals the optimal percentage (optimalPercentDepositedBdv), resulting in an unintended reduction of gauge points to 0 instead of maintaining their current value.

Impact

This behavior can lead to an undesired decrease in incentives for contract participants, potentially affecting participation and reward accumulation within the contract's ecosystem. Users may lose gauge points and, consequently, rewards due to a technical flaw rather than their actions.

Proof of Concept (PoC)

The testnew_GaugePointAdjustment() test demonstrated this flaw by providing inputs where currentGaugePoints = 1189, optimalPercentDepositedBdv = 64, and percentOfDepositedBdv = 64, expecting newGaugePoints to equal currentGaugePoints. However, the outcome was newGaugePoints = 0, indicating an unexpected reduction to zero.

function testnew_GaugePointAdjustment() public {
    uint256 currentGaugePoints = 1189; 
    uint256 optimalPercentDepositedBdv = 64; 
    uint256 percentOfDepositedBdv = 64; 

    uint256 newGaugePoints = gaugePointFacet.defaultGaugePointFunction(
        currentGaugePoints,
        optimalPercentDepositedBdv,
        percentOfDepositedBdv
    );

    assertTrue(newGaugePoints <= MAX_GAUGE_POINTS, "New gauge points exceed the maximum allowed");
    assertEq(newGaugePoints, currentGaugePoints, "Gauge points adjustment does not match expected outcome");
}

Recommendations

Implement Explicit Returns: Ensure the defaultGaugePointFunction has an explicit return for the case where gauge points should not be adjusted. This can be achieved by adding a final return statement that simply returns currentGaugePoints if neither condition for incrementing nor decrementing is met, as shown below:

else {
    return currentGaugePoints; 
}

M-02. Silo is not compatible with Fee-on-transfer or rebasing tokens

Submitted by InAllHonesty.

Relevant GitHub Links

https://github.com/Cyfrin/2024-02-Beanstalk-1/blob/a3658861af8f5126224718af494d02352fbb3ea5/protocol/contracts/libraries/Silo/LibTokenSilo.sol#L266-L292

https://github.com/Cyfrin/2024-02-Beanstalk-1/blob/a3658861af8f5126224718af494d02352fbb3ea5/protocol/contracts/libraries/Silo/LibTokenSilo.sol#L310-L351

https://github.com/Cyfrin/2024-02-Beanstalk-1/blob/a3658861af8f5126224718af494d02352fbb3ea5/protocol/contracts/libraries/Silo/LibTokenSilo.sol#L373-L427

Summary

According to the documentation there are certain conditions that need to be met for a token to be whitelisted:

Additional tokens may be added to the Deposit Whitelist via Beanstalk governance. In order for a token to be added to the Deposit Whitelist, Beanstalk requires:
1. The token address;
2. A function to calculate the Bean Denominated Value (BDV) of the token (see Section 14.2 of the whitepaper for complete formulas); and
3. The number of Stalk and Seeds per BDV received upon Deposit.

Thus if the community proposes any kind of Fee-on-Transfer or rebasing tokens like (PAXG or stETH) and the Beanstalk governance approves it, then the protocol needs to integrate them into the system. But as it is now the system is definitely not compatible with such tokens.

Vulnerability Details

deposit, depositWithBDV, addDepositToAccount, removeDepositFromAccount and any other silo accounting related functions perform operations using inputed/recorded amounts. They don't query the existing balance of tokens before or after receiving/sending in order to properly account for tokens that shift balance when received (FoT) or shift balance over time (rebasing).

Impact

Likelyhood - low/medium - At the moment of writing lido has over 31% of the ETH staked which makes stETH a very popular token. There's a strong chance that stakeholder would want to have stETH inside the silo.

Impact - High - It simply won't work.

Overall severity is medium.

Tools Used

Manual review

Recommendations

Clearly state in the docs that weird tokens won't be implemented via Governance Vote or adjust the code to check the token.balanceOf() before and after doing any operation related to the silo.

M-03. removeWhitelistStatus function Ignores updating milestoneSeason variable

Submitted by 0xBeastBoy, holydevoti0n. Selected submission by: 0xBeastBoy.

Relevant GitHub Links

https://github.com/Cyfrin/2024-02-Beanstalk-1/blob/a3658861af8f5126224718af494d02352fbb3ea5/protocol/contracts/libraries/Silo/LibWhitelistedTokens.sol#L187

Summary

The issue in the LibWhitelistedTokens:removeWhitelistStatus function is that it removes the Whitelist status of a token without considering the impact on other related variables, such as the milestoneSeason variable.

Vulnerability Details

milestoneSeason Is used in many functions for checking whether a token is whitelisted or not i.e.

 require(s.ss[token].milestoneSeason == 0, "Whitelist: Token already whitelisted");

If the milestoneSeason variable is not updated or cleared when removing the Whitelist status, it may lead to incorrect behavior in subsequent checks or operations that rely on this variable.

Impact

Removing the Whitelist status of a token without updating related variables can lead to inconsistencies in the data stored in the contract. The milestoneSeason variable, used for checking whitelist status in many functions, may still hold outdated or incorrect information after removing the status, potentially leading to unexpected behavior or vulnerabilities.

Tools Used

Manual Review

Recommendations

To address this issue, ensure that related variables, such as milestoneSeason, are appropriately updated or cleared when removing the Whitelist status of a token. If the milestoneSeason variable is no longer relevant after removing the Whitelist status, it should be updated or cleared to maintain data integrity.

M-04. Temperature and caseId are incorrectly adjusted when oracle fails

Submitted by holydevoti0n.

Relevant GitHub Links

https://github.com/BeanstalkFarms/Beanstalk/blob/6855a4702e920ae2e43fd4d0809dadee85e6ff6c/protocol/contracts/beanstalk/sun/SeasonFacet/SeasonFacet.sol#L51

https://github.com/BeanstalkFarms/Beanstalk/blob/6855a4702e920ae2e43fd4d0809dadee85e6ff6c/protocol/contracts/beanstalk/sun/SeasonFacet/Weather.sol#L75-L78

https://github.com/BeanstalkFarms/Beanstalk/blob/6855a4702e920ae2e43fd4d0809dadee85e6ff6c/protocol/contracts/libraries/LibDibbler.sol#L372-L388

https://github.com/BeanstalkFarms/Beanstalk/blob/6855a4702e920ae2e43fd4d0809dadee85e6ff6c/protocol/contracts/libraries/Oracle/LibEthUsdOracle.sol#L69

https://github.com/BeanstalkFarms/Beanstalk/blob/6855a4702e920ae2e43fd4d0809dadee85e6ff6c/protocol/contracts/libraries/Minting/LibWellMinting.sol#L172

https://github.com/BeanstalkFarms/Beanstalk/blob/6855a4702e920ae2e43fd4d0809dadee85e6ff6c/protocol/contracts/libraries/Minting/LibWellMinting.sol#L181

Summary

When user calls gm and the call for the chainlink oracle fails, it will return 0 for the deltaB value and this will cause a cascade effect, making the calculation of caseId = 3 and using the incorrect caseId to set up the new temperature on Weather.sol

function updateTemperature(int8 bT, uint256 caseId) private {
        uint256 t = s.w.t;
        if (bT < 0) {
            if (t <= uint256(-bT)) {
                // if (change < 0 && t <= uint32(-change)),
                // then 0 <= t <= type(int8).max because change is an int8.
                // Thus, downcasting t to an int8 will not cause overflow.
                bT = 1 - int8(t);
                s.w.t = 1;
            } else {
                s.w.t = uint32(t - uint256(-bT));
            }
        } else {
            s.w.t = uint32(t + uint256(bT));
        }

        emit TemperatureChange(s.season.current, caseId, bT);
    }

Every consumer of the temperature on the protocol will be affected like:

  • LibDibbler.morningTemperature
  • LibDibbler.beansToPods
  • LibDibbler.remainingPods
  • Sun.setSoilAbovePeg
  • Sun.stepSun
  • FieldFacet.maxTemperature
  • FieldFacet.totalSoil
  • FieldFacet._totalSoilAndTemperature
  • `FieldFacet.sowWithMin

Vulnerability Details

gm function uses the incorrect deltaB(0) to calculate the caseId which is then used to set the temperature.

   function gm(address account, LibTransfer.To mode) public payable returns (uint256) {
        int256 deltaB = stepOracle(); // @audit here if oracle failed, we update the season.timestamp and return deltaB zero here
        uint256 caseId = calcCaseIdandUpdate(deltaB); // @audit caseId will be 3 here if deltaB is zero
        LibGerminate.endTotalGermination(season, LibWhitelistedTokens.getWhitelistedTokens());
        LibGauge.stepGauge();
        stepSun(deltaB, caseId); // @audit wrong deltaB and caseId used here, setting the abovePeg to false and soil to zero
    }

Impact

The interest rate will be wrongly decreased to 1, compromising the protocol peg mechanism when it needs to be maintained with a high interest rate/ temperature.

Sow will be calculated with the lowest temperature, also compromising the peg mechanism due to the wrong exchange of Beans -> Sow -> Pods

Remaining pods function will return zero and users will have an inaccurate number representing their actual pods.

PoC

  1. Prepare the environment to work with Foundry + Updated Mocks https://gist.github.com/h0lydev/fcdb00c797adfdf8e4816031e095fd6c

  2. Make sure to have the mainnet forked through Anvil: anvil --fork-url https://rpc.ankr.com/eth

  3. Create the SeasonTemperature.t.sol file under the folder foundry and paste the code below. Then run forge test --match-contract SeasonTemperatureTest -vv.

// SPDX-License-Identifier: MIT
pragma solidity =0.7.6;
pragma abicoder v2;

import { Sun } from "contracts/beanstalk/sun/SeasonFacet/Sun.sol";
import { MockSeasonFacet } from "contracts/mocks/mockFacets/MockSeasonFacet.sol";
import { MockSiloFacet } from "contracts/mocks/mockFacets/MockSiloFacet.sol";
import { MockFieldFacet } from "contracts/mocks/mockFacets/MockFieldFacet.sol";
import { MockWhitelistFacet } from "contracts/mocks/mockFacets/MockWhitelistFacet.sol";
import {LibWhitelist} from "contracts/libraries/Silo/LibWhitelist.sol";
import { Utils } from "./utils/Utils.sol";
import "./utils/TestHelper.sol";
import "contracts/libraries/LibSafeMath32.sol";
import "contracts/C.sol";

contract SeasonTemperatureTest is MockSeasonFacet, TestHelper {
    using SafeMath for uint256;
    using LibSafeMath32 for uint32;

    bool oracleFailed;
  
    function setUp() public {
        console.log("diamondSetup");
        vm.createSelectFork('local');
        oracleFailed = false;
        setupDiamond();
        dewhitelistCurvePool();
        mintUnripeLPToUser1();   
        mintUnripeBeanToUser1();
        setOraclePrices(false, 1000e6, 1000e6, 1000e6);
        _setReservesForWell(1000000e6, 1000e18);
       
        // user / tokens
        mintTokenForUsers();
        setTokenApprovalForUsers();
       
        enableFertilizerAndMintActiveFertilizers();

        callSunriseForUser1();
    }


    ////////////     Setup functions     ////////////

    function setTokenApprovalForUsers() internal { 
        approveTokensForUser(deployer);
        approveTokensForUser(user1);
        approveTokensForUser(user2);
        approveTokensForUser(user3);
        approveTokensForUser(user4);
        approveTokensForUser(user5);
    }

    function mintTokenForUsers() internal { 
        mintWETHtoUser(deployer);
        mintWETHtoUser(user1);
        mintWETHtoUser(user2);
        mintWETHtoUser(user3);
        mintWETHtoUser(user4);
        mintWETHtoUser(user5);

        // mint C.bean() to users
        C.bean().mint(deployer, 10e6);
        C.bean().mint(user1, 10e6);
        C.bean().mint(user2, 10e6);
        C.bean().mint(user3, 10e6);
        C.bean().mint(user4, 10e6);
        C.bean().mint(user5, 10e6);
    }

    function approveTokensForUser(address user) prank(user) internal { 
        mockWETH.approve(address(diamond), type(uint256).max);
        unripeLP.approve(address(diamond), type(uint256).max);
        unripeBean.approve(address(diamond), type(uint256).max);
        well.approve(address(diamond), type(uint256).max);
        C.bean().approve(address(field), type(uint256).max);
        C.bean().approve(address(field), type(uint256).max);
    }

    function dewhitelistCurvePool() public {
        vm.prank(deployer);
        whitelist.dewhitelistToken(C.CURVE_BEAN_METAPOOL);
    }

    function mintWETHtoUser(address user) prank(user) internal {
        mockWETH.mint(user, 1000e18);
    }

    function mintUnripeLPToUser1() internal { 
        unripeLP.mint(user1, 1000e6);
    }

    function mintUnripeBeanToUser1() internal { 
        unripeBean.mint(user1, 1000e6);
    }

    function enableFertilizerAndMintActiveFertilizers() internal { 
        // second parameter is the unfertilizedIndex
        fertilizer.setFertilizerE(true, 10000e6);

        vm.prank(deployer);
        fertilizer.addFertilizerOwner(7500, 1e11, 99);

        vm.prank(deployer);
        fertilizer.addFertilizerOwner(6200, 1e11, 99);

        addUnripeTokensToFacet();
    }

    function addUnripeTokensToFacet() prank(deployer) internal { 
        unripe.addUnripeToken(C.UNRIPE_BEAN, C.BEAN, bytes32(0));
        unripe.addUnripeToken(C.UNRIPE_LP, C.BEAN_ETH_WELL, bytes32(0));
    }

    function callSunriseForUser1() prank(user1) internal {
        _ensurePreConditions();
        _advanceInTime(2 hours);
        season.sunrise();
    }

    function setOraclePrices(bool makeOracleFail, int256 chainlinkPrice, uint256 ethUsdtPrice, uint256 ethUsdcPrice) internal { 
        if (makeOracleFail) { 
            _addEthUsdPriceChainlink(0);
            oracleFailed = true;
        } else { 
            oracleFailed = false;
            _addEthUsdPriceChainlink(chainlinkPrice);
            _setEthUsdtPrice(ethUsdtPrice);
            _setEthUsdcPrice(ethUsdcPrice);
        }
    }

    ////////////////////////////////////////// TESTS //////////////////////////////////////////

    function testWrongCalcId_whenOracleFails() public { 
        _prepareForAbovePeg();
        _advanceInTime(1 hours);
        uint256 _snapId = vm.snapshot();
        
        // When sunrise succeeds
        vm.prank(user4);
        season.sunrise();

        // Then print results
        _printProtocolState();
        assertEq(season.getT(), 5, "when succeeds t should be 5");
        
        // Then revert it to prepare for the season that will fail
        vm.revertTo(_snapId);

        // Prepare for the season that will fail
        setOraclePrices(true, 0, 0, 0);

        // When sunrise fails
        vm.prank(user4);
        season.sunrise();

        console.log("Oracle failed, see results");
        _printProtocolState();
        assertEq(season.getT(), 1, "when succeeds t should be 1");

    }

    function _printProtocolState() internal { 
        console.log("-------------- Results --------------");
        console.log("");
        console.log("thisSowTime: ", season.thisSowTime());
        console.log("lastSowTime: ", season.lastSowTime());
        console.log("getUsdTokenPrice: ", season.getUsdTokenPrice());
        console.log("getReserve0: ", season.getReserve0());
        console.log("getReserve1: ", season.getReserve1());
        console.log("getAbovePeg: ", season.getAbovePeg());
        console.log("getSoil: ", season.getSoil());
        console.log("lastDSoil: ", season.lastDSoil());
        console.log("s.w.t: ", season.getT());
        console.log("remaining pods: ", field.remainingPods());
    }   

    function _prepareForAbovePeg() internal { 
        season.mockSetSopWell(address(well));
        season.captureWellE(address(well)); 
        season.setYieldE(5); // s.w.t
        setOraclePrices(false, 1000e6, 1000e6, 1000e6);

        season.setLastSowTimeE(1);
        season.setNextSowTimeE(10);
        season.calcCaseIdE(1e18, 1);
        season.setAbovePegE(true);
    }

    ////////////////////////////////////////// HELPERS //////////////////////////////////////////

    function _ensurePreConditions() internal { 
        assertTrue(season.thisSowTime() == type(uint32).max, "thisSowTime should be max");
        assertTrue(season.lastSowTime() == type(uint32).max, "thisLastSowTime should be max");
        assertEq(season.getIsFarm(), 1, "isFarm should be 1");
        assertEq(season.getUsdTokenPrice(), 1, "usdTokenPrice should be 1");
        assertEq(season.getReserve0(), 1, "reserve0 should be 1");
        assertEq(season.getReserve1(), 1, "reserve1 should be 1");
        assertFalse(season.getAbovePeg(), "pre - abovePeg should be false");
        assertEq(season.getSoil(), 0, "soil should be == 0");
    }
}

Output:

 handleRain caseId: 0
 -------------- Results --------------
  
  thisSowTime:  4294967295
  lastSowTime:  4294967295
  getUsdTokenPrice:  1
  getReserve0:  1
  getReserve1:  1
  getAbovePeg:  false
  getSoil:  462832752243
  lastDSoil:  0
  s.w.t:  5
  remaining pods:  467461079765

handleRain caseId: 3
 Oracle failed, see results
  -------------- Results --------------
  
  thisSowTime:  4294967295
  lastSowTime:  4294967295
  getUsdTokenPrice:  1
  getReserve0:  1
  getReserve1:  1
  getAbovePeg:  false
  getSoil:  0
  lastDSoil:  0
  s.w.t:  1
  remaining pods:  0

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 29.45s (3.32ms CPU time)

ps: a console.log was added to the handleRain function to print the caseId.

Result: In a normal scenario the temperature would have remained at the value 5 but in this case was set to 1 and remaining pods/soil are also set to zero when in fact they should not.

Tools Used

Manual Review & Foundry

Recommendations

It is noticed that the developers have the intention of never reverting the sunrise function to decrease the risk of depeg and breaking the incentive for users calling it. But at the same time, those state variables shouldn't be updated as if the system is working correctly because they will impact the next season as stated in this finding.

It is tricky to propose a simple fix to the problem without impacting the system as a whole. Here are a few ideas that could be used:

  1. (Recommended) An effective solution could be store the latest response from chainlink and in case it fails and the timeout(a limit that you can be added to accept a previous response from the oracle) is not reached yet, protocol could use the previous response. Liquity Protocol uses this approach, an example here: https://github.com/liquity/dev/blob/main/packages/contracts/contracts/PriceFeed.sol This solution will be effective for the protocol because the oracle is also called in different places like when minting fertilizers(getMintFertilizerOut), getting the well price(getRatiosAndBeanIndex), and getConstantProductWell. As the oracle is used along the protocol in many places, the latest successful price would be often up-to-date and within the limit time defined to use the previous price when the chainlink oracle fails.
  • Additionally, consider handling the errors properly before updating the deltaB and abovePeg variables, as these disrupt the peg mechanism logic.

M-05. Chainlink oracle returns stale price due to CHAINLINK_TIMEOUT variable in LibChainlinkOracle being set to 4 hours

Submitted by Igdbaxe, Bube, golanger85. Selected submission by: Bube.

Relevant GitHub Links

https://github.com/Cyfrin/2024-02-Beanstalk-1/blob/a3658861af8f5126224718af494d02352fbb3ea5/protocol/contracts/libraries/Oracle/LibChainlinkOracle.sol#L22

https://github.com/Cyfrin/2024-02-Beanstalk-1/blob/a3658861af8f5126224718af494d02352fbb3ea5/protocol/contracts/libraries/Oracle/LibChainlinkOracle.sol#L138-L149

Summary

The LibChainlinkOracle library utilizes a CHAINLINK_TIMEOUT constant set to 14400 seconds (4 hours). This duration is four times longer than the Chainlink heartbeat that is 3600 seconds (1 hour), potentially introducing a significant delay in recognizing stale or outdated price data.

Vulnerability Details

The LibChainlinkOracle::checkForInvalidTimestampOrAnswer function accepts three input arguments: timestamp, answer and currentTimestamp and check if the return answer from Chainlinlink Oracle or the timestamp is invalid:


    function checkForInvalidTimestampOrAnswer(
        uint256 timestamp,
        int256 answer,
        uint256 currentTimestamp
    ) private pure returns (bool) {
        // Check for an invalid timeStamp that is 0, or in the future
        if (timestamp == 0 || timestamp > currentTimestamp) return true;
        // Check if Chainlink's price feed has timed out
        if (currentTimestamp.sub(timestamp) > CHAINLINK_TIMEOUT) return true;
        // Check for non-positive price
        if (answer <= 0) return true;
    }

The function also checks if the difference between the currentTimestamp and the timestamp is greater then CHAINLINK_TIMEOUT. The CHAINLINK_TIMEOUT is defined to be 4 hours:


 uint256 public constant CHAINLINK_TIMEOUT = 14400; // 4 hours: 60 * 60 * 4

Impact

The Chainlink heartbeat indicates the expected frequency of updates from the oracle. The Chainlink heartbeat on Ethereum for Eth/Usd is 3600 seconds (1 hour).

https://docs.chain.link/data-feeds/price-feeds/addresses?network=ethereum&page=1&search=0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419

But the defined CHAINLINK_TIMEOUT in the LibChainlinkOracle is 14400 seconds (4 hours).

A CHAINLINK_TIMEOUT that is significantly longer than the heartbeat can lead to scenarios where the LibChainlinkOracle library accepts data that may no longer reflect current market conditions. Also, in volatile markets, a 4-hour window leads to accepting outdated prices, increasing the risk of price slippage.

Tools Used

Manual Review

Recommendations

Consider reducing the CHAINLINK_TIMEOUT to align more closely with the Chainlink heartbeat on Ethereum, enhancing the relevance of the price data.

M-06. [M] DOS in LibChainlinkOracle when not considering phaseId

Submitted by golanger85.

Relevant GitHub Links

https://github.com/Cyfrin/2024-02-Beanstalk-1/blob/a3658861af8f5126224718af494d02352fbb3ea5/protocol/contracts/libraries/Oracle/LibChainlinkOracle.sol#L18

Summary

LibChainlinkOracle is not fully compatible with Chainlink's data model due to the lack of support for phaseId and aggregatorRoundId. Chainlink's roundID is a composite number combining a phaseID and an aggregatorRoundID.

The phaseID changes whenever there is an upgrade to the underlying aggregator, and this change causes a significant jump in the roundID values due to the bit-shifting operation described in the documentation.

ref here: https://docs.chain.link/data-feeds/historical-data#solidity

Vulnerability Details

The Beanstalk LibChainlinkOracle misinterprets the progression of roundID as sequential, overlooking Chainlink's unique bundling of phaseId and aggregatorRoundId. With the advancement of phaseID, there's an exponential increase in roundID by 2^64, leading to a temporal suspension until a new interval commences. This will instigate a denial-of-service scenario. The getEthUsdTwap and getEthUsdPrice functions are particularly susceptible to this vulnerability, as they rely on accurate TWAP values for their computations, which effects for example any calls reliant on Oracle data.

function getRoundData(uint80 _roundId)
    public
    view
    virtual
    override
    returns (
      uint80 roundId,
      int256 answer,
      uint256 startedAt,
      uint256 updatedAt,
      uint80 answeredInRound
    )
  {
    (uint16 phaseId, uint64 aggregatorRoundId) = parseIds(_roundId);

    (
      uint80 roundId,
      int256 answer,
      uint256 startedAt,
      uint256 updatedAt,
      uint80 ansIn
    ) = phaseAggregators[phaseId].getRoundData(aggregatorRoundId);

    return addPhaseIds(roundId, answer, startedAt, updatedAt, ansIn, phaseId);
  }
 function latestRoundData()
    public
    view
    virtual
    override
    returns (
      uint80 roundId,
      int256 answer,
      uint256 startedAt,
      uint256 updatedAt,
      uint80 answeredInRound
    )
  {
    Phase memory current = currentPhase; // cache storage reads

    (
      uint80 roundId,
      int256 answer,
      uint256 startedAt,
      uint256 updatedAt,
      uint80 ansIn
    ) = current.aggregator.latestRoundData();

    return addPhaseIds(roundId, answer, startedAt, updatedAt, ansIn, current.id);
  }

https://etherscan.io/address/0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419#code

The code segment extracted from the ETH/USD Chainlink aggregator above highlights the composite structure of roundId, integrating both phaseId and aggregatorRoundId. As highlighted, an increment in phaseId leads to a substantial leap in roundId by 2^64, thereby bypassing a number of "rounds." Consequently, any attempt to query currentRound - 1 post-upgrade encounters a non-existent round, triggering a revert. This condition could persist up to 24 hours based on configuration, impacting the timely execution of getEthUsdTwap and getEthUsdPrice.

These functions, once operational again, might utilize altered TWAP values for computations, diverging from expected outcomes

Impact

If a phaseID increment occurs, it results in a jump in ``````roundID values, creating a gap in the sequence. When there are attempts to access round data for roundIDs within this gap, it will encounter inaccurate rounds, potentially causing the function to fail or return incorrect data, considering when the phaseID is incremented the roundID increases by 2 ** 64. This discrepancy can lead to a denial-of-servicein any calls to the oracle.

Tools Used

Manual Review

Recommendations

Check return values of roundId. If the roundID is a nonzero value and is reverting then the oracle needs to try again with a lower phaseId.

Low Risk Findings

L-01. LibEthUsdOracle returning wrong price on minAnswer, impacting fertilizer minting

Submitted by 0xTheBlackPanther, 0xBeastBoy, Igdbaxe. Selected submission by: 0xTheBlackPanther.

Relevant GitHub Links

https://github.com/Cyfrin/2024-02-Beanstalk-1/blob/a3658861af8f5126224718af494d02352fbb3ea5/protocol/contracts/libraries/Oracle/LibEthUsdOracle.sol#L63-L101

Summary

The Chainlink aggregator utilized in the LibEthUsdOracle contract lacks a mechanism to detect and handle scenarios where the price of an asset falls outside of a predetermined price band. This limitation can result in the oracle returning the minPrice instead of the actual price of the asset during extreme market events, such as a significant drop in value. Consequently, users may continue to interact with the system, such as minting fertilizer tokens, using inaccurate price data. similar case happened with Venus on BSC when LUNA imploded

More Refs for similar issues like this:

Impact

The Chainlink aggregator can lead to potential exploitation of price discrepancies during extreme market conditions. For instance, if the price of an asset experiences a sudden crash, the oracle may continue to provide the minPrice, allowing users to conduct transactions at incorrect prices. This could result in financial losses for users and undermine the integrity of the system.

In our scenario, the mintFertilizer function within the FertilizerFacet contract, although it falls out of our immediate scope, relies on the LibEthUsdOracle.getEthUsdPrice() function (within our scope) to fetch the ETH/USD price from the Chainlink oracle. This price is crucial for calculating the amount of Fertilizer tokens that can be acquired with the provided wethAmountIn of WETH. However, if this function returns the minPrice during extreme market events, it would not reflect the actual price of the asset. Consequently, users could continue to mint fertilizer tokens using this inaccurate price data, leading to transactions occurring at incorrect prices.

Recommendation

It is recommended to enhance the Chainlink oracle (LibEthUsdOracle) by implementing a mechanism to check the returned answer against predefined minPrice and maxPrice bounds. If the answer falls outside of these bounds, the oracle should revert the transaction, indicating that the price data is not reliable due to market conditions.

L-02. No validation of total supply of unripe beans & Lp in percentBeansRecapped & percentLPRecapped

Submitted by 0xBeastBoy.

Relevant GitHub Links

https://github.com/Cyfrin/2024-02-Beanstalk-1/blob/a3658861af8f5126224718af494d02352fbb3ea5/protocol/contracts/libraries/LibUnripe.sol#L32

Summary

LibUnripe:percentBeansRecapped & LibUnripe:percentLPRecapped functions calculate the percentage of Unripe Beans and Unripe LPs that have been recapitalized, respectively. These percentages are calculated based on the underlying balance of the Unripe Tokens and their total supply. There is no check if the totalSupply is zero which is used as division in the calculation.

Vulnerability Details

See the following code for both the functions:

  /**
     * @notice Returns the percentage that Unripe Beans have been recapitalized.
     */
 function percentBeansRecapped() internal view returns (uint256 percent) {
        AppStorage storage s = LibAppStorage.diamondStorage();
        return s.u[C.UNRIPE_BEAN].balanceOfUnderlying.mul(DECIMALS).div(C.unripeBean().totalSupply());
  }

  /**
     * @notice Returns the percentage that Unripe LP have been recapitalized.
     */
  function percentLPRecapped() internal view returns (uint256 percent) {
        AppStorage storage s = LibAppStorage.diamondStorage();
        return C.unripeLPPerDollar().mul(s.recapitalized).div(C.unripeLP().totalSupply());
   }

Impact

If the totalSupply in these two functions becomes zero, the calculation of the percentage of recapitalized Unripe Beans or LP tokens would result in a division by zero error. This is because of the denominator in the calculation. When the total supply is zero, dividing by zero is not defined in Solidity, and the contract would revert with an error.

These functions are used widely across the different contracts at crucial places. So they will effect a lot of functionalities.

Tools Used

Manual Review

Recommendations

To handle this scenario, appropriate checks should be added to ensure that the totalSupply of Unripe Beans or LP tokens is non-zero before performing the division operation.