QuantAMM

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

Weight Normalization Process Violates Epsilon Guard Rail Limits

Summary

The QuantAMMMathGuard contract's weight normalization process can violate its own epsilon guard rail limits, allowing weight changes up to 28% larger than the specified maximum. This undermines a core safety mechanism designed to protect against MEV attacks and could enable larger-than-intended arbitrage opportunities.

Vulnerability Details

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

QuantAMMMathGuard.sol

///@notice Normalizes the weights to ensure that the sum of the weights is equal to 1
///@param _prevWeights Previous weights
///@param _newWeights New weights
///@param _epsilonMax Maximum allowed change in weights per update step (epsilon) in the QuantAMM whitepaper
function _normalizeWeightUpdates(
int256[] memory _prevWeights,
int256[] memory _newWeights,
int256 _epsilonMax
) internal pure returns (int256[] memory) {
unchecked {
// Step 1: Find largest weight change
int256 maxAbsChange = _epsilonMax; // Note: initialized to epsilonMax
for (uint i; i < _prevWeights.length; ++i) {
int256 absChange;
if (_prevWeights[i] > _newWeights[i]) {
absChange = _prevWeights[i] - _newWeights[i];
} else {
absChange = _newWeights[i] - _prevWeights[i];
}
if (absChange > maxAbsChange) {
maxAbsChange = absChange;
}
}
// Step 2: Scale changes if needed and sum weights
int256 newWeightsSum;
if (maxAbsChange > _epsilonMax) {
int256 rescaleFactor = _epsilonMax.div(maxAbsChange);
for (uint i; i < _newWeights.length; ++i) {
int256 newDelta = (_newWeights[i] - _prevWeights[i]).mul(rescaleFactor);
_newWeights[i] = _prevWeights[i] + newDelta;
newWeightsSum += _newWeights[i];
}
} else {
for (uint i; i < _newWeights.length; ++i) {
newWeightsSum += _newWeights[i];
}
}
// Step 3: Critical issue - final normalization is not scaled or checked against epsilonMax
_newWeights[0] = _newWeights[0] + (ONE - newWeightsSum);
}
return _newWeights;
}

QuantAMMMathGuard.sol

