QuantAMM

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

Negative Pool Weights Possible in ChannelFollowingUpdateRule

Summary

The ChannelFollowingUpdateRule contract allows negative weights to be created in pools under extreme conditions. The weights can go negative within just 5 updates while maintaining total weight at 1e18, breaking fundamental AMM invariants.

Vulnerability Details

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

The issue occurs in the weight calculation where aggressive parameters and extreme price movements can drive weights negative:

newWeightsConverted[locals.i] = _prevWeights[locals.i] +
locals.kappa[0].mul(locals.signal[locals.i] - locals.normalizationFactor);

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 QuantammChannelFollowingNegativeWeightsTest is Test, QuantAMMTestUtils {
MockChannelFollowingRule public rule;
MockPool public mockPool;
function setUp() public {
rule = new MockChannelFollowingRule(address(this));
mockPool = new MockPool(3600, 1 ether, address(rule));
}
function testChannelFollowingNegativeWeightsExtreme() public {
// Setup parameters with extreme values
int256[][] memory parameters = new int256[][]();
// [0] Kappa - very aggressive update magnitude
parameters[0] = new int256[]();
parameters[0][0] = 10e18; // 10x more aggressive than normal
// [1] Width - very narrow channel
parameters[1] = new int256[]();
parameters[1][0] = 0.1e18; // Narrow width increases sensitivity
// [2] Amplitude - high amplitude
parameters[2] = new int256[]();
parameters[2][0] = 5e18; // 5x normal amplitude
// [3] Exponents - aggressive trend following
parameters[3] = new int256[]();
parameters[3][0] = 0.5e18; // Square root for more aggressive response
// [4] Inverse Scaling - standard
parameters[4] = new int256[]();
parameters[4][0] = 0.541519e18;
// [5] Pre-exp Scaling - reduced to amplify effect
parameters[5] = new int256[]();
parameters[5][0] = 0.1e18; // Small scaling to amplify trends
// [6] Use raw price
parameters[6] = new int256[]();
parameters[6][0] = 1e18; // Use raw price for more volatility
// Extreme initial weights
int256[] memory prevWeights = new int256[]();
prevWeights[0] = 0.99e18; // 99%
prevWeights[1] = 0.01e18; // 1%
// Extreme price movements
int256[] memory data = new int256[]();
data[0] = 20e18; // Price increased 20x
data[1] = 0.05e18; // Price dropped 95%
// Extreme variances
int256[] memory variances = new int256[]();
variances[0] = 0.001e18; // Very low variance
variances[1] = 1000e18; // Very high variance
// Extreme moving averages
int256[] memory prevMovingAverages = new int256[]();
prevMovingAverages[0] = 0.1e18; // Very low
prevMovingAverages[1] = 10e18; // Very high
prevMovingAverages[2] = 0.1e18; // Very low
prevMovingAverages[3] = 0.05e18; // Extremely low
int128[] memory lambda = new int128[]();
lambda[0] = int128(0.999e18); // Very high lambda
// Initialize pool
mockPool.setNumberOfAssets(2);
rule.initialisePoolRuleIntermediateValues(
address(mockPool),
prevMovingAverages,
variances,
2
);
// Track multiple updates
int256[] memory results;
bool foundNegative = false;
// Perform multiple updates to try to force negative weights
for(uint i = 0; i < 10; i++) {
rule.CalculateUnguardedWeights(
prevWeights,
data,
address(mockPool),
parameters,
lambda,
prevMovingAverages
);
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]);
if (results[0] < 0 || results[1] < 0) {
foundNegative = true;
if (results[0] < 0) {
emit log_string("Weight 0 went negative");
}
if (results[1] < 0) {
emit log_string("Weight 1 went negative");
}
break;
}
// Use results as next prevWeights
prevWeights = results;
}
// Verify total weight remains 1e18 even with extreme conditions
assertEq(
results[0] + results[1],
1e18,
"Total weight should remain exactly 1e18"
);
// Check if we found any negative weights
assertTrue(
foundNegative,
"Should be able to produce negative weights under extreme conditions"
);
}
// Helper function for absolute value
function abs(int256 x) internal pure returns (int256) {
return x >= 0 ? x : -x;
}
}

Test Results:

Update: 1
Weight 0: 902884388529738820
Weight 1: 97115611470261190
Total Weight: 1000000000000000010
Update: 2
Weight 0: 719073687365397700
Weight 1: 280926312634602320
Total Weight: 1000000000000000020
Update: 3
Weight 0: 438669156159542750
Weight 1: 561330843840457280
Total Weight: 1000000000000000030
Update: 4
Weight 0: 61773881363725370
Weight 1: 938226118636274670
Total Weight: 1000000000000000040
Update: 5
Weight 0: -411507299345927140
Weight 1: 1411507299345927190
Total Weight: 1000000000000000050
Weight 0 went negative

Impact

  • Weights can go negative within 5 updates

  • Occurs with combination of:

    • Aggressive kappa (10x normal)

    • Narrow channel width

    • High amplitude

    • Extreme price movements

    • Imbalanced initial weights

  • Breaks fundamental AMM invariants about weight bounds

  • Could lead to invalid pool states and economic exploits

  • Could cause catastrophic issues with price calculations and swaps

Recommendations

  1. Add negative weight validation:

int256 newWeight = _prevWeights[locals.i] +
locals.kappa[0].mul(locals.signal[locals.i] - locals.normalizationFactor);
require(newWeight >= 0, "Invalid weight");
newWeightsConverted[locals.i] = newWeight;
  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

    • Add maximum weight bounds to prevent extreme values

  2. Add tests:

    • Test weight bounds across all parameter combinations

    • 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!