QuantAMM

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

Negative Pool Weights Possible in DifferenceMomentumUpdateRule

Summary

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

Vulnerability Details

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

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

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

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/MockDifferenceMomentumRule.sol";
import "../../../contracts/mock/MockPool.sol";
import "../utils.t.sol";
contract QuantammDifferenceMomentumNegativeWeightsTest is Test, QuantAMMTestUtils {
MockDifferenceMomentumRule public rule;
MockPool public mockPool;
function setUp() public {
rule = new MockDifferenceMomentumRule(address(this));
mockPool = new MockPool(3600, 1 ether, address(rule));
}
function testDifferenceMomentumNegativeWeightsCompounding() public {
// Setup parameters with higher kappa
int256[][] memory parameters = new int256[][]();
parameters[0] = new int256[](); // Changed to length 1 for scalar kappa
parameters[0][0] = 10e18; // Higher kappa to amplify effect
parameters[1] = new int256[]();
parameters[1][0] = 0.5e18; // Valid lambda
// 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 prevShortMovingAverage = new int256[]();
prevShortMovingAverage[0] = 1e18;
prevShortMovingAverage[1] = 1e18;
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,
prevShortMovingAverage,
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: 12500000000000000000
Weight 1: -11500000000000000000
Total Weight: 1000000000000000000
Update: 2
Weight 0: 30500000000000000000
Weight 1: -29500000000000000000
Total Weight: 1000000000000000000
Update: 3
Weight 0: 51500000000000000000
Weight 1: -50500000000000000000
Total Weight: 1000000000000000000
Update: 4
Weight 0: 74000000000000000000
Weight 1: -73000000000000000000
Total Weight: 1000000000000000000
Update: 5
Weight 0: 97250000000000000000
Weight 1: -96250000000000000000
Total Weight: 1000000000000000000

Impact

Severity: HIGH

  1. Technical Impact:

    • Negative weights compound rapidly to extreme values (-9625%+ after just 5 updates)

    • Weights grow unbounded while maintaining total weight at 100%

    • Breaks fundamental AMM invariants about weight bounds

  2. Economic Impact:

    • Potential manipulation of pool pricing

    • Invalid pool states could lead to economic exploits

    • Risk of fund loss through arbitrage against negative weights

    • Extreme weight values could cause catastrophic price impacts

    • Compounding effect makes exploitation easier over time

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"); // Add same check as vector path
newWeightsConverted[locals.i] = res;
  1. Consider architectural improvements:

    • Extract weight validation to a shared function

    • Use unsigned integers for weights

    • Implement consistent validation across all weight calculation paths

    • Add maximum weight bounds to prevent extreme values

    • Add compounding protection mechanisms

  2. Add invariant checks:

    • Validate weights are non-negative before returning

    • Consider adding minimum weight thresholds

    • Add explicit weight range validation

    • Add maximum weight thresholds to prevent extreme values

    • Add checks for rate of weight change between 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!