QuantAMM

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

Total Weight Invariant Broken in MomentumUpdateRule

Summary

The MomentumUpdateRule contract fails to maintain the fundamental invariant that pool weights must sum exactly to 1e18 (100%). This can lead to invalid pool states where the total weight exceeds 100%, potentially causing incorrect price calculations and pool imbalances. While the deviation is small (2 out of 1e18) and doesn't appear to compound, it could still lead to minor calculation inaccuracies in the pool.

Vulnerability Details

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

The issue occurs in the weight calculation logic where there's no validation that the sum of weights equals 1e18:

// Current implementation:
int256 res = int256(_prevWeights[locals.i]) +
locals.kappaStore[0].mul(locals.newWeights[locals.i] - locals.normalizationFactor);
newWeightsConverted[locals.i] = res;
// No validation of total weight sum

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 testMomentumBreaksTotalWeightInvariant() public {
// Setup parameters
int256[][] memory parameters = new int256[][]();
parameters[0] = new int256[](); // Scalar kappa
parameters[0][0] = 2e18; // Large kappa value to amplify effect
parameters[1] = new int256[]();
parameters[1][0] = 0; // Use raw price
// Create price movement that will trigger negative weights
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);
int256[] memory prevWeights = new int256[]();
prevWeights[0] = 0.5e18;
prevWeights[1] = 0.5e18;
// Create large price difference to trigger negative weight
int256[] memory data = new int256[]();
data[0] = 2e18; // Price doubled
data[1] = 0.5e18; // Price halved
// Initialize pool
mockPool.setNumberOfAssets(2);
rule.initialisePoolRuleIntermediateValues(
address(mockPool),
prevMovingAverages,
new int256[](2), // Not used in momentum
2
);
// Calculate weights
rule.CalculateUnguardedWeights(
prevWeights,
data,
address(mockPool),
parameters,
lambdas,
movingAverages
);
int256[] memory results = rule.GetResultWeights();
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]);
// Check for negative weights while maintaining sum = 1e18
assertTrue(
(results[0] < 0 || results[1] < 0) &&
(results[0] + results[1] == 1e18),
"Should produce negative weights while maintaining sum = 1e18"
);
// Log which weight is negative for clarity
if (results[0] < 0) {
emit log_string("Weight 0 is negative");
}
if (results[1] < 0) {
emit log_string("Weight 1 is negative");
}
}
}

Test Results:

Weight 0: 692857142857142856
Weight 1: 307142857142857146
Total weight: 1000000000000000002

Impact

  • Total pool weight exceeds 100%

  • Incorrect price calculations due to invalid weight normalization

  • Potential economic exploits through weight manipulation

  • Violation of core AMM invariants

  • Even small deviations from 1e18 can compound over multiple updates

  • Price calculations assume exact 100% total weight

Recommendations

  1. Add total weight validation:

int256 totalWeight;
for (uint i = 0; i < newWeightsConverted.length; i++) {
totalWeight += newWeightsConverted[i];
}
require(totalWeight == 1e18, "Invalid total weight");
  1. Consider architectural improvements:

    • Add weight normalization step to ensure sum equals 1e18

    • Implement weight validation as a shared function

    • Add explicit invariant checks in base classes

  2. Add tests:

    • Test total weight invariant across all update rules

    • Add fuzz testing for weight calculations

    • Test edge cases with extreme price movements

References

Updates

Lead Judging Commences

n0kto Lead Judge 11 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

invalid_sum_of_weights_can_exceeds_one_no_guard

According the sponsor and my understanding, sum of weights does not have to be exactly 1 to work fine. So no real impact here. Please provide a PoC showing a realistic impact if you disagree. This PoC cannot contains negative weights because they will be guarded per clampWeights.

Support

FAQs

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

Give us feedback!