QuantAMM

QuantAMM
49,600 OP
View results
Submission Details
Severity: low
Invalid

High Gas Costs in `UpliftOnlyExample` Contract

Summary

A potential vulnerability has been identified in the original UpliftOnlyExample contract, related to high gas consumption and the possibility of a partial DoS when multiple deposits and NFT transfers are performed. Specifically, the logic of storing deposits in an array and manually reordering it during each transfer can trigger costly iterations, increasing the likelihood of gas-related reverts (especially in congested scenarios or with high block.gaslimit).

Vulnerability Details

  1. Location and Behavior

  • The vulnerability primarily appears in the onAfterRemoveLiquidity and afterUpdate functions, where the contract manually reorders a user’s FeeData[] array.

  • onAfterRemoveLiquidity iterates backward over the array to update and remove deposits, applying fees and computing amounts. This reverse loop can trigger substantial gas usage if many deposits exist.

  • UpliftOnlyExample.sol - onAfterRemoveLiquidity

  • afterUpdate (NFT transfers) searches for the tokenID in the array, then shifts elements in storage to maintain FILO order, causing multiple writes when the array holds numerous entries.

  • More generally, each deposit is “pushed” to the array on deposit, while withdrawals and transfers require multiple storage touches (deletion, rewriting, copying) that become expensive as the array grows (up to 100 entries per user).

  • UpliftOnlyExample.sol - afterUpdate

  1. Potential Partial DoS

    • In a high gas cost environment or with a low block.gaslimit, these loops might exceed the available gas, causing the transaction to revert.

    • This situation blocks withdrawals or transfers for users holding many deposits, resulting in a partial DoS: user funds remain secure, but certain operations are effectively inaccessible during periods of elevated gas usage.

Impact

The vulnerability impacts the usability and efficiency of the contract:

  1. Partial Denial of Service:

    • Users with a large number of deposits (close to 100) may experience failed transfers or withdrawals during periods of high gas prices or network congestion.

  2. Excessive Gas Costs:

    • Regular operations (e.g., NFT transfers) become prohibitively expensive, discouraging participation and reducing the system's scalability.

  3. Risk of DoS Exploitation:

    • A malicious actor could intentionally bloat arrays to degrade system performance, affecting other users indirectly.

Proof of Concept

Test

  • Add the following test to pkg/pool-hooks/test/foundry/UpliftExample.t.sol:

function testExcessiveGasWhenTransferringAfter100Deposits() public {
console2.log('TEST START: testExcessiveGasWhenTransferringAfter100Deposits');
// Step 1: Prepare Bob's 100 deposits
uint256[] memory maxAmountsIn = [dai.balanceOf(bob), usdc.balanceOf(bob)]
.toMemoryArray();
uint256 smallBptDeposit = bptAmount / 200; // A small fraction to avoid draining liquidity
vm.startPrank(bob);
console2.log('--- Creating 100 deposits for Bob. This may be gas-intensive');
uint256 gasStartDeposits = gasleft(); // Record gas before deposits loop
for (uint256 i = 0; i < 100; i++) {
upliftOnlyRouter.addLiquidityProportional(
pool,
maxAmountsIn,
smallBptDeposit,
false,
bytes('')
);
skip(1 days); // Simulate waiting time (optional)
}
uint256 gasUsedDeposits = gasStartDeposits - gasleft(); // Calculate gas used in deposits
console2.log('Gas used for 100 deposits:', gasUsedDeposits);
vm.stopPrank();
// Step 2: Check Bob has exactly 100 deposits
UpliftOnlyExample.FeeData[] memory bobDeposits = upliftOnlyRouter
.getUserPoolFeeData(pool, bob);
assertEq(bobDeposits.length, 100, 'Bob should have exactly 100 deposits');
// Step 3: Transfer the last NFT (tokenID = 100) to Alice, triggering array reorder
LPNFT lpNft = upliftOnlyRouter.lpNFT();
vm.startPrank(bob);
console2.log(
'--- Transferring NFT #100 from Bob to Alice (forces array reorder)'
);
uint256 gasStartTransfer = gasleft(); // Record gas before transfer
lpNft.transferFrom(bob, alice, 100);
uint256 gasUsedTransfer = gasStartTransfer - gasleft(); // Calculate gas used in transfer
console2.log('Gas used for NFT transfer & array reorder:', gasUsedTransfer);
vm.stopPrank();
// Step 4: Verify Alice and Bob's final deposit arrays
UpliftOnlyExample.FeeData[] memory aliceDeposits = upliftOnlyRouter
.getUserPoolFeeData(pool, alice);
UpliftOnlyExample.FeeData[] memory bobDepositsAfter = upliftOnlyRouter
.getUserPoolFeeData(pool, bob);
// Alice should have 1 deposit with tokenID = 100
assertEq(
aliceDeposits.length,
1,
'Alice should have received exactly 1 deposit after transfer'
);
assertEq(
aliceDeposits[0].tokenID,
100,
"The tokenID in Alice's array should be 100"
);
// Bob should have 99 deposits left
assertEq(
bobDepositsAfter.length,
99,
'Bob should have exactly 99 deposits after the transfer'
);
// Check that the last deposit in Bob's array is now tokenID = 99
uint256 lastIndexBob = bobDepositsAfter.length - 1;
assertEq(
bobDepositsAfter[lastIndexBob].tokenID,
99,
"The last deposit in Bob's array should now be tokenID = 99"
);
console2.log('TEST END: testExcessiveGasWhenTransferringAfter100Deposits');
}

