QuantAMM

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

ExcessiveBlockMultiplier in `QuantAMMWeightedPool` Contract

Summary

The QuantAMMWeightedPool contract allows dynamic weight adjustments through a blockMultiplier mechanism. However, there is no upper bound validation for the blockMultiplier, enabling excessive values to be set. This leads to rapid inflation of token weights within the pool, causing significant imbalance and exposing liquidity providers (LPs) to arbitrage losses. The issue persists in the setWeights function, where no limit restricts the multiplier values.

Note: Do not confuse this vulnerability with the one previously reported by Cyfrin titled “Incorrect handling of negative multipliers.” While both findings relate to the handling of blockMultiplier and its effects on weight calculations, Cyfrin's report focuses on errors caused by negative values (leading to underflow), whereas this new vulnerability targets excessive multipliers (e.g., > 1) that destabilize the pool.

Vulnerability Details

The QuantAMMWeightedPool contract relies on a blockMultiplier to interpolate token weights over time. However, the implementation does not enforce an upper bound on the blockMultiplier. As a result, when a malicious or flawed external rule sets a multiplier to an excessively high value (e.g., 2e18 or beyond), the token’s weight inflates rapidly in just a few blocks or seconds. This leads to the following issues:

  1. Extreme Weight Imbalances: One token’s weight can quickly exceed 1e18 (100%), causing other tokens to lose nearly all of their share in the pool.

  2. Price Manipulation: Swaps become mispriced because the pool’s formula believes one token dominates the entire pool, allowing arbitrageurs to exploit the discrepancy.

  3. Significant LP Losses: Liquidity providers face amplified slippage and potential impermanent losses, as the pool’s mispriced ratio encourages large profit-taking swaps by external actors.

By allowing an unlimited blockMultiplier, the pool fails to prevent scenarios where a single token effectively “absorbs” the entire pool in a short timeframe, revealing a high-severity vulnerability that directly impacts pool stability and user funds.

Root Cause

The root cause is the lack of validation in the setWeights function for blockMultiplier values. The function accepts arrays of weights and multipliers but does not enforce an upper bound on the multiplier values, allowing arbitrary inflation.

QuantAMMWeightedPool.sol - setWeights

The vulnerability is in the setWeights function:

function setWeights(
int256[] calldata _weights,
address _poolAddress,
uint40 _lastInterpolationTimePossible
) external override {
// No upper bound check for multipliers
}

Impact

An excessive blockMultiplier immediately threatens the pool’s stability. When weights inflate beyond 1.0 for a single token, the pool’s pricing logic breaks, making that token appear to dominate the entire liquidity. This leads to:

  • Severe Arbitrage Opportunities: External actors see a token over-represented and swap in or out at highly favorable rates, causing sudden and large losses to the pool’s liquidity providers (LPs).

  • Rapid Liquidity Drain: Other tokens effectively lose their share of the pool, creating price imbalances that incentivize large‐scale swaps against the inflated token.

  • Unchecked Price Manipulation: The AMM formula relies on normalized weights. If a weight becomes greater than 1.0, the implied pricing model no longer reflects reality, leaving the pool vulnerable to exploit.

Because this happens without triggering any safe‐stop mechanism, the potential for immediate, irreversible loss is high, making it a critical issue for all users of the pool.

Proof of Concept

Test

  • Add the following test to pkg/pool-quantamm/test/foundry/QuantAMMWeightedPool2Token.t.sol:

function testExcessiveBlockMultiplier() public {
// Step 1: Create a new pool with initial weights (0.5, 0.5)
console2.log('Step 1: Deploying pool with 0.5 / 0.5 initial weights');
QuantAMMWeightedPoolFactory.NewPoolParams memory params = _createPoolParams();
(address quantAMMWeightedPool, ) = quantAMMWeightedPoolFactory.create(params);
// Step 2: Define newWeights with an excessively large blockMultiplier for the first token
console2.log('Step 2: Setting newWeights to [0.5, 0.5, 2.0, 0.0]');
int256[] memory newWeights = new int256[]();
newWeights[0] = 0.5e18; // base weight for token0
newWeights[1] = 0.5e18; // base weight for token1
newWeights[2] = 2e18; // excessive blockMultiplier for token0
newWeights[3] = 0; // no multiplier for token1
// Step 3: Call setWeights() with a 10-second interpolation limit
console2.log(
'Step 3: setWeights with interpolation limit = block.timestamp + 10s'
);
vm.prank(address(updateWeightRunner));
QuantAMMWeightedPool(quantAMMWeightedPool).setWeights(
newWeights,
quantAMMWeightedPool,
uint40(block.timestamp + 10)
);
// Step 4: Warp 5 seconds to let blockMultiplier inflate the weight
console2.log('Step 4: Warping 5 seconds forward');
vm.warp(block.timestamp + 5);
// Step 5: Retrieve current normalized weights
console2.log('Step 5: getNormalizedWeights()');
uint256[] memory weights = QuantAMMWeightedPool(quantAMMWeightedPool)
.getNormalizedWeights();
// Step 6: Sum the weights to see if it exceeds 1e18 (100%)
console2.log('Step 6: Summation of token0 and token1 weights');
uint256 sumWeights = weights[0] + weights[1];
console2.log('Weights: (%s, %s)', weights[0], weights[1]);
console2.log('Sum of weights after 5 seconds: %s', sumWeights);
// Step 7: Check if sum of weights is > 1e18
console2.log('Step 7: assert sumWeights > 1e18 to confirm the vulnerability');
assertGt(sumWeights, 1e18);
console2.log(
'Vulnerability confirmed: sum of weights > 1, no revert triggered.'
);
}

