QuantAMM

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

Array Index Out of Bounds in Weight Calculation

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) {
//get the index in the second storage int
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 {
// Setup test params
FuzzParams memory params;
params.numTokens = 8; // Test with max tokens to check array bounds
// 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);
// Test case 1: Cross-boundary weight access
int256[] memory newWeights = new int256[](); // 8 weights + 8 multipliers
for(uint i = 0; i < 8; i++) {
newWeights[i] = int256(0.125e18); // Equal weights
newWeights[i + 8] = 0; // No multipliers
}
// Set weights through update weight runner
vm.prank(address(updateWeightRunner));
QuantAMMWeightedPool(quantAMMWeightedPool).setWeights(
newWeights,
quantAMMWeightedPool,
uint40(block.timestamp + 5)
);
// Get normalized weights and verify array bounds
uint256[] memory weights = QuantAMMWeightedPool(quantAMMWeightedPool).getNormalizedWeights();
assertEq(weights.length, params.numTokens, "Incorrect weights array length");
// Test case 2: Swap with out-of-bounds indices
uint256[] memory balances = new uint256[]();
for(uint i = 0; i < params.numTokens; i++) {
balances[i] = 1000e18;
}
// Try swap with invalid indices
PoolSwapParams memory swapParams = PoolSwapParams({
kind: SwapKind.EXACT_IN,
amountGivenScaled18: 100e18,
balancesScaled18: balances,
indexIn: 0,
indexOut: params.numTokens, // Invalid index
router: address(router),
userData: abi.encode(0)
});
vm.prank(address(vault));
vm.expectRevert(); // Should revert on invalid index
QuantAMMWeightedPool(quantAMMWeightedPool).onSwap(swapParams);
// Test case 3: Weight interpolation with boundary indices
newWeights[7] = int256(0.3e18); // Set last weight higher
newWeights[15] = int256(0.001e18); // Set last multiplier
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, // <-- Index outside array bounds
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

  • Denial of service through transaction reverts when accessing weights for higher index tokens

  • Incorrect weight calculations affecting swap prices and pool balances

Tools Used

  • Manual review

  • Foundry

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;
// Handle first array (indices 0-3)
if (tokenIndex < 4) {
targetWrappedToken = _normalizedFirstFourWeights;
tokenIndexInPacked = Math.min(totalTokens, 4);
}
// Handle second array (indices 4-7)
else {
require(tokenIndex < 8, "Token index out of bounds");
index = tokenIndex - 4;
targetWrappedToken = _normalizedSecondFourWeights;
tokenIndexInPacked = totalTokens - 4;
}
// Additional validation
require(index < tokenIndexInPacked, "Index exceeds packed length");
// Rest of the function...
}
Updates

Lead Judging Commences

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

Informational or Gas / Admin is trusted / Pool creation is trusted / User mistake / Suppositions

Please read the CodeHawks documentation to know which submissions are valid. If you disagree, provide a coded PoC and explain the real likelyhood and the detailed impact on the mainnet without any supposition (if, it could, etc) to prove your point.

Support

FAQs

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

Give us feedback!