QuantAMM

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

Missing Weight Validation Controls in QuantAMMWeightedPool

Summary

The setWeights function in QuantAMMWeightedPool lacks critical validation controls for minimum weight values and total weight sum, allowing weights to be set below minimum thresholds and with incorrect total weight sums.

Vulnerability Details

https://github.com/Cyfrin/2024-12-quantamm/blob/a775db4273eb36e7b4536c5b60207c9f17541b92/pkg/pool-quantamm/contracts/QuantAMMWeightedPool.sol#L617

function setWeights(
int256[] calldata _weights,
address _poolAddress,
uint40 _lastInterpolationTimePossible
) external override {
require(msg.sender == address(updateWeightRunner), "ONLYUPDW");
require(_weights.length == _totalTokens * 2, "WLDL");
// No validation total weight = 1e18
// There is no minimum weight validation
if (_weights.length > 8) {
int256[][] memory splitWeights = _splitWeightAndMultipliers(_weights);
_normalizedFirstFourWeights = quantAMMPack32Array(splitWeights[0])[0];
_normalizedSecondFourWeights = quantAMMPack32Array(splitWeights[1])[0];
} else {
_normalizedFirstFourWeights = quantAMMPack32Array(_weights)[0];
}
poolSettings.quantAMMBaseInterpolationDetails = QuantAMMBaseInterpolationVariables({
lastPossibleInterpolationTime: _lastInterpolationTimePossible,
lastUpdateIntervalTime: uint40(block.timestamp)
});
emit WeightsUpdated(_poolAddress, _weights);
}
function _calculateMultiplerAndSetWeights(CalculateMuliplierAndSetWeightsLocal memory local) internal {
// There is no validation that the sum of the weights after interpolation remains 1 (100%)
for (uint i; i < local.currentWeights.length; ) {
targetWeightsAndBlockMultiplier[i] = local.currentWeights[i];
int256 blockMultiplier = (local.updatedWeights[i] - local.currentWeights[i]) / local.updateInterval;
targetWeightsAndBlockMultiplier[i + local.currentWeights.length] = blockMultiplier;
// ...
}
}
function calculateBlockNormalisedWeight(
int256 weight,
int256 multiplier,
uint256 timeSinceLastUpdate
) internal pure returns (uint256) {
// There is no validation that the interpolation result does not exceed 100%
int256 multiplierScaled18 = multiplier * 1e18;
if (multiplier > 0) {
return uint256(weight) + FixedPoint.mulDown(uint256(multiplierScaled18), timeSinceLastUpdate);
} else {
return uint256(weight) - FixedPoint.mulUp(uint256(-multiplierScaled18), timeSinceLastUpdate);
}
}
function _getNormalizedWeight(
uint256 tokenIndex,
uint256 timeSinceLastUpdate,
uint256 totalTokens
) internal view virtual returns (uint256) {
// There is no validation of the total weight sum after interpolation.
return _calculateCurrentBlockWeight(
unwrappedWeightsAndMultipliers,
index,
timeSinceLastUpdate,
tokenIndexInPacked
);
}

The root of the problem is no validation of total weight = 1e18, no validation of minimum weight, and edge cases for 2 tokens. Which can cause weight to be < expected minimum, total weight can be != 1e18 after interpolation, and can cause inaccurate swap and liquidity calculations. In addition, there is no validation that the sum of weights after interpolation remains 1 (100%) in the _calculateMultiplerAndSetWeights function, there is no validation that the interpolation result does not exceed 100% in the calculateBlockNormalisedWeight function, there is no validation of the total sum of weights after interpolation in the _getNormalizedWeight function.

POC

Add this to QuantAMMWeightedPoolGenericFuzzer.t.sol and run it forge test --match-test "testWeightValidationVulnerability|testMinimumWeightVulnerability" -vvvv.

