QuantAMM

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

Negative Pool Weights Possible in MomentumUpdateRule

Summary

The MomentumUpdateRule 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/MomentumUpdateRule.sol

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

// Current implementation:
int256 res = int256(_prevWeights[locals.i]) +
locals.kappaStore[0].mul(locals.newWeights[locals.i] - locals.normalizationFactor);
newWeightsConverted[locals.i] = res;
// Missing: require(res >= 0, "Invalid weight");

Proof of Concept:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "forge-std/Test.sol";
import "@prb/math/contracts/PRBMathSD59x18.sol";
import "../../../contracts/mock/mockRules/MockMomentumRule.sol";
import "../../../contracts/mock/MockPool.sol";
import "../utils.t.sol";
contract QuantammMomentumNegativeWeightsTest is Test, QuantAMMTestUtils {
MockMomentumRule public rule;
MockPool public mockPool;
function setUp() public {
rule = new MockMomentumRule(address(this));
mockPool = new MockPool(3600, 1 ether, address(rule));
}
function testMomentumNegativeWeightsCompounding() public {
// Setup parameters with higher kappa
int256[][] memory parameters = new int256[][]();
parameters[0] = new int256[](); // Scalar kappa
parameters[0][0] = 10e18; // Higher kappa (10 instead of 2) to amplify effect
parameters[1] = new int256[]();
parameters[1][0] = 0; // Use raw price
// Initial weights
int256[] memory prevWeights = new int256[]();
prevWeights[0] = 0.5e18;
prevWeights[1] = 0.5e18;
// Create 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 multiple updates
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();
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", results[0] + results[1]);
// Use results as next prevWeights
prevWeights = results;
}
// Check for extreme negative weights while maintaining sum = 1e18
assertTrue(
(abs(results[0]) > 20e18 || abs(results[1]) > 20e18) &&
(results[0] + results[1] == 1e18),
"Should produce extreme negative weights while maintaining sum = 1e18"
);
}
// Helper function for absolute value
function abs(int256 x) internal pure returns (int256) {
return x >= 0 ? x : -x;
}
}

Test Results:

Update: 1
Weight 0: 3585714285714285680
Weight 1: -2585714285714285680
Total Weight: 1000000000000000000
Update: 2
Weight 0: 8831428571428571340
Weight 1: -7831428571428571330
Total Weight: 1000000000000000010
Update: 3
Weight 0: 15589142857142856980
Weight 1: -14589142857142856970
Total Weight: 1000000000000000010
Update: 4
Weight 0: 23405257142857142610
Weight 1: -22405257142857142590
Total Weight: 1000000000000000020
Update: 5
Weight 0: 31962251428571428230
Weight 1: -30962251428571428210
Total Weight: 1000000000000000020

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 AntiMomentumUpdateRule with similar compounding effects

Recommendations

  1. Add negative weight validation in scalar path:

int256 res = int256(_prevWeights[locals.i]) +
locals.kappaStore[0].mul(locals.newWeights[locals.i] - locals.normalizationFactor);
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: 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.

Give us feedback!