QuantAMM

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

QuantAMMCovarianceBasedRule Asymmetric Handling of Negative Prices

Summary

The QuantAMMCovarianceBasedRule contract handles negative prices asymmetrically compared to positive prices, leading to distorted covariance matrix calculations. While ChainlinkOracle enforces positive prices (require(data > 0)), negative prices can occur through the project's MultiHopOracle which performs mathematical operations that may result in negative values. This asymmetry (4.4% difference) causes imbalances in all covariance-based weight calculations and could impact pools using MultiHopOracle or similar oracles that support negative prices.

Vulnerability Details

Location: pkg/pool-quantamm/contracts/rules/base/QuantammCovarianceBasedRule.sol

The issue occurs in the covariance calculation where negative prices produce asymmetric results compared to equivalent positive prices:

locals.intermediateState =
locals.convertedLambda.mul(intermediateCovarianceState[i][j]) +
locals.u[i].mul(locals.v[j]).div(TENPOWEIGHTEEN); // i is the row, j the column -> u_i * v_j the result of the outer product.

When handling negative prices, the sign propagation in the outer product calculation (u_i * v_j) causes asymmetric covariance values. For example, with identical magnitude price deviations but opposite signs:

  • Positive price deviation: covariance[0][0] = 4.800

  • Negative price deviation: covariance[0][0] = 5.010

  • Asymmetry: 4.4% difference

Proof of Concept

// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.8.24;
import {Test} from "forge-std/Test.sol";
import {PRBMathSD59x18} from "@prb/math/contracts/PRBMathSD59x18.sol";
import {QuantAMMCovarianceBasedRule} from "../../../contracts/rules/base/QuantammCovarianceBasedRule.sol";
import {QuantAMMPoolParameters} from "../../../contracts/UpdateWeightRunner.sol";
contract MockQuantammCovarianceBasedRule is QuantAMMCovarianceBasedRule {
using PRBMathSD59x18 for int256;
function exposed_calculateQuantAMMCovariance(
int256[] memory _newData,
QuantAMMPoolParameters memory _poolParameters
) public returns (int256[][] memory) {
return _calculateQuantAMMCovariance(_newData, _poolParameters);
}
function exposed_setIntermediateCovariance(
address _poolAddress,
int256[][] memory _initialValues,
uint _numberOfAssets
) public {
_setIntermediateCovariance(_poolAddress, _initialValues, _numberOfAssets);
}
}
contract QuantammCovarianceNegativePricesTest is Test {
using PRBMathSD59x18 for int256;
MockQuantammCovarianceBasedRule public rule;
address public mockPool;
uint256 public constant N_ASSETS = 2;
int256 public constant LAMBDA = 0.95e18;
int256 public constant ONE = 1e18;
int256 public constant TOLERANCE = 0.0001e18;
function setUp() public {
rule = new MockQuantammCovarianceBasedRule();
mockPool = address(0x1);
}
function testCovarianceNegativePriceSymmetry() public {
// Initial covariance matrix
int256[][] memory initialCovariance = new int256[][]();
for (uint i = 0; i < N_ASSETS; i++) {
initialCovariance[i] = new int256[]();
for (uint j = 0; j < N_ASSETS; j++) {
initialCovariance[i][j] = 100e18;
}
}
rule.exposed_setIntermediateCovariance(mockPool, initialCovariance, N_ASSETS);
// Setup parameters
QuantAMMPoolParameters memory params = QuantAMMPoolParameters({
pool: mockPool,
numberOfAssets: N_ASSETS,
lambda: new int128[](N_ASSETS),
movingAverage: new int256[](N_ASSETS * 2)
});
for (uint i = 0; i < N_ASSETS; i++) {
params.lambda[i] = int128(LAMBDA);
params.movingAverage[i] = 1e18; // Current MA
params.movingAverage[N_ASSETS + i] = 1e18; // Previous MA
}
// Test with positive prices
int256[] memory positivePrices = new int256[]();
positivePrices[0] = 2e18; // 2x the MA
positivePrices[1] = 1e18; // Equal to MA
// Test with equivalent negative deviation
int256[] memory negativePrices = new int256[]();
negativePrices[0] = -2e18; // -2x the MA (same magnitude deviation)
negativePrices[1] = 1e18; // Equal to MA
// Calculate covariance for positive prices
int256[][] memory positiveResult = rule.exposed_calculateQuantAMMCovariance(positivePrices, params);
// Calculate covariance for negative prices
int256[][] memory negativeResult = rule.exposed_calculateQuantAMMCovariance(negativePrices, params);
// Log results
emit log_named_decimal_int("Positive price covariance[0][0]", positiveResult[0][0], 18);
emit log_named_decimal_int("Positive price covariance[0][1]", positiveResult[0][1], 18);
emit log_named_decimal_int("Negative price covariance[0][0]", negativeResult[0][0], 18);
emit log_named_decimal_int("Negative price covariance[0][1]", negativeResult[0][1], 18);
// Check symmetry - covariance should be the same for equivalent positive/negative deviations
assertApproxEqAbs(
positiveResult[0][0],
negativeResult[0][0],
1e16,
"Covariance asymmetry detected in [0][0]"
);
assertApproxEqAbs(
positiveResult[0][1],
negativeResult[0][1],
1e16,
"Covariance asymmetry detected in [0][1]"
);
// Verify matrix symmetry
assertApproxEqAbs(
positiveResult[0][1],
positiveResult[1][0],
1e16,
"Positive covariance matrix not symmetric"
);
assertApproxEqAbs(
negativeResult[0][1],
negativeResult[1][0],
1e16,
"Negative covariance matrix not symmetric"
);
}
}