Test Result

[PASS] testExcessiveGasWhenTransferringAfter100Deposits() (gas: 26809115)
Logs:
TEST START: testExcessiveGasWhenTransferringAfter100Deposits
--- Creating 100 deposits for Bob. This may be gas-intensive
Gas used for 100 deposits: 26331593
--- Transferring NFT #100 from Bob to Alice (forces array reorder)
Gas used for NFT transfer & array reorder: 210236
TEST END: testExcessiveGasWhenTransferringAfter100Deposits
  1. Test Explanation

    • We run testExcessiveGasWhenTransferringAfter100Deposits() (shown below) to create 100 deposits for Bob, then transfer the NFT #100 to Alice. The test logs reveal significant gas usage for both:

      1. Creating 100 deposits.

      2. Transferring the NFT and triggering array reordering.

  2. Logs

    • The logs show Gas used for 100 deposits: 26331593 and Gas used for NFT transfer & array reorder: 210236.

    • This high gas usage affirms the vulnerability, as the array reordering calls multiple storage writes that can revert transactions under congested conditions.

Tools Used

  • Foundry: Used to develop and run tests to validate the vulnerability.

  • Manual Code Review: Confirms the array reordering logic exists in the contract and triggers repeated storage writes.

Recommendations

  1. Use a Mapping Instead of Arrays

    • Map deposit IDs to structs (e.g., mapping(uint256 => FeeData)) and store only references to relevant data. This design avoids shifting array elements on transfer or removal.

  2. Lower the Maximum Deposit Count

    • Reduce the hard limit from 100 to a smaller number (e.g., 30) to limit the iteration and partially mitigate gas usage if array logic is retained.

  3. Aggregate Deposits

    • Consolidate multiple deposits into a single record when feasible. This drastically reduces the overall deposit count per user.

  4. Off-Chain Indexing

    • Outsource complex ordering or deposit history to an off-chain index (like The Graph), thereby removing heavy on-chain loops.

  5. User Warnings

    • Document or notify users that high deposit counts may yield significant gas costs and potential transaction failures in congested conditions.

Updates

Lead Judging Commences

n0kto Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

Informational or Gas / Admin is trusted / Pool creation is trusted / User mistake / Suppositions

Please read the CodeHawks documentation to know which submissions are valid. If you disagree, provide a coded PoC and explain the real likelyhood and the detailed impact on the mainnet without any supposition (if, it could, etc) to prove your point.

Support

FAQs

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

Give us feedback!