Test Result

Logs:
Step 1: Deploying pool with 0.5 / 0.5 initial weights
Step 2: Setting newWeights to [0.5, 0.5, 2.0, 0.0]
Step 3: setWeights with interpolation limit = block.timestamp + 10s
Step 4: Warping 5 seconds forward
Step 5: getNormalizedWeights()
Step 6: Summation of token0 and token1 weights
Weights: (10500000000000000000, 500000000000000000)
Sum of weights after 5 seconds: 11000000000000000000
Step 7: assert sumWeights > 1e18 to confirm the vulnerability
Vulnerability confirmed: sum of weights > 1, no revert triggered.

Explanation

  • The base weight starts at 0.5e18 for token0 and token1.

  • A blockMultiplier of 2e18 for token0 quickly inflates its effective weight.

  • After interpolating for just 5 seconds, the total combined weights exceed 1e18.

Fuzz Test

  • Add the following test to pkg/pool-quantamm/test/foundry/QuantAMMWeightedPool2Token.t.sol:

function testExcessiveBlockMultiplierFuzzed(uint256 randomMultiplier) public {
// Step 1: Bound randomMultiplier to >= 1e18 (1.0) and <= 2_147_483_647e9.
// This ensures we always get a "large enough" multiplier to exceed 1,
// while preventing overflow in the 32-bit packing.
randomMultiplier = bound(randomMultiplier, 1e18, 2_147_483_647e9);
console2.log('Fuzz Test: randomMultiplier =', randomMultiplier);
// Step 2: Deploy a pool with initial weights 0.5 / 0.5
console2.log('Step 2: Deploying pool with 0.5 / 0.5 initial weights');
QuantAMMWeightedPoolFactory.NewPoolParams memory params = _createPoolParams();
(address quantAMMWeightedPool, ) = quantAMMWeightedPoolFactory.create(params);
// Step 3: Define newWeights for token0 and token1:
// - base weight = 0.5 each
// - randomMultiplier >= 1e18 for token0
console2.log('Step 3: Setting newWeights with randomMultiplier for token0');
int256[] memory newWeights = new int256[]();
newWeights[0] = 0.5e18; // base weight token0
newWeights[1] = 0.5e18; // base weight token1
newWeights[2] = int256(randomMultiplier); // fuzzed blockMultiplier
newWeights[3] = 0; // no multiplier for token1
// Step 4: Call setWeights() with a 10-second interpolation limit
console2.log(
'Step 4: setWeights with interpolation limit = block.timestamp + 10s'
);
vm.prank(address(updateWeightRunner));
QuantAMMWeightedPool(quantAMMWeightedPool).setWeights(
newWeights,
quantAMMWeightedPool,
uint40(block.timestamp + 10)
);
// Step 5: Warp 5 seconds to allow the blockMultiplier to inflate token0's weight
console2.log('Step 5: Warping 5 seconds forward');
vm.warp(block.timestamp + 5);
// Step 6: Retrieve current normalized weights
console2.log('Step 6: getNormalizedWeights()');
uint256[] memory weights = QuantAMMWeightedPool(quantAMMWeightedPool)
.getNormalizedWeights();
// Step 7: Sum the weights to see if it exceeds 1e18
console2.log('Step 7: Summation of token0 and token1 weights');
uint256 sumWeights = weights[0] + weights[1];
console2.log('Weights: (%s, %s)', weights[0], weights[1]);
console2.log('Sum of weights after 5 seconds: %s', sumWeights);
// Step 8: Assert sumWeights > 1e18 to confirm the vulnerability
console2.log(
'Step 8: assert sumWeights > 1e18 to confirm potential vulnerability'
);
assertGt(sumWeights, 1e18);
console2.log(
'Fuzz Test Successful: sum of weights > 1, multiplier was %s',
randomMultiplier
);
}

