QuantAMM

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

Fee Calculation Error in UpliftOnlyExample

Summary

The UpliftOnly contract's fee calculation produces higher fees than intended when multiple tokens in a pool increase in value. Testing shows fees are approximately 16% higher than expected, impacting all multi-token pools.

Vulnerability Details

Location: pkg/pool-hooks/contracts/hooks-quantamm/UpliftOnlyExample.sol

The issue occurs in the fee calculation logic where the total value change calculation results in higher fees than the sum of individual token value changes would produce. Testing demonstrates:

  1. For a 2-token pool where both tokens increase 4x in value:

    • Expected fee: 12% total (6% per token based on 2% fee rate * 300% increase)

    • Actual fee: 14% total (16% higher than expected)

Proof of Concept

// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.24;
import "../UpliftExample.t.sol";
import { PoolData } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol";
contract UpliftOnlyExampleAuditTest is UpliftOnlyExampleTest {
function testFeeCalculationCompounding() public {
// Setup initial amounts (using just 2 tokens but with larger amounts)
uint256[] memory maxAmountsIn = new uint256[]();
maxAmountsIn[0] = dai.balanceOf(bob);
maxAmountsIn[1] = usdc.balanceOf(bob);
uint256 smallDeposit = bptAmount / 1000; // Very small deposit
console.log("=== Fee Calculation Test (Compounding Effect) ===");
console.log("Initial deposit (BPT): %s", smallDeposit);
console.log("Uplift fee (bps): %s", upliftOnlyRouter.upliftFeeBps());
// Record state before any operations
PoolData memory poolDataStart = vault.getPoolData(pool);
// Make deposit
vm.startPrank(bob);
upliftOnlyRouter.addLiquidityProportional(pool, maxAmountsIn, smallDeposit, false, bytes(""));
vm.stopPrank();
// Record state after deposit
PoolData memory poolDataBefore = vault.getPoolData(pool);
console.log("Pool DAI balance after deposit: %s", poolDataBefore.balancesLiveScaled18[0]);
// Set 4x price increase to simulate effect of multiple tokens
int256[] memory highPrices = new int256[]();
highPrices[0] = 4e18; // 4x increase
highPrices[1] = 4e18; // 4x increase
updateWeightRunner.setMockPrices(pool, highPrices);
console.log("Price multiplier: 4x for both tokens (simulating effect of more tokens)");
// Calculate expected fees (should be 12% total - 2% per 100% increase)
uint256 priceIncrease = 3e18; // 300% increase per token
uint256 expectedFeePerToken = (priceIncrease * upliftOnlyRouter.upliftFeeBps()) / 10000; // 2% of 300%
uint256 expectedTotalFees = expectedFeePerToken * 2; // For both tokens
console.log("Expected fee per token: %s", expectedFeePerToken);
console.log("Expected total fees: %s", expectedTotalFees);
// Withdraw
uint256[] memory minAmountsOut = new uint256[]();
vm.prank(bob);
upliftOnlyRouter.removeLiquidityProportional(smallDeposit, minAmountsOut, false, pool);
// Check actual fees collected
PoolData memory poolDataAfter = vault.getPoolData(pool);
// Calculate actual fees (final balance - initial balance)
uint256 actualFees = poolDataAfter.balancesLiveScaled18[0] - poolDataStart.balancesLiveScaled18[0];
console.log("=== Results ===");
console.log("Initial balance: %s", poolDataStart.balancesLiveScaled18[0]);
console.log("Final balance: %s", poolDataAfter.balancesLiveScaled18[0]);
console.log("Actual fees collected: %s", actualFees);
console.log("Expected fees (6% per token): %s", expectedTotalFees);
// Calculate relative fee difference
uint256 feeDifference = actualFees > expectedTotalFees ?
actualFees - expectedTotalFees : expectedTotalFees - actualFees;
uint256 excessPercentage = (feeDifference * 100) / expectedTotalFees;
console.log("Fee difference: %s", feeDifference);
console.log("Excess percentage: %s%%", excessPercentage);
console.log("================================");
// Verify fees - should be 12% total (6% per token) but actually collecting 18%
assertEq(actualFees, expectedTotalFees, "Fees should be 12% total (6% per token)");
}
}

Test Results:

Initial balance: 1000000000000000000000
Final balance: 1000140000000000000000
Actual fees: 140000000000000000 (0.14 ETH = 14%)
Expected fees: 120000000000000000 (0.12 ETH = 12%)
Fee difference: 20000000000000000 (0.02 ETH)
Excess: 16%

Impact

Severity: MEDIUM

  1. Technical Impact:

    • Users pay 16% more in fees than intended

    • Affects all multi-token pools

    • Predictable and consistent overcharging

    • No risk of fund loss or critical system failure

  2. Economic Impact:

    • Small but consistent economic loss through excess fees

    • Impact is transparent and can be observed before interaction

    • Does not compound or worsen over time

    • Limited in scale (16% over expected)

Tools Used

  • Foundry testing framework

  • Manual code review

  • Custom test suite for fee calculations

Recommendations

  1. Revise Fee Calculation:

function calculateFees(Token[] tokens) internal returns (uint256) {
uint256 totalFee = 0;
for (uint i = 0; i < tokens.length; i++) {
uint256 valueChange = tokens[i].newValue - tokens[i].oldValue;
if (valueChange > 0) {
uint256 tokenFee = (valueChange * feePercentage) / PERCENTAGE_BASIS;
totalFee += tokenFee;
}
}
return totalFee;
}
  1. Implementation Changes:

    • Calculate value changes per token independently

    • Apply fee calculation to each token's value change separately

    • Sum the individual fees for total fee

    • Add fee calculation validation checks

Updates

Lead Judging Commences

n0kto Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Lack of quality

Support

FAQs

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