QuantAMM

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

Unvalidated Denominator in MomentumUpdateRule Weight Calculation Leading to Extreme Pool Weight Manipulation

Summary

The MomentumUpdateRule contract fails to validate denominator values in weight calculations, allowing manipulation of pool weights through small denominator values, resulting in extreme weight imbalances that could destabilize the pool.

Vulnerability Details

locals.denominator = _poolParameters.movingAverage[locals.i];
if (locals.useRawPrice) {
locals.denominator = _data[locals.i];
}
// 1/p(t) * ∂p(t)/∂t calculated and stored
locals.newWeights[locals.i] = ONE.div(locals.denominator).mul(locals.newWeights[locals.i]);

In the _getWeights function there is no minimum denominator validation and the division operation (div) is performed directly without checking the denominator value which causes the result of division with a small denominator (1) to produce a very large value.

if (locals.kappaStore.length == 1) {
locals.normalizationFactor += locals.newWeights[locals.i];
} else {
locals.normalizationFactor += (locals.newWeights[locals.i].mul(locals.kappaStore[locals.i]));
}

Extreme values ​​of the newWeights affect the normalizationFactor and there is no maximum limit for the calculation result.

function validParameters(int256[][] calldata _parameters) external pure override returns (bool) {
// There is no validation for the denominator value.
if (_parameters.length == 1 || (_parameters.length == 2 && _parameters[1].length == 1)) {
int256[] memory kappa = _parameters[0];
uint16 valid = uint16(kappa.length) > 0 ? 1 : 0;
// Only kappa validation, no denominator validation
...
}
return false;
}

In the validParameters function there is also no validation for the denominator value.

POC

Add this to QuantAMMMomentum.t.sol and run it forge test --match-test testDenominatorValidationExploit -vvvv.

// Add constant ONE
int256 private constant ONE = 1e18; // Same value as in MomentumUpdateRule
function testDenominatorValidationExploit() public {
// Setup parameters with very small denominators
int256[][] memory parameters = new int256[][]();
parameters[0] = new int256[]();
parameters[0][0] = PRBMathSD59x18.fromInt(1); // kappa = 1
parameters[1] = new int256[]();
parameters[1][0] = ONE; // useRawPrice = true
int256[] memory previousAlphas = new int256[]();
previousAlphas[0] = PRBMathSD59x18.fromInt(1);
previousAlphas[1] = PRBMathSD59x18.fromInt(1);
int256[] memory prevMovingAverages = new int256[]();
prevMovingAverages[0] = 1; // Very small denominator
prevMovingAverages[1] = PRBMathSD59x18.fromInt(1);
int256[] memory movingAverages = new int256[]();
movingAverages[0] = 1; // Very small denominator
movingAverages[1] = PRBMathSD59x18.fromInt(1);
int128[] memory lambdas = new int128[]();
lambdas[0] = int128(0.7e18);
int256[] memory prevWeights = new int256[]();
prevWeights[0] = 0.5e18;
prevWeights[1] = 0.5e18;
int256[] memory data = new int256[]();
data[0] = 1; // Very small denominator
data[1] = PRBMathSD59x18.fromInt(1);
vm.startPrank(owner);
rule.initialisePoolRuleIntermediateValues(
address(mockPool),
movingAverages,
previousAlphas,
2
);
// Execute calculations with small denominators
rule.CalculateUnguardedWeights(
prevWeights,
data,
address(mockPool),
parameters,
lambdas,
movingAverages
);
vm.stopPrank();
// Verify results
int256[] memory resultWeights = rule.GetResultWeights();
// The weight becomes very large because the denominator is small
assertGt(resultWeights[0], 1e20); // The weight becomes very large
assertLt(resultWeights[1], -1e20); // Very large negative weight
}

Trace:

MockMomentumRule::GetResultWeights() [staticcall]
└─ ← [Return] [13500000000000000486500000000000000 [1.35e34], -13499999999999999486500000000000000 [-1.349e34]]

First weight: 1.35e34 (very large) and second weight: -1.349e34 (negative).

locals.newWeights[locals.i] = ONE.div(locals.denominator).mul(locals.newWeights[locals.i]);

This happens because of the very small denominator (1) in the calculation.

MockMomentumRule::CalculateUnguardedWeights(
[500000000000000000 [5e17], 500000000000000000 [5e17]], // prevWeights
[1, 1000000000000000000 [1e18]], // data with denominator = 1
...
)

The input that caused the problem.

VM::assertGt(13500000000000000486500000000000000 [1.35e34], 100000000000000000000 [1e20]) [staticcall]
VM::assertLt(-13499999999999999486500000000000000 [-1.349e34], -100000000000000000000 [-1e20]) [staticcall]

Verify that there is a problem.

Impact

  • Creates severe imbalance in pool assets

  • Very large/small weights can cause pool operation failure.

Tools Used

  • Manual review

  • Foundry

Recommendations

Implement denominator validation.

// Add minimum denominator constant
int256 private constant MIN_DENOMINATOR = 1e6;
// Add validation in _getWeights
function _getWeights(...) internal override returns (int256[] memory) {
...
locals.denominator = _poolParameters.movingAverage[locals.i];
if (locals.useRawPrice) {
locals.denominator = _data[locals.i];
}
// Add validation
require(locals.denominator >= MIN_DENOMINATOR, "Invalid denominator");
locals.newWeights[locals.i] = ONE.div(locals.denominator).mul(locals.newWeights[locals.i]);
...
}

Add weight result validation.

int256 private constant MAX_WEIGHT_VALUE = 1e20;
require(
locals.newWeights[locals.i] <= MAX_WEIGHT_VALUE &&
locals.newWeights[locals.i] >= -MAX_WEIGHT_VALUE,
"Weight exceeds limits"
);
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.

Give us feedback!