function testWeightValidationVulnerability() public {
// Setup test params
FuzzParams memory params;
params.numTokens = 2; // Test dengan 2 token untuk edge case
// Construct momentum rule
RuleFuzzParams memory ruleParams = RuleFuzzParams({
ruleType: 0,
kappa: _KAPPA
});
params.ruleParams = ruleParams;
// Set pool params
params.poolParams.epsilonMax = _EPSILON_MAX;
params.poolParams.lambda = _LAMBDA;
params.poolParams.maxSwapfee = _MAX_SWAP_FEE_PERCENTAGE;
params.poolParams.absoluteWeightGuardRail = _ABSOLUTE_WEIGHT_GUARD_RAIL;
params.poolParams.maxTradeSizeRatio = _MAX_TRADE_SIZE_RATIO;
params.poolParams.updateInterval = _UPDATE_INTERVAL;
// Create pool
VariationTestVariables memory variables;
variables.params = _createPoolParams(params.numTokens, params.poolParams, params.ruleParams);
address quantAMMWeightedPool = quantAMMWeightedPoolFactory.createWithoutArgs(variables.params);
// Set initial weights that will cause total > 1e18 after interpolation
int256[] memory newWeights = new int256[]();
newWeights[0] = 0.6e18; // First weight 60%
newWeights[1] = 0.5e18; // Second weight 50%
newWeights[2] = 0.001e18; // Small positive multiplier
newWeights[3] = 0.001e18; // Small positive multiplier
// Set weights through update weight runner
vm.prank(address(updateWeightRunner));
QuantAMMWeightedPool(quantAMMWeightedPool).setWeights(
newWeights,
quantAMMWeightedPool,
uint40(block.timestamp + 5)
);
// Move time forward to trigger interpolation
vm.warp(block.timestamp + 2);
// Get interpolated weights
uint256[] memory weights = QuantAMMWeightedPool(quantAMMWeightedPool).getNormalizedWeights();
// Calculate total weight after interpolation
uint256 totalWeight = weights[0] + weights[1];
// Verify total weight exceeds 1e18 (100%)
assertGt(totalWeight, 1e18, "Total weight should exceed 100%");
// Verify individual weights
assertGt(weights[0], 0.6e18, "Weight 0 should increase");
assertGt(weights[1], 0.5e18, "Weight 1 should increase");
}
function testMinimumWeightVulnerability() public {
// Setup test params
FuzzParams memory params;
params.numTokens = 2;
RuleFuzzParams memory ruleParams = RuleFuzzParams({
ruleType: 0,
kappa: _KAPPA
});
params.ruleParams = ruleParams;
// Set pool params
params.poolParams.epsilonMax = _EPSILON_MAX;
params.poolParams.lambda = _LAMBDA;
params.poolParams.maxSwapfee = _MAX_SWAP_FEE_PERCENTAGE;
params.poolParams.absoluteWeightGuardRail = _ABSOLUTE_WEIGHT_GUARD_RAIL;
params.poolParams.maxTradeSizeRatio = _MAX_TRADE_SIZE_RATIO;
params.poolParams.updateInterval = _UPDATE_INTERVAL;
// Create pool
VariationTestVariables memory variables;
variables.params = _createPoolParams(params.numTokens, params.poolParams, params.ruleParams);
address quantAMMWeightedPool = quantAMMWeightedPoolFactory.createWithoutArgs(variables.params);
// Set weights where one will go below minimum after interpolation
int256[] memory newWeights = new int256[]();
newWeights[0] = 0.99e18; // First weight 99%
newWeights[1] = 0.01e18; // Second weight 1% (at minimum)
newWeights[2] = 0; // No change for first weight
newWeights[3] = -0.001e18; // Negative multiplier will push below minimum
vm.prank(address(updateWeightRunner));
QuantAMMWeightedPool(quantAMMWeightedPool).setWeights(
newWeights,
quantAMMWeightedPool,
uint40(block.timestamp + 5)
);
vm.warp(block.timestamp + 2);
uint256[] memory weights = QuantAMMWeightedPool(quantAMMWeightedPool).getNormalizedWeights();
// Verify second weight goes below minimum
assertLt(weights[1], _ABSOLUTE_WEIGHT_GUARD_RAIL, "Weight should drop below minimum");
}

Trace:

testMinimumWeightVulnerability

├─ QuantAMMWeightedPool::setWeights([990000000000000000 [9.9e17], 10000000000000000 [1e16], 0, -1000000000000000 [-1e15]], ...)
├─ QuantAMMWeightedPool::getNormalizedWeights() [staticcall]
│ └─ ← [990000000000000000 [9.9e17], 8000000000000000 [8e15]]

The second weight drops to 8e15 (0.8%) which is below the minimum weight which should be 1e16 (1%). This is because of the negative multiplier (-1e15) used to lower the weight below the minimum limit.

testWeightValidationVulnerability

├─ QuantAMMWeightedPool::setWeights([600000000000000000 [6e17], 500000000000000000 [5e17], 1000000000000000 [1e15], 1000000000000000 [1e15]], ...)
├─ QuantAMMWeightedPool::getNormalizedWeights() [staticcall]
│ └─ ← [602000000000000000 [6.02e17], 502000000000000000 [5.02e17]]
├─ VM::assertGt(1104000000000000000 [1.104e18], 1000000000000000000 [1e18], "Total weight should exceed 100%")

The total weight becomes 110.4% (1.104e18) which exceeds 100% (1e18). This is because of the positive multiplier (1e15) which increases the weight to exceed the maximum limit of 100%.

Impact

  • Weights below minimum threshold can cause calculation errors and economic vulnerabilities

  • Incorrect total weight sums break core pool invariants

Tools Used

  • Manual review

  • Foundry

Recommendations

Add validation in setWeights.

function setWeights(
int256[] calldata _weights,
address _poolAddress,
uint40 _lastInterpolationTimePossible
) external override {
require(msg.sender == address(updateWeightRunner), "ONLYUPDW");
require(_weights.length == _totalTokens * 2, "WLDL");
// Validate minimum weights
for(uint i = 0; i < _totalTokens; i++) {
require(_weights[i] >= absoluteWeightGuardRail, "Weight below minimum");
}
// Validate total weight
int256 totalWeight = 0;
for(uint i = 0; i < _totalTokens; i++) {
totalWeight += _weights[i];
}
require(totalWeight == 1e18, "Total weight must be 100%");
// Rest of function...
}
Updates

Lead Judging Commences

n0kto Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

invalid_sum_of_weights_can_exceeds_one_no_guard

According the sponsor and my understanding, sum of weights does not have to be exactly 1 to work fine. So no real impact here. Please provide a PoC showing a realistic impact if you disagree. This PoC cannot contains negative weights because they will be guarded per clampWeights.

Support

FAQs

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

Give us feedback!