QuantAMM

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

Unprotected Weight Validation in Scalar Kappa Mode in `ChannelFollowingUpdateRule.sol` Allows for Negative Portfolio Weights

Summary:

The AMM's weight calculation function (_getWeights) in ChannelFollowingUpdateRule.sol implements asymmetric validation between scalar and vector kappa modes. While vector kappa mode enforces positive weight validation through a require statement, scalar kappa mode lacks this protection, allowing portfolio weights to become negative. This discrepancy creates a potential for portfolio imbalance and system manipulation.

Affected section of code: https://github.com/Cyfrin/2024-12-quantamm/blob/a775db4273eb36e7b4536c5b60207c9f17541b92/pkg/pool-quantamm/contracts/rules/ChannelFollowingUpdateRule.sol#L226-L236

Vulnerability Details:

Within the _getWeights function, weight validation occurs differently based on the kappa parameter structure:

  1. In vector kappa mode (when kappa.length > 1), weights are validated:

require(newWeightsConverted[locals.i] >= 0, "Invalid weight");
  1. In scalar kappa mode (when kappa.length == 1), no validation exists, allowing negative weights:

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

The issue manifests under certain parameter combinations:

  • Small initial weights

  • Large price differentials

  • High amplitude settings

  • Narrow width parameters

POC

Paste the following code into the QuantAMMChannelFollowing.t.sol file

function testNegativeWeightsAllowedForScalarKappa() public {
// Initialize parameters array with all required parameters
int256[][] memory parameters = new int256[][]();
parameters[0] = new int256[]();
parameters[0][0] = PRBMathSD59x18.fromInt(200);
parameters[1] = new int256[]();
parameters[1][0] = 1e18;
parameters[2] = new int256[]();
parameters[2][0] = PRBMathSD59x18.fromInt(1);
parameters[3] = new int256[]();
parameters[3][0] = PRBMathSD59x18.fromInt(2);
// Standard inverse scaling
parameters[4] = new int256[]();
parameters[4][0] = PRBMathSD59x18.fromInt(1);
parameters[5] = new int256[]();
parameters[5][0] = 0.1e18;
// Use raw price
parameters[6] = new int256[]();
parameters[6][0] = PRBMathSD59x18.fromInt(1);
// Previous alphas initialization
int256[] memory prevAlphas = new int256[]();
prevAlphas[0] = PRBMathSD59x18.fromInt(1);
prevAlphas[1] = PRBMathSD59x18.fromInt(1);
// Moving averages initialization
int256[] memory prevMovingAverages = new int256[]();
prevMovingAverages[0] = PRBMathSD59x18.fromInt(2);
prevMovingAverages[1] = PRBMathSD59x18.fromInt(2);
int256[] memory movingAverages = new int256[]();
movingAverages[0] = PRBMathSD59x18.fromInt(2);
movingAverages[1] = PRBMathSD59x18.fromInt(2);
// Lambda initialization
int128[] memory lambdas = new int128[]();
lambdas[0] = int128(0.2e18);
// Small initial weight for first asset
int256[] memory prevWeights = new int256[]();
prevWeights[0] = int256(0.1e18);
prevWeights[1] = int256(0.9e18);
// Extreme price difference to create large gradient
int256[] memory data = new int256[]();
data[0] = PRBMathSD59x18.fromInt(1);
data[1] = PRBMathSD59x18.fromInt(2);
// Initialize pool
mockPool.setNumberOfAssets(2);
// Initialize the rule
rule.initialisePoolRuleIntermediateValues(
address(mockPool),
prevMovingAverages,
prevAlphas,
2
);
// Calculate weights
rule.CalculateUnguardedWeights(
prevWeights,
data,
address(mockPool),
parameters,
lambdas,
movingAverages
);
// Get results
int256[] memory resultWeights = rule.GetResultWeights();
// Log weights for inspection
emit log_named_int("Weight 0", resultWeights[0]);
emit log_named_int("Weight 1", resultWeights[1]);
// Check for negative weights
bool hasNegativeWeight = false;
for (uint256 i = 0; i < resultWeights.length; i++) {
if (resultWeights[i] < 0) {
hasNegativeWeight = true;
break;
}
}
assertTrue(hasNegativeWeight, "Expected to find at least one negative weight");
}

Impact:

  1. Portfolio Imbalance: Negative weights can create mathematically inconsistent portfolio allocations

  2. Economic Risks: Negative weights could be exploited to manipulate pool balances

  3. Protocol Inconsistency: Different behavior between scalar and vector modes creates unexpected edge cases

  4. Potential System Gaming: Attackers could intentionally trigger negative weights to exploit pool dynamics

Tools Used:

  • Manual code review

  • Foundry testing framework

Recommendations:

  1. Primary Fix: Add weight validation to scalar kappa mode:

// Add in scalar kappa section
for (locals.i = 0; locals.i < locals.prevWeightLength; ) {
newWeightsConverted[locals.i] = _prevWeights[locals.i] +
locals.kappa[0].mul(locals.signal[locals.i] - locals.normalizationFactor);
+ require(newWeightsConverted[locals.i] >= 0, "Invalid weight"); // Add this line
unchecked {
++locals.i;
}
}
Updates

Lead Judging Commences

n0kto Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
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.