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");
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 {
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) {
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) {
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 {
FuzzParams memory params;
params.numTokens = 2;
RuleFuzzParams memory ruleParams = RuleFuzzParams({
ruleType: 0,
kappa: _KAPPA
});
params.ruleParams = ruleParams;
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;
VariationTestVariables memory variables;
variables.params = _createPoolParams(params.numTokens, params.poolParams, params.ruleParams);
address quantAMMWeightedPool = quantAMMWeightedPoolFactory.createWithoutArgs(variables.params);
int256[] memory newWeights = new int256[]();
newWeights[0] = 0.6e18;
newWeights[1] = 0.5e18;
newWeights[2] = 0.001e18;
newWeights[3] = 0.001e18;
vm.prank(address(updateWeightRunner));
QuantAMMWeightedPool(quantAMMWeightedPool).setWeights(
newWeights,
quantAMMWeightedPool,
uint40(block.timestamp + 5)
);
vm.warp(block.timestamp + 2);
uint256[] memory weights = QuantAMMWeightedPool(quantAMMWeightedPool).getNormalizedWeights();
uint256 totalWeight = weights[0] + weights[1];
assertGt(totalWeight, 1e18, "Total weight should exceed 100%");
assertGt(weights[0], 0.6e18, "Weight 0 should increase");
assertGt(weights[1], 0.5e18, "Weight 1 should increase");
}
function testMinimumWeightVulnerability() public {
FuzzParams memory params;
params.numTokens = 2;
RuleFuzzParams memory ruleParams = RuleFuzzParams({
ruleType: 0,
kappa: _KAPPA
});
params.ruleParams = ruleParams;
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;
VariationTestVariables memory variables;
variables.params = _createPoolParams(params.numTokens, params.poolParams, params.ruleParams);
address quantAMMWeightedPool = quantAMMWeightedPoolFactory.createWithoutArgs(variables.params);
int256[] memory newWeights = new int256[]();
newWeights[0] = 0.99e18;
newWeights[1] = 0.01e18;
newWeights[2] = 0;
newWeights[3] = -0.001e18;
vm.prank(address(updateWeightRunner));
QuantAMMWeightedPool(quantAMMWeightedPool).setWeights(
newWeights,
quantAMMWeightedPool,
uint40(block.timestamp + 5)
);
vm.warp(block.timestamp + 2);
uint256[] memory weights = QuantAMMWeightedPool(quantAMMWeightedPool).getNormalizedWeights();
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
Tools Used
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");
for(uint i = 0; i < _totalTokens; i++) {
require(_weights[i] >= absoluteWeightGuardRail, "Weight below minimum");
}
int256 totalWeight = 0;
for(uint i = 0; i < _totalTokens; i++) {
totalWeight += _weights[i];
}
require(totalWeight == 1e18, "Total weight must be 100%");
}