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

Unbounded Loop Leading to Denial of Service (DoS) Due to Gas Limit Exhaustion

Summary

The contract includes batch processing functions that iterate over unbounded arrays. If an excessively large array is passed as input, the transaction could run out of gas, causing it to fail. This creates a Denial of Service (DoS) vulnerability, where an attacker or even a legitimate user could unintentionally render certain functions unusable.

Vulnerability Details

Affected Functions:

  • batchAddLiquidity(AddLiquidityArgs[] calldata _addLiquidityArgs)

  • batchRemoveLiquidity(RemoveLiquidityArgs[] calldata _removeLiquidityArgs)

  • batchRedeemWToken(RedeemWTokenArgs[] calldata _redeemWTokenArgs)

Vulnerable Code Snippet:

function batchAddLiquidity(AddLiquidityArgs[] calldata _addLiquidityArgs) external override nonReentrant {
uint256 _length = _addLiquidityArgs.length;
for (uint256 i = 0; i < _length; i++) {
_addLiquidity(
_addLiquidityArgs[i].poolId,
_addLiquidityArgs[i].collateralAmount,
_addLiquidityArgs[i].longRecipient,
_addLiquidityArgs[i].shortRecipient
);
}
}

Root Cause

The function processes a user-supplied array of arbitrary length in a single transaction without imposing any gas limit checks or execution constraints. If the array is too large, it can exceed the block gas limit, leading to a transaction failure.
https://github.com/Cyfrin/2025-01-diva/blob/main/contracts/src/AaveDIVAWrapper.sol#L126

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

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

Impact

  • Denial of Service (DoS): Any user calling the function with a large enough array will cause the function to fail. This could make it impossible to batch process liquidity, effectively freezing assets.

  • Increased Gas Fees: The absence of an upper bound on loop iterations could lead to inefficient gas usage, making transactions unnecessarily expensive.

  • Chain-Wide Impact: In extreme cases, execution of large arrays could congest the network by consuming an excessive amount of block gas.


Tools Used

  • Hardhat: For testing and simulating the vulnerability.

  • Slither: For static analysis to detect unbounded loops.

  • Solidity Visual Auditor: For manual contract inspection.


Proof of Concept (PoC) – Hardhat Test

The following test simulates calling the batchAddLiquidity function with a large input array to demonstrate the DoS attack via gas exhaustion.

Steps in the PoC Test:

  1. Deploy the contract.

  2. Call batchAddLiquidity with an excessively large array.

  3. Verify that the transaction fails due to gas exhaustion.

Hardhat Test Code:

const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Batch Function Inefficiency - Gas Exhaustion", function () {
let AaveDIVAWrapper, aaveDivaWrapper, owner, addr1, addr2;
beforeEach(async function () {
[owner, addr1, addr2] = await ethers.getSigners();
AaveDIVAWrapper = await ethers.getContractFactory("AaveDIVAWrapper");
aaveDivaWrapper = await AaveDIVAWrapper.deploy(
ethers.constants.AddressZero,
ethers.constants.AddressZero,
owner.address
);
await aaveDivaWrapper.deployed();
});
it("Should fail when calling batchAddLiquidity with a large array", async function () {
let largeArray = [];
for (let i = 0; i < 10000; i++) { // Simulating an excessively large input
largeArray.push({
poolId: ethers.utils.formatBytes32String("test"),
collateralAmount: ethers.utils.parseEther("1"),
longRecipient: addr1.address,
shortRecipient: addr2.address
});
}
await expect(aaveDivaWrapper.batchAddLiquidity(largeArray)).to.be.reverted;
});
});

Expected Outcome:

  • failed due to gas exhaustion, proving the vulnerability.


Mitigation

To prevent this issue, implement one or both of the following solutions:

  1. Limit the Batch Size

    • Introduce a maximum number of items that can be processed in a single batch.

    • Example:

    uint256 public constant MAX_BATCH_SIZE = 100;
    function batchAddLiquidity(AddLiquidityArgs[] calldata _addLiquidityArgs) external override nonReentrant {
    require(_addLiquidityArgs.length <= MAX_BATCH_SIZE, "Batch too large");
    for (uint256 i = 0; i < _addLiquidityArgs.length; i++) {
    _addLiquidity(
    _addLiquidityArgs[i].poolId,
    _addLiquidityArgs[i].collateralAmount,
    _addLiquidityArgs[i].longRecipient,
    _addLiquidityArgs[i].shortRecipient
    );
    }
    }
  2. Implement Partial Execution with State Tracking

    • Allow users to execute the function in multiple smaller batches and keep track of processed entries.

    • Example:

    mapping(address => uint256) public lastProcessedIndex;
    function batchAddLiquidityPaginated(
    AddLiquidityArgs[] calldata _addLiquidityArgs,
    uint256 startIndex,
    uint256 batchSize
    ) external override nonReentrant {
    uint256 endIndex = startIndex + batchSize;
    require(endIndex <= _addLiquidityArgs.length, "Out of range");
    for (uint256 i = startIndex; i < endIndex; i++) {
    _addLiquidity(
    _addLiquidityArgs[i].poolId,
    _addLiquidityArgs[i].collateralAmount,
    _addLiquidityArgs[i].longRecipient,
    _addLiquidityArgs[i].shortRecipient
    );
    }
    lastProcessedIndex[msg.sender] = endIndex;
    }
    • This allows processing in chunks rather than all at once.

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.