Summary
There is an issue in the QuantAMMWeightedPool contract where malicious manipulation of the weight vector index during weight updates can lead to incorrect weight calculations and pool imbalances.
Vulnerability Details
https://github.com/Cyfrin/2024-12-quantamm/blob/a775db4273eb36e7b4536c5b60207c9f17541b92/pkg/pool-quantamm/contracts/QuantAMMWeightedPool.sol#L722
The issue lies in the logic of handling weight vectors in the _splitWeightAndMultipliers function in QuantAMMWeightedPool. The function incorrectly processes weight indices when splitting weights and multipliers for pools with more than 4 tokens.
function _splitWeightAndMultipliers(
int256[] memory weights
) internal pure returns (int256[][] memory splitWeights) {
uint256 tokenLength = weights.length / 2;
splitWeights = new int256[][]();
splitWeights[0] = new int256[]();
for (uint i; i < 4; ) {
splitWeights[0][i] = weights[i];
splitWeights[0][i + 4] = weights[i + tokenLength];
unchecked { i++; }
}
splitWeights[1] = new int256[]();
uint256 moreThan4Tokens = tokenLength - 4;
for (uint i = 0; i < moreThan4Tokens; ) {
uint256 i4 = i + 4;
splitWeights[1][i] = weights[i4];
splitWeights[1][i + moreThan4Tokens] = weights[i4 + tokenLength];
unchecked { i++; }
}
}
The root of the problem is no validation on tokenLength that allows index manipulation, inflexible hardcoded array size 8 assumption, no validation that moreThan4Tokens is not negative. This function is used in setWeights which is the main entry point for updating pool weights.
POC
Add this to QuantAMMWeightedPoolGenericFuzzer.t.sol and run it forge test --match-test testSplitWeightsIndexManipulationVulnerability -vvvv.
function testSplitWeightsIndexManipulationVulnerability() public {
FuzzParams memory params;
params.numTokens = 8;
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 maliciousWeights = new int256[]();
for (uint i = 0; i < 8; i++) {
maliciousWeights[i] = int256(i);
maliciousWeights[i + 8] = -1;
}
vm.prank(address(updateWeightRunner));
QuantAMMWeightedPool(quantAMMWeightedPool).setWeights(
maliciousWeights,
quantAMMWeightedPool,
uint40(block.timestamp + 5)
);
}
Trace:
├─ [18798] QuantAMMWeightedPool::setWeights([0, 1, 2, 3, 4, 5, 6, 7, -1, -1, -1, -1, -1, -1, -1, -1], QuantAMMWeightedPool: [0xBAF398386F89157d964dD7AEf40179BA536E1725], 1682899205 [1.682e9])
Here we can see that the weights array that was sent has negative values (-1) at index 8-15. This should not be valid because weights cannot be negative.
├─ emit WeightsUpdated(poolAddress: QuantAMMWeightedPool: [0xBAF398386F89157d964dD7AEf40179BA536E1725], weights: [0, 1, 2, 3, 4, 5, 6, 7, -1, -1, -1, -1, -1, -1, -1, -1])
The WeightsUpdated event successfully diets with the weights array containing negative values.
The transaction was successfully executed (empty return) without any revert, even though this transaction should have failed.
Impact
Tools Used
Recommendations
Implement proper bounds checking.
function _splitWeightAndMultipliers(
int256[] memory weights
) internal pure returns (int256[][] memory splitWeights) {
uint256 tokenLength = weights.length / 2;
require(tokenLength <= 8, "Max 8 tokens supported");
splitWeights = new int256[][]();
splitWeights[0] = new int256[]();
uint256 firstHalfTokens = tokenLength < 4 ? tokenLength : 4;
for (uint i; i < firstHalfTokens;) {
splitWeights[0][i] = weights[i];
splitWeights[0][i + 4] = weights[i + tokenLength];
unchecked { i++; }
}
if (tokenLength > 4) {
splitWeights[1] = new int256[]();
uint256 remainingTokens = tokenLength - 4;
for (uint i = 0; i < remainingTokens;) {
splitWeights[1][i] = weights[i + 4];
splitWeights[1][i + 4] = weights[i + 4 + tokenLength];
unchecked { i++; }
}
}
int256 totalWeight = 0;
for (uint i = 0; i < tokenLength;) {
totalWeight += splitWeights[i < 4 ? 0 : 1][i % 4];
unchecked { i++; }
}
require(totalWeight == 1e18, "Invalid total weight");
}