/// @notice Guards QuantAMM weights updates
/// @param _weights Raw weights to be guarded and normalized
/// @param _prevWeights Previous weights to be used for normalization
/// @param _epsilonMax Maximum allowed change in weights per update step (epsilon) in the QuantAMM whitepaper
/// @param _absoluteWeightGuardRail Maximum allowed weight in the QuantAMM whitepaper
function _guardQuantAMMWeights(
int256[] memory _weights,
int256[] calldata _prevWeights,
int256 _epsilonMax,
int256 _absoluteWeightGuardRail
) internal pure returns (int256[] memory guardedNewWeights) {

The issue occurs in the _normalizeWeightUpdates function where the normalization process can result in weight changes that exceed the epsilonMax parameter. While the code comments acknowledge that "a very small (1e-18) rounding error" might break the guard rail and is "modelled to be acceptable", our testing demonstrates violations that are orders of magnitude larger than this accepted threshold.

Proof of Concept

  1. Test Setup:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.26;
import "forge-std/Test.sol";
import "@prb/math/contracts/PRBMathSD59x18.sol";
import { MockCalculationRule } from "../../../contracts/mock/MockCalculationRule.sol";
import { MockPool } from "../../../contracts/mock/MockPool.sol";
import { MockQuantAMMMathGuard } from "../../../contracts/mock/MockQuantAMMMathGuard.sol";
contract QuantammMathGuardEpsilonTest is Test {
using PRBMathSD59x18 for int256;
MockQuantAMMMathGuard mockQuantAMMMathGuard;
function setUp() public {
mockQuantAMMMathGuard = new MockQuantAMMMathGuard();
}
function test_EpsilonViolationExact() public {
// Exact values from second fuzzing failure
int256[] memory prevWeights = new int256[]();
prevWeights[0] = 0.979999999999999947e18; // ~98%
prevWeights[1] = 0.018530000200344578e18; // ~1.85%
prevWeights[2] = 0.001469999799655475e18; // ~0.15%
int256[] memory newWeights = new int256[]();
newWeights[0] = 0.949168597366431485e18; // Attempted ~3% decrease
newWeights[1] = 0.049361402833913040e18; // Attempted large increase
newWeights[2] = 0.001469999799655475e18; // No change
int256 epsilonMax = 0.015415701316784231e18; // ~1.54%
emit log_named_int("epsilonMax", epsilonMax);
int256[] memory result = mockQuantAMMMathGuard.mockGuardQuantAMMWeights(
newWeights,
prevWeights,
epsilonMax,
0.01e18 // absoluteWeightGuardRail
);
// Log all weight changes for analysis
for(uint i = 0; i < 3; i++) {
int256 change = (result[i] - prevWeights[i]).abs();
emit log_named_uint("Asset Index", i);
emit log_named_int("Previous Weight", prevWeights[i]);
emit log_named_int("New Weight", result[i]);
emit log_named_int("Change Amount", change);
emit log_named_int("Epsilon Max", epsilonMax);
assertLe(change, epsilonMax, "Change exceeded epsilonMax");
}
}
}
  1. Result:

// Test failure output
[FAIL] Change exceeded epsilonMax: 19680701416956494 > 15415701316784231
Asset Index: 0
Previous Weight: 979999999999999947
New Weight: 960319298583043453
Change Amount: 19680701416956494
Epsilon Max: 15415701316784231
  1. Analysis:

  • Allowed change (epsilonMax): 1.54%

  • Actual change: 1.97%

  • Violation margin: ~28% larger than allowed

  • Consistently reproducible with similar weight distributions

Impact

Severity: HIGH

  1. Technical Impact:

    • Core safety mechanism (epsilon guard rail) is violated

    • Weight changes can exceed intended maximum by ~28%

    • Affects all pools using QuantAMMMathGuard

    • Occurs during normal operation

    • No special conditions required

  2. Economic Impact:

    • Larger-than-intended arbitrage opportunities

    • Increased MEV attack surface

    • Potential for faster pool imbalances

    • Could affect connected protocols relying on weight change limits

Tools Used

  • Foundry fuzzing tests

  • Manual code review

  • Mathematical analysis of normalization algorithm

  • Custom test suite for epsilon boundary conditions

Recommendations

  1. Modify Normalization Process:

function _normalizeWeightUpdates(
int256[] memory _prevWeights,
int256[] memory _newWeights,
int256 _epsilonMax
) internal pure returns (int256[] memory) {
unchecked {
// First pass: Calculate required scaling to respect epsilon
int256 maxAbsChange = 0;
for (uint i = 0; i < _prevWeights.length; ++i) {
int256 absChange = (_newWeights[i] - _prevWeights[i]).abs();
if (absChange > maxAbsChange) {
maxAbsChange = absChange;
}
}
// Apply epsilon scaling first
if (maxAbsChange > _epsilonMax) {
int256 rescaleFactor = _epsilonMax.div(maxAbsChange);
for (uint i = 0; i < _newWeights.length; ++i) {
_newWeights[i] = _prevWeights[i] +
(_newWeights[i] - _prevWeights[i]).mul(rescaleFactor);
}
}
// Then normalize to sum to 1 while preserving relative distances
int256 sum = 0;
for (uint i = 0; i < _newWeights.length; ++i) {
sum += _newWeights[i];
}
if (sum != ONE) {
int256 normFactor = ONE.div(sum);
for (uint i = 0; i < _newWeights.length; ++i) {
int256 normalizedWeight = _newWeights[i].mul(normFactor);
// Verify epsilon not violated by normalization
require(
(normalizedWeight - _prevWeights[i]).abs() <= _epsilonMax,
"Normalization violated epsilon"
);
_newWeights[i] = normalizedWeight;
}
}
return _newWeights;
}
}
  1. Add Safety Checks:

    • Verify epsilon constraints after normalization

    • Add explicit bounds checking

    • Consider implementing a two-step normalization process

    • Add invariant checks throughout weight update process

Known Issues Analysis

This issue was checked against the known issues in the README and is not covered by any of them:

  1. The README mentions "Issues when a pool is configured to have inappropriate guard rails is out of scope", but this is different as it's an implementation bug in the guard rail mechanism itself, not a configuration issue.

  2. While the README notes "multi-block MEV if appropriate guard rails are not applied" as a known risk, this finding demonstrates that even properly configured guard rails can be violated due to a normalization implementation flaw.

  3. The issue is not related to any of the other known limitations like oracle manipulation, random update intervals, or extreme weight configurations.

References

Updates

Lead Judging Commences

n0kto Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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