QuantAMM

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

QuantAMMGradientBasedRule Asymmetric Handling of Negative Prices

Summary

The QuantAMMGradientBasedRule contract, which serves as a base for multiple update rules, handles negative prices asymmetrically compared to positive prices. This leads to inconsistent gradient calculations that propagate to derived rules. While ChainlinkOracle enforces positive prices, negative prices can occur through MultiHopOracle's mathematical operations. The asymmetric gradient calculations could cause unexpected pool behavior and potential economic vulnerabilities.

Vulnerability Details

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

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

// In _calculateQuantAMMGradient:
locals.intermediateValue =
convertedLambda.mul(locals.intermediateGradientState[i]) +
(_newData[i] - _poolParameters.movingAverage[i]).div(oneMinusLambda);

When handling negative prices, the asymmetry arises from:

  1. The price difference calculation (_newData[i] - _poolParameters.movingAverage[i])

  2. Division by oneMinusLambda

  3. Multiplication by the mulFactor (λ^3 / (1-λ))

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test} from "forge-std/Test.sol";
import {PRBMathSD59x18} from "@prb/math/contracts/PRBMathSD59x18.sol";
import {QuantAMMGradientBasedRule} from "../../../contracts/rules/base/QuantammGradientBasedRule.sol";
import {QuantAMMPoolParameters} from "../../../contracts/UpdateWeightRunner.sol";
contract TestQuantAMMGradientRule is QuantAMMGradientBasedRule {
using PRBMathSD59x18 for int256;
function exposed_calculateGradient(
int256[] memory newData,
QuantAMMPoolParameters memory poolParameters
) public returns (int256[] memory) {
return _calculateQuantAMMGradient(newData, poolParameters);
}
function exposed_setGradient(
address poolAddress,
int256[] memory initialValues,
uint numberOfAssets
) public {
_setGradient(poolAddress, initialValues, numberOfAssets);
}
}
contract QuantammGradientNegativeWeightsTest is Test {
using PRBMathSD59x18 for int256;
TestQuantAMMGradientRule internal rule;
address internal mockPool;
uint256 constant N_ASSETS = 2;
int256 constant LAMBDA = 0.95e18;
int256 constant ONE = 1e18;
function setUp() public {
rule = new TestQuantAMMGradientRule();
mockPool = address(0x1);
}
function testGradientNegativePriceSymmetry() public {
// Initial setup
int256[] memory initialGradients = new int256[]();
for (uint i = 0; i < N_ASSETS; i++) {
initialGradients[i] = ONE;
}
rule.exposed_setGradient(mockPool, initialGradients, N_ASSETS);
// Setup parameters
QuantAMMPoolParameters memory params = QuantAMMPoolParameters({
pool: mockPool,
numberOfAssets: N_ASSETS,
lambda: new int128[](1),
movingAverage: new int256[](N_ASSETS * 2)
});
params.lambda[0] = int128(LAMBDA);
for (uint i = 0; i < N_ASSETS * 2; i++) {
params.movingAverage[i] = ONE;
}
// Test with positive prices
int256[] memory positivePrices = new int256[]();
positivePrices[0] = 2e18; // Price 2x
positivePrices[1] = ONE; // Price unchanged
// Test with negative prices (same magnitude)
int256[] memory negativePrices = new int256[]();
negativePrices[0] = -2e18; // Price -2x
negativePrices[1] = ONE; // Price unchanged
// Calculate gradients for both scenarios
int256[] memory positiveGradients = rule.exposed_calculateGradient(positivePrices, params);
int256[] memory negativeGradients = rule.exposed_calculateGradient(negativePrices, params);
// Log results
emit log_named_decimal_int("Positive price gradient 0", positiveGradients[0], 18);
emit log_named_decimal_int("Positive price gradient 1", positiveGradients[1], 18);
emit log_named_decimal_int("Negative price gradient 0", negativeGradients[0], 18);
emit log_named_decimal_int("Negative price gradient 1", negativeGradients[1], 18);
// Check for asymmetry in magnitude
int256 positiveGradientMagnitude = abs(positiveGradients[0]);
int256 negativeGradientMagnitude = abs(negativeGradients[0]);
emit log_named_decimal_int("Positive gradient magnitude", positiveGradientMagnitude, 18);
emit log_named_decimal_int("Negative gradient magnitude", negativeGradientMagnitude, 18);
emit log_named_decimal_int("Magnitude difference", positiveGradientMagnitude - negativeGradientMagnitude, 18);
// The gradients should be symmetric (equal in magnitude but opposite in sign)
assertApproxEqAbs(
positiveGradientMagnitude,
negativeGradientMagnitude,
1e16, // Allow 1% difference
"Gradient asymmetry detected"
);
// The unchanged asset should have similar gradients in both cases
assertApproxEqAbs(
positiveGradients[1],
negativeGradients[1],
1e16,
"Control asset gradient mismatch"
);
}
function abs(int256 x) internal pure returns (int256) {
return x >= 0 ? x : -x;
}
}

Test Results:

Positive price gradient: 0.002756578947368420
Negative price gradient: -0.005275986842105261
Magnitude difference: -0.002519407894736841 (91.7% larger for negative prices)
Control asset gradient (positive): 0.000125
Control asset gradient (negative): 0.000118

Impact

  • Base gradient calculations are asymmetric for positive vs negative prices

  • This asymmetry propagates to all derived update rules

  • ~91.7% larger gradient magnitude for negative prices

  • Inconsistent pool behavior depending on price sign

  • Could lead to unexpected weight distributions in derived rules

  • May create arbitrage opportunities due to predictable asymmetry

  • Breaks mathematical symmetry expected in gradient calculations

  • Could compound with other weight calculation issues

Recommendations

  1. Modify gradient calculation to maintain symmetry:

// Handle price difference symmetrically
int256 priceDiff = _newData[i] - _poolParameters.movingAverage[i];
int256 sign = priceDiff >= 0 ? ONE : -ONE;
int256 magnitude = abs(priceDiff);
locals.intermediateValue =
convertedLambda.mul(locals.intermediateGradientState[i]) +
sign.mul(magnitude.div(oneMinusLambda));
  1. Consider architectural improvements:

    • Add explicit sign handling throughout gradient calculations

    • Implement symmetry validation in base rule tests

    • Add invariant checks for gradient sign handling

    • Consider using absolute values for intermediate calculations

    • Add documentation about gradient sign handling expectations

  2. Add comprehensive tests:

    • Test gradient symmetry with various price magnitudes

    • Test edge cases with extreme price values

    • Add property-based tests for gradient sign handling

    • Test interactions with derived rules

    • Test compounding effects over multiple updates

Updates

Lead Judging Commences

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