QuantAMM

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

Total Weight Invariant Precision Loss in MinimumVarianceUpdateRule

Summary

The MinimumVarianceUpdateRule contract experiences minor precision loss in maintaining the pool weight invariant (sum of weights = 1e18). The deviation stabilizes at -4 (4e-18 below target) after a single compounding, representing a very small but consistent mathematical imprecision in the total weight calculation.

Vulnerability Details

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

The issue occurs in the weight calculations where small rounding errors accumulate to a stable deviation:

// Scalar path:
for (uint i; i < _prevWeights.length; ) {
int256 res = mixingVariance.mul(int256(_prevWeights[i])) + newWeights[i].div(divisionFactor);
newWeightsConverted[i] = res;
unchecked {
++i;
}
}
// No normalization to ensure exact 1e18 total

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/MockMinimumVarianceRule.sol";
import "../../../contracts/mock/MockPool.sol";
import "../utils.t.sol";
contract QuantammMinimumVarianceTotalWeightTest is Test, QuantAMMTestUtils {
MockMinimumVarianceRule public rule;
MockPool public mockPool;
function setUp() public {
rule = new MockMinimumVarianceRule(address(this));
mockPool = new MockPool(3600, 1 ether, address(rule));
}
function testMinimumVarianceTotalWeightCompounding() public {
// Setup parameters
int256[][] memory parameters = new int256[][]();
parameters[0] = new int256[]();
parameters[0][0] = 0.9e18; // High lambda (0.9)
// Initial weights
int256[] memory prevWeights = new int256[]();
prevWeights[0] = 0.5e18; // 50%
prevWeights[1] = 0.5e18; // 50%
// Price movements
int256[] memory data = new int256[]();
data[0] = 1.1e18; // 10% increase
data[1] = 0.9e18; // 10% decrease
// Variances
int256[] memory variances = new int256[]();
variances[0] = 1e18; // Equal variance
variances[1] = 1e18;
// Moving averages
int256[] memory prevMovingAverages = new int256[]();
prevMovingAverages[0] = 1e18;
prevMovingAverages[1] = 1e18;
prevMovingAverages[2] = 1e18;
prevMovingAverages[3] = 1e18;
int128[] memory lambda = new int128[]();
lambda[0] = int128(0.95e18);
// Initialize pool
mockPool.setNumberOfAssets(2);
rule.initialisePoolRuleIntermediateValues(
address(mockPool),
prevMovingAverages,
variances,
2
);
// Track total weight deviation over multiple updates
int256[] memory results;
int256 initialDeviation;
int256 finalDeviation;
// First update
rule.CalculateUnguardedWeights(
prevWeights,
data,
address(mockPool),
parameters,
lambda,
prevMovingAverages
);
results = rule.GetResultWeights();
initialDeviation = (results[0] + results[1]) - 1e18;
emit log_named_int("Initial total weight", results[0] + results[1]);
emit log_named_int("Initial deviation", initialDeviation);
// Use previous results as new weights for next update
prevWeights = results;
// Perform multiple additional updates
for (uint i = 0; i < 5; i++) {
rule.CalculateUnguardedWeights(
prevWeights,
data,
address(mockPool),
parameters,
lambda,
prevMovingAverages
);
results = rule.GetResultWeights();
prevWeights = results;
emit log_named_uint("Update", i + 2);
emit log_named_int("Total weight", results[0] + results[1]);
emit log_named_int("Deviation", (results[0] + results[1]) - 1e18);
}
finalDeviation = (results[0] + results[1]) - 1e18;
emit log_named_int("Initial deviation", initialDeviation);
emit log_named_int("Final deviation", finalDeviation);
// Check if deviation got worse
assertTrue(
abs(finalDeviation) <= abs(initialDeviation) * 2,
"Deviation should not compound significantly"
);
// Verify weights still sum to approximately 1e18
assertApproxEqAbs(
results[0] + results[1],
1e18,
1e15, // Still allow small rounding errors up to 0.1%
"Total weight should remain ~100% even after multiple updates"
);
}
// Helper function to get absolute value
function abs(int256 x) internal pure returns (int256) {
return x >= 0 ? x : -x;
}
}

Test Results:

Initial total weight: 999999999999999998
Initial deviation: -2
Update: 2
Total weight: 999999999999999996
Deviation: -4
Update: 3
Total weight: 999999999999999996
Deviation: -4
Update: 4
Total weight: 999999999999999996
Deviation: -4
Update: 5
Total weight: 999999999999999996
Deviation: -4
Update: 6
Total weight: 999999999999999996
Deviation: -4
Initial deviation: -2
Final deviation: -4

Impact

  • Small but consistent deviation from exact 1e18 total weight

  • Deviation stabilizes at 4e-18 below target (0.0000000000000004%)

  • Initial compounding from -2 to -4, then stabilizes

  • Mathematical imprecision in weight calculations

  • Well within typical DeFi rounding error tolerances (1e15)

Recommendations

  1. Consider adding weight normalization for perfect precision:

int256 totalWeight = 0;
for (uint i = 0; i < newWeightsConverted.length; i++) {
totalWeight += newWeightsConverted[i];
}
// Normalize weights to ensure exact 1e18 total
for (uint i = 0; i < newWeightsConverted.length; i++) {
newWeightsConverted[i] = newWeightsConverted[i].mul(1e18) / totalWeight;
}
  1. Optional improvements:

    • Add normalization step in base update rule

    • Consider higher precision for intermediate calculations

    • Add weight sum validation in testing utilities

    • Document expected precision characteristics

References

Updates

Lead Judging Commences

n0kto Lead Judge 10 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!