QuantAMM

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

Negative Pool Weights Possible in QuantAMMMathGuard

Summary

The QuantAMMMathGuard contract's weight normalization process can result in negative weights under certain conditions, violating a core invariant that pool weights must always be positive and above the minimum weight threshold.

Vulnerability Details

Location: pkg/pool-quantamm/contracts/rules/base/QuantAMMMathGuard.sol

The issue occurs during weight normalization when:

  1. Initial weights are highly uneven (e.g., 80%/19%/1%)

  2. Target weights request extreme changes

  3. The normalization process attempts to maintain the sum-to-one invariant

This can result in negative weights while still maintaining the total weight at 100%, as demonstrated by the following test:

Proof of Concept:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.26;
import "forge-std/Test.sol";
import "@prb/math/contracts/PRBMathSD59x18.sol";
import { MockCalculationRule } from "../../../contracts/mock/MockCalculationRule.sol";
import { MockPool } from "../../../contracts/mock/MockPool.sol";
import { MockQuantAMMMathGuard } from "../../../contracts/mock/MockQuantAMMMathGuard.sol";
contract QuantAMMMathGuardWeightLockTest is Test {
using PRBMathSD59x18 for int256;
MockQuantAMMMathGuard mockQuantAMMMathGuard;
function setUp() public {
mockQuantAMMMathGuard = new MockQuantAMMMathGuard();
}
function testFuzz_WeightLockScenario(
int256 initialLargeWeight,
int256 initialMediumWeight,
int256 epsilonMax
) public {
// Bound the initial weights to create an uneven but valid distribution
initialLargeWeight = bound(initialLargeWeight, 0.7e18, 0.85e18);
initialMediumWeight = bound(initialMediumWeight, 0.14e18, 0.29e18);
epsilonMax = bound(epsilonMax, 0.01e18, 0.2e18); // 1% to 20% max change
int256[] memory prevWeights = new int256[]();
prevWeights[0] = initialLargeWeight;
prevWeights[1] = initialMediumWeight;
prevWeights[2] = 1e18 - initialLargeWeight - initialMediumWeight;
// Create extreme weight changes
int256[] memory newWeights = new int256[]();
newWeights[0] = 0.01e18;
newWeights[1] = 0.01e18;
newWeights[2] = 0.98e18;
int256 absoluteWeightGuardRail = 0.01e18;
int256[] memory guardedWeights = mockQuantAMMMathGuard.mockGuardQuantAMMWeights(
newWeights,
prevWeights,
epsilonMax,
absoluteWeightGuardRail
);
// Verify the weights change is limited by epsilonMax
for(uint i = 0; i < guardedWeights.length; i++) {
int256 change = (guardedWeights[i] - prevWeights[i]).abs();
assertLe(change, epsilonMax, "Change exceeded epsilonMax");
assertGe(guardedWeights[i], absoluteWeightGuardRail, "Weight below minimum");
}
// Sum should still be 1e18
int256 totalWeight = guardedWeights[0] + guardedWeights[1] + guardedWeights[2];
assertEq(totalWeight, 1e18);
}
}

The test reveals that under specific conditions:

  • Initial weights: [0.828e18, 0.268e18, -0.096e18]

  • Target weights: [0.01e18, 0.01e18, 0.98e18]

  • Results in weight: -7.907e15 (negative)

  • While maintaining sum = 1e18

Impact

Severity: HIGH

  1. Technical Impact:

    • Negative weights break core pool mathematics

    • Invalidates pricing calculations

    • Violates fundamental pool invariants

  2. Economic Impact:

    • Potential economic exploits through arbitrage

    • Invalid pool state could lead to loss of funds

    • System-wide failures when interacting with affected pools

Tools Used

  • Foundry fuzzing tests

  • Manual code review

  • Mathematical analysis of weight normalization

Recommendations

  1. Add explicit non-negative validation:

function _normalizeWeights(int256[] memory weights) internal pure {
// ... existing normalization code ...
// Add explicit non-negative check
for (uint i = 0; i < weights.length; i++) {
require(weights[i] >= 0, "Weight cannot be negative");
}
}
  1. Implement safe normalization:

    • Add checks before weight adjustments

    • Ensure intermediate calculations cannot result in negative values

    • Consider using a different normalization algorithm that preserves non-negativity

  2. Consider architectural changes:

    • Use unsigned integers (uint256) for weights

    • Implement a two-step normalization process that first ensures non-negativity

    • Add invariant checks at key points in the weight update process

Updates

Lead Judging Commences

n0kto Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

invalid_weights_can_be_negative_or_extreme_values

_clampWeights will check that these weights are positive and in the boundaries before writing them in storage.

Support

FAQs

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