Test Results:

Positive price covariance[0][0]: 4.800000000000000000
Positive price covariance[0][1]: 4.750000000000000000
Negative price covariance[0][0]: 5.010000000000000000
Negative price covariance[0][1]: 4.512500000000000000

Impact

  • Asymmetric covariance calculations (4.4% difference for equivalent price movements)

  • Distortion of covariance-based weight calculations

  • Could lead to pool imbalances

  • Creates arbitrage opportunities

  • Compounds through intermediate state storage

  • Affects multiple rules that use covariance matrices

  • Breaks mathematical invariants for covariance calculations

Recommendations

  1. Modify covariance calculation to handle signs correctly:

// Calculate price differences
int256 u_i = _newData[i] - _poolParameters.movingAverage[locals.n + i];
int256 v_j = _newData[j] - _poolParameters.movingAverage[j];
// Ensure symmetric handling of positive/negative deviations
locals.intermediateState =
locals.convertedLambda.mul(intermediateCovarianceState[i][j]) +
u_i.mul(v_j).abs().div(TENPOWEIGHTEEN);
  1. Consider architectural improvements:

    • Add explicit covariance sign handling

    • Implement covariance symmetry validation

    • Add covariance-specific invariant checks

    • Consider using absolute values for price differences

    • Add documentation about covariance calculation expectations

    • Consider extracting covariance calculation to a library

    • Add safeguards against extreme covariance values

  2. Add comprehensive tests:

    • Test covariance symmetry with various price magnitudes

    • Test edge cases with extreme price values

    • Add property-based tests for covariance calculations

    • Test covariance state evolution over time

    • Test compounding effects through intermediate state

    • Test interactions with all covariance-based rules

    • Add specific tests for negative price scenarios

References

Updates

Lead Judging Commences

n0kto Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

invalid_getData_negative_or_zero_price

Multihop will call ChainlinkOracle and the check is in it: `require(data > 0, "INVLDDATA");` MultiHop is just here to combine Chainlinks feed when there is no direct USD price feed for a token.

Support

FAQs

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

Give us feedback!