QuantAMM

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

Compounding Total Weight Deviation in ChannelFollowingUpdateRule

Summary

The ChannelFollowingUpdateRule contract exhibits a compounding deviation from the fundamental invariant that pool weights must sum exactly to 1e18 (100%). Under extreme conditions, the deviation grows linearly at +10 wei per update, leading to significant cumulative errors over time. Even under normal conditions, the rule shows consistent negative deviations. This compounding behavior makes this a more severe issue than similar weight deviations in other rules.

Vulnerability Details

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

The issue occurs in the weight calculation logic where there's no validation that the sum of weights equals 1e18, and more critically, where deviations compound over time:

// Weight calculation lacks total weight validation:
newWeightsConverted[locals.i] = _prevWeights[locals.i] +
locals.kappa[0].mul(locals.signal[locals.i] - locals.normalizationFactor);
// No validation of total weight sum
// Each update compounds the previous deviation

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/MockChannelFollowing.sol";
import "../../../contracts/mock/MockPool.sol";
import "../utils.t.sol";
contract QuantammChannelFollowingTotalWeightTest is Test, QuantAMMTestUtils {
MockChannelFollowingRule public rule;
MockPool public mockPool;
int256[][] public parameters;
int256[] public prevWeights;
int256[] public data;
int256[] public variances;
int256[] public prevMovingAverages;
int128[] public lambda;
function setUp() public {
rule = new MockChannelFollowingRule(address(this));
mockPool = new MockPool(3600, 1 ether, address(rule));
// Setup normal parameters
setupParameters();
setupInitialState();
}
function setupParameters() private {
parameters = new int256[][]();
// [0] Kappa - normal update magnitude
parameters[0] = new int256[]();
parameters[0][0] = 0.1e18; // Standard kappa
// [1] Width - standard channel width
parameters[1] = new int256[]();
parameters[1][0] = 0.5e18; // Standard width
// [2] Amplitude - normal amplitude
parameters[2] = new int256[]();
parameters[2][0] = 0.2e18; // Standard amplitude
// [3] Exponents - standard trend following
parameters[3] = new int256[]();
parameters[3][0] = 1e18; // Linear trend following
// [4] Inverse Scaling - standard
parameters[4] = new int256[]();
parameters[4][0] = 0.541519e18;
// [5] Pre-exp Scaling - standard
parameters[5] = new int256[]();
parameters[5][0] = 1e18;
// [6] Use moving average
parameters[6] = new int256[]();
parameters[6][0] = 0;
}
function setupInitialState() private {
// Standard initial weights
prevWeights = new int256[](2);
prevWeights[0] = 0.5e18; // 50%
prevWeights[1] = 0.5e18; // 50%
// Normal price movements
data = new int256[](2);
data[0] = 1.01e18; // 1% increase
data[1] = 0.99e18; // 1% decrease
// Standard variances
variances = new int256[](2);
variances[0] = 1e18;
variances[1] = 1e18;
// Standard moving averages
prevMovingAverages = new int256[](4);
prevMovingAverages[0] = 1e18;
prevMovingAverages[1] = 1e18;
prevMovingAverages[2] = 1e18;
prevMovingAverages[3] = 1e18;
lambda = new int128[](1);
lambda[0] = int128(0.95e18); // Standard lambda
}
// Helper function for absolute value
function abs(int256 x) internal pure returns (int256) {
return x >= 0 ? x : -x;
}
function testChannelFollowingTotalWeightNormal() public {
// Use existing normal parameters
setupParameters();
setupInitialState();
runTotalWeightTest("Normal Conditions");
}
function testChannelFollowingTotalWeightExtreme() public {
setupParameters();
setupInitialState();
// Modify to extreme parameters
parameters[0][0] = 10e18; // Aggressive kappa
parameters[1][0] = 0.1e18; // Narrow width
parameters[2][0] = 5e18; // High amplitude
// Extreme initial weights
prevWeights[0] = 0.99e18; // 99%
prevWeights[1] = 0.01e18; // 1%
// Extreme price movements
data[0] = 20e18; // Price increased 20x
data[1] = 0.05e18; // Price dropped 95%
runTotalWeightTest("Extreme Conditions");
}
function runTotalWeightTest(string memory testType) private {
mockPool.setNumberOfAssets(2);
rule.initialisePoolRuleIntermediateValues(
address(mockPool),
prevMovingAverages,
variances,
2
);
int256[] memory results;
int256 maxPositiveDeviation;
int256 maxNegativeDeviation;
emit log_string("=== Test Type: ");
emit log_string(testType);
emit log_string(" ===");
// First update
rule.CalculateUnguardedWeights(
prevWeights,
data,
address(mockPool),
parameters,
lambda,
prevMovingAverages
);
results = rule.GetResultWeights();
prevWeights = results;
// Track updates
for (uint i = 0; i < 24; i++) {
rule.CalculateUnguardedWeights(
prevWeights,
data,
address(mockPool),
parameters,
lambda,
prevMovingAverages
);
results = rule.GetResultWeights();
int256 totalWeight = results[0] + results[1];
int256 deviation = totalWeight - 1e18;
emit log_named_uint("Update", i + 1);
emit log_named_int("Total weight", totalWeight);
emit log_named_int("Deviation", deviation);
// Track maximum deviations
if (deviation > maxPositiveDeviation) {
maxPositiveDeviation = deviation;
}
if (deviation < maxNegativeDeviation) {
maxNegativeDeviation = deviation;
}
prevWeights = results;
}
emit log_string("=== Final Results ===");
emit log_named_int("Max positive deviation", maxPositiveDeviation);
emit log_named_int("Max negative deviation", maxNegativeDeviation);
// Different assertions based on test type
if (keccak256(bytes(testType)) == keccak256(bytes("Normal Conditions"))) {
assertLt(
abs(maxNegativeDeviation),
3, // Normal conditions should have very small deviation
"Normal conditions should have minimal negative deviation"
);
assertEq(
maxPositiveDeviation,
0,
"Normal conditions should not have positive deviation"
);
} else {
// Extreme conditions have different expectations
assertTrue(
maxPositiveDeviation > 0,
"Extreme conditions should show positive deviation"
);
}
}
}

