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
function _normalizeWeightUpdates(
int256[] memory _prevWeights,
int256[] memory _newWeights,
int256 _epsilonMax
) internal pure returns (int256[] memory) {
unchecked {
int256 maxAbsChange = _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;
}
}
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];
}
}
_newWeights[0] = _newWeights[0] + (ONE - newWeightsSum);
}
return _newWeights;
}
QuantAMMMathGuard.sol
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
Test Setup:
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 {
int256[] memory prevWeights = new int256[]();
prevWeights[0] = 0.979999999999999947e18;
prevWeights[1] = 0.018530000200344578e18;
prevWeights[2] = 0.001469999799655475e18;
int256[] memory newWeights = new int256[]();
newWeights[0] = 0.949168597366431485e18;
newWeights[1] = 0.049361402833913040e18;
newWeights[2] = 0.001469999799655475e18;
int256 epsilonMax = 0.015415701316784231e18;
emit log_named_int("epsilonMax", epsilonMax);
int256[] memory result = mockQuantAMMMathGuard.mockGuardQuantAMMWeights(
newWeights,
prevWeights,
epsilonMax,
0.01e18
);
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");
}
}
}
Result:
[FAIL] Change exceeded epsilonMax: 19680701416956494 > 15415701316784231
Asset Index: 0
Previous Weight: 979999999999999947
New Weight: 960319298583043453
Change Amount: 19680701416956494
Epsilon Max: 15415701316784231
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
-
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
-
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
Recommendations
Modify Normalization Process:
function _normalizeWeightUpdates(
int256[] memory _prevWeights,
int256[] memory _newWeights,
int256 _epsilonMax
) internal pure returns (int256[] memory) {
unchecked {
int256 maxAbsChange = 0;
for (uint i = 0; i < _prevWeights.length; ++i) {
int256 absChange = (_newWeights[i] - _prevWeights[i]).abs();
if (absChange > maxAbsChange) {
maxAbsChange = absChange;
}
}
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);
}
}
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);
require(
(normalizedWeight - _prevWeights[i]).abs() <= _epsilonMax,
"Normalization violated epsilon"
);
_newWeights[i] = normalizedWeight;
}
}
return _newWeights;
}
}
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:
-
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.
-
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.
-
The issue is not related to any of the other known limitations like oracle manipulation, random update intervals, or extreme weight configurations.
References