The getBatchIds function within the smart contract contains a vulnerability that can lead to an Out-of-Gas (OOG) error and Denial-of-Service (DoS). This vulnerability arises when a large number of withdrawal requests are accumulated for a single user, causing the _withdrawalIds array to grow excessively. The function’s nested loop structure, which iterates over _withdrawalIds and withdrawalBatches arrays, increases gas consumption linearly with the array size, potentially leading to gas limit exhaustion.
Function Involved: getBatchIds(uint256[] memory _withdrawalIds)
Issue: The _withdrawalIds array, passed as a parameter to getBatchIds, can become very large as a result of users making frequent or small withdrawals. The function contains a nested loop that iterates over both _withdrawalIds and the withdrawalBatches arrays, leading to an exponential increase in gas costs as the array size grows.
Scenario: Users can accumulate a large number of withdrawal requests, either intentionally or unintentionally, resulting in a long list of withdrawal IDs. When querying the corresponding batch IDs using getBatchIds, the large size of the _withdrawalIds array, combined with the nested loops, may cause the function to hit the gas limit, resulting in a transaction revert.
The user calls onTokenTransfer or withdraw which leads to _withdraw being called internally in the PriorityPool contract.
If there isn’t enough liquidity in the pool to satisfy the withdrawal request (i.e., toWithdraw > totalQueued), the contract will queue the remaining amount for withdrawal by calling withdrawalPool.queueWithdrawal.
queueWithdrawal Adds Withdrawal to the PoolWhen queueWithdrawal is called, it performs the following:
Checks that the withdrawal amount is above minWithdrawalAmount.
Transfers the amount from the user to the contract.
Pushes a new Withdrawal struct to the queuedWithdrawals array.
Updates the user's withdrawal list (queuedWithdrawalsByAccount) by appending the new withdrawalId.
Each withdrawal request by the user results in a new withdrawal ID being created and stored in the mappings queuedWithdrawalsByAccount and withdrawalOwners.
A single user can request many small withdrawals. For example:
If the contract allows the user to request multiple withdrawals and the user is not constrained by any time limits or minimum amount (other than minWithdrawalAmount), they can repeatedly call functions that trigger _withdraw and end up queuing many small withdrawals.
Each call to queueWithdrawal adds a new entry to the queuedWithdrawals array, and the user’s queuedWithdrawalsByAccount mapping grows in size.
Over time, this results in the user having many withdrawal IDs.
Eventually, the user wants to retrieve information about the batches corresponding to their many withdrawal requests or calls getFinalizedWithdrawalIdsByOwner function, which calls getBatchIds function.
The user calls getBatchIds, passing in the array of _withdrawalIds that was generated when they queued the withdrawals. This array can contain hundreds or even thousands of IDs, depending on how many withdrawal requests the user made.
Here, the function does the following:
Iterates over the entire _withdrawalIds array (loop over i).
For each withdrawal ID, it then iterates over the entire withdrawalBatches array (loop over j), checking whether the withdrawalId matches the batch's indexOfLastWithdrawal.
Since both loops can grow large, the gas consumption increases exponentially as the number of _withdrawalIds increases.
getBatchIdsIf the user has accumulated a large number of withdrawal IDs (say, 500–1000), and the withdrawalBatches array is also large, the nested loops will quickly cause the function to consume more gas than is allowed by the block gas limit.
As a result, the transaction will revert due to Out-of-Gas (OOG), causing the getBatchIds function to fail.
Denial of Service for Large Withdrawal Requests:
Any user (or attacker) with a large number of queued withdrawals can trigger this gas exhaustion issue.
Once the number of _withdrawalIds becomes too large to process within a single transaction (due to the nested loops), it becomes impossible for the user to retrieve their batch IDs using getBatchIds.
The user’s withdrawal information becomes inaccessible via this function, effectively causing a Denial-of-Service for that user.
This DoS condition could also impact the whole contract if batch queries are essential for other parts of the contract's operation (e.g., processing withdrawals in batches).
If you run the testLargeInput function from below, you will see that it will revert due to OOG:
Denial-of-Service (DoS): A user with a large number of withdrawal requests could unintentionally cause their query to fail due to an OOG error. This could prevent them from retrieving their batch IDs. Additionally, a malicious user could exploit this by intentionally submitting many small withdrawals to overload the system, potentially preventing other users from querying their batch IDs.
Gas Limit Issues: The vulnerability primarily affects contract performance by leading to excessively high gas costs, especially in situations where multiple users are queuing and querying withdrawals.
Manual Review
Batch Processing of Withdrawal Queries: Limit the number of withdrawal IDs that can be passed to getBatchIds in a single transaction. For instance, restrict users to querying only a maximum of 100 withdrawal IDs at a time, allowing the rest to be processed in subsequent calls.
Pagination Implementation: Introduce pagination to allow users to query their withdrawal IDs and batch IDs over several smaller transactions instead of handling them all at once. This would reduce gas costs per transaction.
Direct Mapping Optimization: Consider storing a direct mapping between each withdrawalId and its corresponding batchId. This would remove the need for iterating through the withdrawalBatches array and reduce the gas consumption by eliminating the nested loop.
Frequency or Volume Limits: Implement a limit on how frequently or how many withdrawal requests a single user can queue within a specific timeframe. This could prevent malicious users from spamming the system with small withdrawal requests.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.