Test Results:

Normal Conditions (Small but consistent deviation):

=== Test Type:
Normal Conditions
===
Update: 1
Total weight: 1000000000000000000
Deviation: 0
Update: 2
Total weight: 1000000000000000000
Deviation: 0
Update: 3
Total weight: 999999999999999999
Deviation: -1
Update: 4
Total weight: 999999999999999999
Deviation: -1
Update: 5
Total weight: 999999999999999999
Deviation: -1
Update: 6
Total weight: 999999999999999999
Deviation: -1
Update: 7
Total weight: 999999999999999999
Deviation: -1
Update: 8
Total weight: 999999999999999999
Deviation: -1
Update: 9
Total weight: 999999999999999999
Deviation: -1
Update: 10
Total weight: 999999999999999999
Deviation: -1
Update: 11
Total weight: 999999999999999999
Deviation: -1
Update: 12
Total weight: 999999999999999999
Deviation: -1
Update: 13
Total weight: 999999999999999999
Deviation: -1
Update: 14
Total weight: 999999999999999999
Deviation: -1
Update: 15
Total weight: 999999999999999999
Deviation: -1
Update: 16
Total weight: 999999999999999999
Deviation: -1
Update: 17
Total weight: 999999999999999999
Deviation: -1
Update: 18
Total weight: 999999999999999999
Deviation: -1
Update: 19
Total weight: 999999999999999999
Deviation: -1
Update: 20
Total weight: 999999999999999999
Deviation: -1
Update: 21
Total weight: 999999999999999999
Deviation: -1
Update: 22
Total weight: 999999999999999999
Deviation: -1
Update: 23
Total weight: 999999999999999998
Deviation: -2
Update: 24
Total weight: 999999999999999998
Deviation: -2
=== Final Results ===
Max positive deviation: 0
Max negative deviation: -2

Extreme Conditions (Linear compounding growth):

=== Test Type:
Extreme Conditions
===
Update: 1
Total weight: 999999999999999980
Deviation: -20
Update: 2
Total weight: 999999999999999980
Deviation: -20
Update: 3
Total weight: 999999999999999970
Deviation: -30
Update: 4
Total weight: 999999999999999980
Deviation: -20
Update: 5
Total weight: 999999999999999990
Deviation: -10
Update: 6
Total weight: 1000000000000000000
Deviation: 0
Update: 7
Total weight: 1000000000000000000
Deviation: 0
Update: 8
Total weight: 1000000000000000010
Deviation: 10
Update: 9
Total weight: 1000000000000000010
Deviation: 10
Update: 10
Total weight: 1000000000000000010
Deviation: 10
Update: 11
Total weight: 1000000000000000010
Deviation: 10
Update: 12
Total weight: 1000000000000000010
Deviation: 10
Update: 13
Total weight: 1000000000000000010
Deviation: 10
Update: 14
Total weight: 1000000000000000020
Deviation: 20
Update: 15
Total weight: 1000000000000000030
Deviation: 30
Update: 16
Total weight: 1000000000000000040
Deviation: 40
Update: 17
Total weight: 1000000000000000050
Deviation: 50
Update: 18
Total weight: 1000000000000000050
Deviation: 50
Update: 19
Total weight: 1000000000000000060
Deviation: 60
Update: 20
Total weight: 1000000000000000060
Deviation: 60
Update: 21
Total weight: 1000000000000000070
Deviation: 70
Update: 22
Total weight: 1000000000000000080
Deviation: 80
Update: 23
Total weight: 1000000000000000090
Deviation: 90
Update: 24
Total weight: 1000000000000000100
Deviation: 100
=== Final Results ===
Max positive deviation: 100
Max negative deviation: -30

Impact

High - The compounding nature of this issue leads to increasingly severe deviations:

  1. Compounding Growth:

    • 1 day (24 updates): +240 wei

    • 1 week (168 updates): +1,680 wei

    • 1 month (~720 updates): +7,200 wei

    • 1 year (~8760 updates): +87,600 wei

  2. Core Issues:

    • Fundamental break in AMM invariant

    • Compounding deviation unlike other rules

    • Affects all operations relying on total weight being 1e18

    • More severe under extreme market conditions

    • Could lead to significant price calculation errors over time

Recommendations

  1. Add total weight validation and normalization:

int256 totalWeight = 0;
for (uint i = 0; i < newWeightsConverted.length; i++) {
totalWeight += newWeightsConverted[i];
}
for (uint i = 0; i < newWeightsConverted.length; i++) {
newWeightsConverted[i] = (newWeightsConverted[i] * 1e18) / totalWeight;
}
  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

    • Track and compensate for division remainders

  2. Add tests:

    • Test total weight invariant across all parameter combinations

    • Add fuzz testing for weight calculations

    • Test edge cases with extreme price movements

    • Test for compounding effects over longer periods

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!