QuantAMM

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

Negative Pool Weights Possible in AntiMomentumUpdateRule

Summary

The AntiMomentumUpdateRule contract allows negative weights to be created in pools when using the scalar path. More critically, these negative weights can compound to extreme values (-3000%+) over multiple updates while maintaining total weight at 100%, severely breaking fundamental AMM invariants.

Vulnerability Details

Location: pkg/pool-quantamm/contracts/rules/AntiMomentumUpdateRule.sol

The issue occurs in the scalar path weight calculation where there's no validation against negative weights:

// Vector path has validation:
require(res >= 0, "Invalid weight");
// Scalar path lacks validation:
int256 res = int256(_prevWeights[locals.i]) +
int256(locals.kappa[0]).mul(locals.normalizationFactor - locals.newWeights[locals.i]);
newWeightsConverted[locals.i] = res;

Proof of Concept showing compounding negative weights:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "forge-std/Test.sol";
import "@prb/math/contracts/PRBMathSD59x18.sol";
import "../../../contracts/mock/mockRules/MockAntiMomentumRule.sol";
import "../../../contracts/mock/MockPool.sol";
import "../utils.t.sol";
contract QuantammAntimomentumNegativeWeightInvariantTest is Test, QuantAMMTestUtils {
MockAntiMomentumRule public rule;
MockPool public mockPool;
function setUp() public {
rule = new MockAntiMomentumRule(address(this));
mockPool = new MockPool(3600, 1 ether, address(rule));
}
function testAntimomentumNegativeWeightDeviationCompounding() public {
// Setup parameters
int256[][] memory parameters = new int256[][]();
parameters[0] = new int256[](); // Scalar kappa
parameters[0][0] = 10e18; // Higher kappa (10 instead of 2)
parameters[1] = new int256[]();
parameters[1][0] = 1e18; // Use raw price
// Initial weights
int256[] memory prevWeights = new int256[]();
prevWeights[0] = 0.5e18;
prevWeights[1] = 0.5e18;
// More extreme price movements
int256[] memory data = new int256[]();
data[0] = 5e18; // Price 5x
data[1] = 0.2e18; // Price dropped 80%
// Moving averages setup
int256[] memory prevMovingAverages = new int256[]();
prevMovingAverages[0] = 1e18;
prevMovingAverages[1] = 1e18;
int256[] memory movingAverages = new int256[]();
movingAverages[0] = 1e18;
movingAverages[1] = 1e18;
int128[] memory lambdas = new int128[]();
lambdas[0] = int128(0.7e18);
// Initialize pool
mockPool.setNumberOfAssets(2);
rule.initialisePoolRuleIntermediateValues(
address(mockPool),
prevMovingAverages,
new int256[](2),
2
);
// Track total weight deviation over multiple updates
int256 maxDeviation = 0;
int256[] memory results;
// Perform multiple updates
for(uint i = 0; i < 5; i++) {
rule.CalculateUnguardedWeights(
prevWeights,
data,
address(mockPool),
parameters,
lambdas,
movingAverages
);
results = rule.GetResultWeights();
// Calculate deviation
int256 totalWeight = results[0] + results[1];
int256 deviation = totalWeight - 1e18;
emit log_named_uint("Update", i + 1);
emit log_named_int("Weight 0", results[0]);
emit log_named_int("Weight 1", results[1]);
emit log_named_int("Total Weight", totalWeight);
emit log_named_int("Deviation", deviation);
// Track maximum deviation
if (abs(deviation) > abs(maxDeviation)) {
maxDeviation = deviation;
}
// Use results as next prevWeights
prevWeights = results;
}
emit log_named_int("Maximum deviation observed", maxDeviation);
// Check if deviation grows
assertTrue(
abs(maxDeviation) > 2,
"Deviation should be larger than original case"
);
}
// Helper function for absolute value
function abs(int256 x) internal pure returns (int256) {
return x >= 0 ? x : -x;
}
}

Test Results:

Update: 1
Weight 0: -2585714285714285680
Weight 1: 3585714285714285680
Total Weight: 1000000000000000000
Deviation: 0
Update: 2
Weight 0: -7831428571428571330
Weight 1: 8831428571428571330
Total Weight: 1000000000000000000
Deviation: 0
Update: 3
Weight 0: -14589142857142856980
Weight 1: 15589142857142856980
Total Weight: 1000000000000000000
Deviation: 0
Update: 4
Weight 0: -22405257142857142600
Weight 1: 23405257142857142600
Total Weight: 1000000000000000000
Deviation: 0
Update: 5
Weight 0: -30962251428571428220
Weight 1: 31962251428571428220
Total Weight: 1000000000000000000
Deviation: 0
Maximum deviation observed: 0

Impact

  • Negative weights can compound to extreme values (-3000%+)

  • Weights can grow unbounded while maintaining total weight at 100%

  • Breaks fundamental AMM invariants about weight bounds

  • Could lead to invalid pool states and economic exploits

  • Inconsistent with vector path that prevents negative weights

  • Could cause catastrophic issues with price calculations and swaps

  • Same vulnerability pattern as MomentumUpdateRule but with more severe compounding effects

Recommendations

  1. Add negative weight validation in scalar path:

int256 res = int256(_prevWeights[locals.i]) +
int256(locals.kappa[0]).mul(locals.normalizationFactor - locals.newWeights[locals.i]);
require(res >= 0, "Invalid weight");
newWeightsConverted[locals.i] = res;
  1. Consider architectural improvements:

    • Add weight validation in base class

    • Implement weight bounds checking as a shared function

    • Add explicit invariant checks for all weight updates

    • Ensure consistent validation between scalar and vector paths

    • Add maximum weight bounds to prevent extreme values

  2. Add tests:

    • Test weight bounds across all update rules

    • Add fuzz testing for weight calculations

    • Test edge cases with extreme price movements

    • Test for compounding effects over multiple updates

Updates

Lead Judging Commences

n0kto Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
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.

Give us feedback!