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

Report #35183

Report Date
September 8, 2024
Status
Closed
Payout

Data Overwriting in Storage

‣
Report Info

Report ID

#35183

Report type

Smart Contract

Has PoC?

Yes

Target

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

Impacts

Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol)

Description

The contract uses inline assembly to read and write to storage slots, which involves calculating and manipulating storage slot offsets directly. This approach is error-prone and could potentially lead to overwriting unintended data if the slot calculations or assumptions about storage layout are incorrect. This type of issue can compromise the integrity of the stored data, potentially leading to unexpected behavior or security vulnerabilities in the contract.

Affected Function:

update(uint256[] calldata reserves, bytes calldata data) _init(bytes32 slot, uint40 lastTimestamp, uint256[] memory reserves)

Vulnerability Details

''' function update(uint256[] calldata reserves, bytes calldata data) external { // Require two token well if (reserves.length != 2) { revert TooManyTokens(); }

}

''' Vulnerability Type: Data Overwriting

Affected Storage Variables:

lastReserves emaReserves cumulativeReserves

Issue Description: The update function performs operations that write to specific slots in the contract’s storage. There is a risk that the calculation and storage operations can overwrite critical data if not handled correctly. The vulnerability arises from:

Incorrect Slot Management: The function manipulates storage slots using low-level assembly, which can lead to data corruption if the slot calculations are incorrect. Potential Data Overwriting: If the slots are not managed correctly, there is a risk that new data could overwrite existing critical data, leading to incorrect reserve calculations and potentially breaking the contract's logic.

Impact Details

Data Corruption: Incorrect handling or overwriting of storage can corrupt critical financial data. Operational Failure: Users might experience incorrect reserve values or disruptions in functionality. Financial Risk: Inaccurate reserve data can lead to significant financial losses or mismanagement of assets.

References

Consensys: Smart Contract Security Best Practices OpenZeppelin: Smart Contract Security Ethereum Yellow Paper: Ethereum Protocol Specification

Proof of concept

Deploy a Malicious Contract: Create a contract that will interact with the Beanstalk contract. This contract will send a crafted update call with data designed to corrupt the storage.

Craft Malicious Data: Prepare data for the update call that will intentionally corrupt storage. This involves creating payloads that will overwrite critical storage values.

Execute the Malicious Update Call: Use the malicious contract to call the update function on the Beanstalk contract with the crafted data.

''' // SPDX-License-Identifier: MIT pragma solidity ^0.8.0;

interface IBeanstalk { function update(uint256[] calldata reserves, bytes calldata data) external; }

contract MaliciousBeanstalkExploit { IBeanstalk public beanstalk;

}

'''

Immunefi Response

Unfortunately, after reviewing your report, Immunefi has decided to close it due to the assessed impact being out of scope.

Immunefi review:

  • The claimed impact Griefing by the whitehat is in scope of the bug bounty program but the assessed impact doesn't match with the claimed impact for the following reasons.
    • Whitehat did not prove actual damage to the users or the protocol, as he did not show how the storage slots containing important data can be overwritten
  • assessed asset by the triage team is in scope for the bug bounty program

Please note that the project will receive a report of the closed submission and may choose to re-open it, but they are not obligated to do so.

(bytes16 alpha, uint256 capInterval, CapReservesParameters memory crp) =
    abi.decode(data, (bytes16, uint256, CapReservesParameters));
uint256 numberOfReserves = reserves.length;
PumpState memory pumpState;

// All reserves are stored starting at the msg.sender address slot in storage.
bytes32 slot = _getSlotForAddress(msg.sender);

// Read: Last Timestamp & Last Reserves
(, pumpState.lastTimestamp, pumpState.lastReserves) = slot.readLastReserves();

// If the last timestamp is 0, then the pump has never been used before.
if (pumpState.lastTimestamp == 0) {
    _init(slot, uint40(block.timestamp), reserves);
    return;
}

bytes16 alphaN;
bytes16 deltaTimestampBytes;
uint256 capExponent;

// Isolate in brackets to prevent stack too deep errors
{
    uint256 deltaTimestamp = _getDeltaTimestamp(pumpState.lastTimestamp);
    // If no time has passed, don't update the pump reserves.
    if (deltaTimestamp == 0) return;
    alphaN = alpha.powu(deltaTimestamp);
    deltaTimestampBytes = deltaTimestamp.fromUInt();
    // Round up in case capInterval > block time to guarantee capExponent > 0 if time has passed since the last update.
    capExponent = calcCapExponent(deltaTimestamp, capInterval);
}

pumpState.lastReserves = _capReserves(msg.sender, pumpState.lastReserves, reserves, capExponent, crp);

// Read: Cumulative & EMA Reserves
// Start at the slot after pumpState.lastReserves
uint256 numSlots = _getSlotsOffset(numberOfReserves);
assembly {
    slot := add(slot, numSlots)
}
pumpState.emaReserves = slot.readBytes16(numberOfReserves);
assembly {
    slot := add(slot, numSlots)
}
pumpState.cumulativeReserves = slot.readBytes16(numberOfReserves);

bytes16 lastReserve;
for (uint256 i; i < numberOfReserves; ++i) {
    lastReserve = pumpState.lastReserves[i].fromUIntToLog2();
    pumpState.emaReserves[i] =
        lastReserve.mul((ABDKMathQuad.ONE.sub(alphaN))).add(pumpState.emaReserves[i].mul(alphaN));
    pumpState.cumulativeReserves[i] = pumpState.cumulativeReserves[i].add(lastReserve.mul(deltaTimestampBytes));
}

// Write: Cumulative & EMA Reserves
// Order matters: work backwards to avoid using a new memory var to count up
slot.storeBytes16(pumpState.cumulativeReserves);
assembly {
    slot := sub(slot, numSlots)
}
slot.storeBytes16(pumpState.emaReserves);
assembly {
    slot := sub(slot, numSlots)
}

// Write: Last Timestamp & Last Reserves
slot.storeLastReserves(uint40(block.timestamp), pumpState.lastReserves);
constructor(address _beanstalk) {
    beanstalk = IBeanstalk(_beanstalk);
}

// Function to execute the malicious update
function exploitUpdate() external {
    // Prepare reserves - adjust these values based on the Beanstalk contract's expectations
    uint256;
    reserves[0] = 0; // Example value to potentially trigger unexpected behavior
    reserves[1] = 0; // Example value to potentially trigger unexpected behavior

    // Prepare malicious data to overwrite critical storage
    bytes memory data = abi.encode(
        bytes16(0x00000000000000000000000000000000), // Example alpha value
        uint256(0), // Example capInterval, adjust as needed
        CapReservesParameters({
            // Adjust parameters to match the Beanstalk implementation
        })
    );

    // Call update with malicious data
    beanstalk.update(reserves, data);
}