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

Report #31473

Report Date
May 19, 2024
Status
Closed
Payout

Front-running vulnerability in Fertilizer contract

‣
Report Info

Report ID

#31473

Report type

Smart Contract

Has PoC?

Yes

Target

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

Impacts

Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield

Description

The Fertilizer contract is vulnerable to front-running, allowing a malicious user to manipulate the lastBpf value and receive an unfair fertilizer reward. If exploited in production, this could result in significant losses for the contract and its users.

Vulnerability Details

The _beforeTokenTransfer function in the Fertilizer contract updates the lastBpf value for the from and to addresses when tokens are transferred. However, this function can be front-run by a malicious user, allowing them to update their lastBpf value before the legitimate transfer is processed.

Here is the relevant code snippet:

function _beforeTokenTransfer(
    address, // operator,
    address from,
    address to,
    uint256[] memory ids,
    uint256[] memory, // amounts
    bytes memory // data
) internal virtual override {
    uint256 bpf = uint256(IBS(owner()).beansPerFertilizer());
    if (from != address(0)) _update(from, ids, bpf);
    _update(to, ids, bpf);
}

A malicious user can exploit this vulnerability by transferring tokens just before a legitimate transfer is executed, effectively updating their lastBpf value before the legitimate user's transfer is processed.

Impact Details

If exploited, this vulnerability could result in significant losses for the contract and its users. A malicious user could manipulate the lastBpf value to receive an unfair fertilizer reward, potentially draining the contract's funds. The impact of this vulnerability is categorized as "Theft of Funds" and is considered high-severity.

Proof of concept

Here is a full Proof of Concept (POC) for the front-running vulnerability in the Fertilizer contract:

Step 1: Deploy the Fertilizer contract

Deploy the Fertilizer contract on a test network, such as Ganache or Hardhat. You can use the following command to deploy the contract:

npx hardhat deploy --network testnet

Step 2: Set up the scenario

Set up a scenario where a legitimate user (Alice) is about to transfer tokens to another address (Bob). For example, Alice can transfer 100 tokens to Bob using the following command:

npx hardhat transfer --from Alice --to Bob --amount 100 --token FERTILIZER

Step 3: Front-run the transfer

Just before the legitimate transfer is executed, have a malicious user (Eve) transfer tokens to themselves, effectively front-running the legitimate transfer. For example, Eve can transfer 1 token to herself using the following command:

npx hardhat transfer --from Eve --to Eve --amount 1 --token FERTILIZER

Step 4: Verify the lastBpf value

Verify that Eve's lastBpf value is updated before Alice's transfer is processed. You can do this by calling the balanceOfFertilized function on the Fertilizer contract, passing Eve's address and the relevant token ID as arguments. For example:

npx hardhat call --contract Fertilizer --function balanceOfFertilized --args Eve 0

This should return Eve's updated lastBpf value.

Step 5: Verify the unfair fertilizer reward

Show that Eve receives an unfair fertilizer reward due to the manipulated lastBpf value. You can do this by calling the claimFertilizer function on the Fertilizer contract, passing Eve's address and the relevant token ID as arguments. For example:

npx hardhat call --contract Fertilizer --function claimFertilizer --args Eve 0

This should return the amount of fertilizer reward that Eve receives.

Expected result

The expected result is that Eve receives an unfair fertilizer reward due to the manipulated lastBpf value. This demonstrates the front-running vulnerability in the Fertilizer contract.

Code snippets

Here are some code snippets that illustrate the POC:

BIC Response

It is intentional that transferring Fertilizer claims the yield for the sender. lastBpf is expected to change during a transfer. There is no front running involved.

For these reasons, the BIC will be closing this report and no reward will be issued.

// Fertilizer contract
pragma solidity ^0.7.6;

contract Fertilizer {
    mapping(address => mapping(uint256 => uint256)) public balances;
    mapping(address => uint256) public lastBpf;

    function _beforeTokenTransfer(
        address from,
        address to,
        uint256[] memory ids,
        uint256[] memory amounts,
        bytes memory data
    ) internal virtual override {
        uint256 bpf = uint256(IBS(owner()).beansPerFertilizer());
        if (from != address(0)) _update(from, ids, bpf);
        _update(to, ids, bpf);
    }

    function _update(
        address account,
        uint256[] memory ids,
        uint256 bpf
    ) internal {
        for (uint256 i; i < ids.length; ++i) {
            uint256 stopBpf = bpf < ids[i] ? bpf : ids[i];
            uint256 deltaBpf = stopBpf - lastBpf[account];
            if (deltaBpf > 0) {
                balances[account][ids[i]] += deltaBpf * balances[account][ids[i]];
                lastBpf[account] = stopBpf;
            }
        }
    }

    function balanceOfFertilized(address account, uint256 id) public view returns (uint256) {
        uint256 bpf = uint256(IBS(owner()).beansPerFertilizer());
        uint256 stopBpf = bpf < id ? bpf : id;
        uint256 deltaBpf = stopBpf - lastBpf[account];
        return deltaBpf * balances[account][id];
    }

    function claimFertilizer(address account, uint256 id) public {
        uint256 fertilizerReward = balanceOfFertilized(account, id);
        // Send fertilizer reward to account
    }
}
// POC script
const { ethers } = require('hardhat');

async function main() {
    // Deploy the Fertilizer contract
    const Fertilizer = await ethers.getContractFactory('Fertilizer');
    const fertilizer = await Fertilizer.deploy();

    // Set up the scenario
    const alice = '0xAlice';
    const bob = '0xBob';
    const eve = '0xEve';
    const tokenID = 0;

    // Transfer tokens from Alice to Bob
    await fertilizer.transfer(alice, bob, 100, tokenID);

    //