Test Result

[PASS] testExcessiveBlockMultiplierFuzzed(uint256) (runs: 10003, μ: 12476389, ~: 12476410)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 20.78s (20.72s CPU time)

Additional Research

  • The fuzz test explores multiple random multipliers ≥ 1e18, consistently reproducing the vulnerability whenever blockMultiplier >= 1e18.

Tools Used

  • Foundry: Executes unit tests and fuzz tests to illustrate that the pool does not revert on excessive multipliers.

  • Manual Code Review: To identify and confirm the absence of multiplier validation in the setWeights function.

Recommendations

  1. Enforce an Upper Bound on Multiplier

    • In setWeights(...), add a check that reverts if any blockMultiplier ≥ 1e18 (or a suitably safe threshold). Example:

      uint256 half = _weights.length / 2;
      for (uint256 i = half; i < _weights.length; i++) {
      if (_weights[i] >= 1e18) {
      revert("MultiplierTooHigh");
      }
      }
  2. Optionally Limit Summations

    • If desired, ensure that at no interpolation point can a token’s weight exceed 1.0 individually, or that the sum never surpasses 1.0 across the whole pool.

  3. Audit External Weight Update Rules

    • Verify that off-chain or separate “weight update” logic does not produce large multipliers beyond the allowed threshold.

Validation Through Testing

  • After adding the recommended mitigation to the QuantAMMWeightedPool contract, include the following test in pkg/pool-quantamm/test/foundry/QuantAMMWeightedPool2Token.t.sol:

function testSetWeightsRevertIfMultiplierTooHigh() public {
// Step 1: Deploy a new pool with initial weights 0.5 / 0.5
console2.log('Step 1: Deploying pool with 0.5 / 0.5 initial weights');
QuantAMMWeightedPoolFactory.NewPoolParams memory params = _createPoolParams();
(address quantAMMWeightedPool, ) = quantAMMWeightedPoolFactory.create(params);
// Step 2: Define newWeights with blockMultiplier = 1e18 to force revert
console2.log(
'Step 2: Setting newWeights with blockMultiplier=1e18 (expect revert)'
);
int256[] memory newWeights = new int256[]();
newWeights[0] = 0.5e18; // base weight token0
newWeights[1] = 0.5e18; // base weight token1
newWeights[2] = 1e18; // blockMultiplier >= 1e18 -> should revert
newWeights[3] = 0; // no multiplier for token1
// Step 3: Expect revert with "MultiplierTooHigh"
console2.log(
"Step 3: Calling setWeights() -> vm.expectRevert('MultiplierTooHigh')"
);
vm.prank(address(updateWeightRunner));
vm.expectRevert(bytes('MultiplierTooHigh'));
QuantAMMWeightedPool(quantAMMWeightedPool).setWeights(
newWeights,
quantAMMWeightedPool,
uint40(block.timestamp + 10)
);
}

Test Result

Logs:
Step 1: Deploying pool with 0.5 / 0.5 initial weights
Step 2: Setting newWeights with blockMultiplier=1e18 (expect revert)
Step 3: Calling setWeights() -> vm.expectRevert('MultiplierTooHigh')
├─ [0] console::log("Step 2: Setting newWeights with blockMultiplier=1e18 (expect revert)") [staticcall]
│ └─ ← [Stop]
├─ [0] console::log("Step 3: Calling setWeights() -> vm.expectRevert('MultiplierTooHigh')") [staticcall]
│ └─ ← [Stop]
├─ [0] VM::prank(MockUpdateWeightRunner: [0xDe09E74d4888Bc4e65F589e8c13Bce9F71DdF4c7])
│ └─ ← [Return]
├─ [0] VM::expectRevert(MultiplierTooHigh)
│ └─ ← [Return]
├─ [1216] QuantAMMWeightedPool::setWeights([500000000000000000 [5e17], 500000000000000000 [5e17], 1000000000000000000 [1e18], 0], QuantAMMWeightedPool: [0xF4C24b22a553567a9936B7F1CAb8497386f3c046], 1682899210 [1.682e9])
│ └─ ← [Revert] revert: MultiplierTooHigh
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 105.92ms (3.65ms CPU time)

This mitigation ensures that excessively large multipliers are rejected, preventing pool imbalances and protecting LPs.

Updates

Lead Judging Commences

n0kto Lead Judge 7 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.