QuantAMM

QuantAMM
49,600 OP
View results
Submission Details
Severity: high
Valid

Out-of-Bounds Array Access in `_calculateQuantAMMVariance` with Odd Number of Assets and Vector Lambda

Summary

The _calculateQuantAMMVariance function in the QuantAMMVarianceBasedRule contract contains a critical vulnerability that leads to an out-of-bounds array access error when the following conditions are met: 1) the number of assets in the pool is odd, and 2) a vector of lambda values is used (i.e., _poolParameters.lambda.length > 1). This error occurs because the loop index locals.nMinusOne is not correctly decremented before the main loop when these conditions are satisfied. The vulnerability results in a denial-of-service (DoS) for a core functionality of the protocol: updating pool weights based on the Minimum Variance rule. The bug also makes the weight calculation fail with normal data when the above two conditions are satisfied, which will also impact the normal usage of this function.

Vulnerability Details

The vulnerability resides in the _calculateQuantAMMVariance function of the QuantAMMVarianceBasedRule contract (QuantammVarianceBasedRule.sol#L174), specifically in how the loop termination condition and array indices are handled when the number of assets is odd and a vector lambda is used.

The function has two main code paths: one for when _poolParameters.lambda.length == 1 (scalar lambda) and another for when _poolParameters.lambda.length > 1 (vector lambda). Within each path, there's a conditional check for locals.notDivisibleByTwo, indicating an odd number of assets.

In the scalar lambda case:

  1. locals.nMinusOne is decremented before the main loop, within the if (locals.notDivisibleByTwo) block (QuantammVarianceBasedRule.sol#L74).

  2. locals.nMinusOne is incremented after the loop, within the if (locals.notDivisibleByTwo) block (QuantammVarianceBasedRule.sol#L116).

In the vector lambda case:

  1. locals.nMinusOne is not decremented before the main loop (QuantammVarianceBasedRule.sol#L130).

  2. locals.nMinusOne is incremented after the loop, within the if (locals.notDivisibleByTwo) block (QuantammVarianceBasedRule.sol#L174).

function _calculateQuantAMMVariance(
int256[] memory _newData,
QuantAMMPoolParameters memory _poolParameters
) internal returns (int256[] memory) {
QuantAMMVarianceLocals memory locals;
@> locals.n = _poolParameters.numberOfAssets;
@> locals.finalState = new int256[](locals.n);
@> locals.intermediateVarianceState = _quantAMMUnpack128Array(
intermediateVarianceStates[_poolParameters.pool],
@> locals.n
);
@> locals.nMinusOne = locals.n - 1;
@> locals.notDivisibleByTwo = locals.n % 2 != 0;
locals.convertedLambda = int256(_poolParameters.lambda[0]);
locals.oneMinusLambda = ONE - locals.convertedLambda;
if (_poolParameters.lambda.length == 1) {
if (locals.notDivisibleByTwo) {
unchecked {
@> --locals.nMinusOne;
}
}
for (uint i; i < locals.nMinusOne; ) {
// update states ...
unchecked {
i += 2;
++locals.storageIndex;
}
}
if (locals.notDivisibleByTwo) {
unchecked {
@> ++locals.nMinusOne;
}
// update states ...
}
} else {
for (uint i; i < locals.nMinusOne; ) {
// update states ...
unchecked {
i += 2;
++locals.storageIndex;
}
}
if (locals.notDivisibleByTwo) {
unchecked {
@> ++locals.nMinusOne;
@> locals.convertedLambda = int256(_poolParameters.lambda[locals.nMinusOne]);
@> locals.oneMinusLambda = ONE - locals.convertedLambda;
}
locals.intermediateState =
@> locals.convertedLambda.mul(locals.intermediateVarianceState[locals.nMinusOne]) +
@> (_newData[locals.nMinusOne] - _poolParameters.movingAverage[locals.n + locals.nMinusOne])
@> .mul(_newData[locals.nMinusOne] - _poolParameters.movingAverage[locals.nMinusOne])
.div(TENPOWEIGHTEEN);
@> locals.intermediateVarianceState[locals.nMinusOne] = locals.intermediateState;
@> locals.finalState[locals.nMinusOne] = locals.oneMinusLambda.mul(locals.intermediateState);
@> intermediateVarianceStates[_poolParameters.pool][locals.storageIndex] = locals
@> .intermediateVarianceState[locals.nMinusOne];
}
}
return locals.finalState;
}

This discrepancy leads to the "Out-of-Bounds Access" issue when _poolParameters.lambda.length > 1 and the number of assets is odd: after the loop, when locals.notDivisibleByTwo is true, locals.nMinusOne is incremented, making it equal to the number of assets. Subsequent array accesses using this index (e.g., _poolParameters.lambda[locals.nMinusOne], _newData[locals.nMinusOne], _poolParameters.movingAverage[locals.n + locals.nMinusOne], locals.intermediateVarianceState[locals.nMinusOne]) will be out-of-bounds, as array indices are zero-based.

This issue causes the function to revert whenever the pool has an odd number of assets and uses a vector lambda, effectively breaking the minimum variance update rule under these conditions. The normal data that will cause this revert and the core functionality of updating weights will be disabled.

Impact

The vulnerability has a critical impact on the functionality of QuantAMM pools that utilize the minimum variance update rule with an odd number of assets and a vector lambda configuration. Specifically, it leads to the following:

  1. Denial of Service: The _calculateQuantAMMVariance function, which is crucial for calculating new weights based on the minimum variance rule, will consistently revert due to the out-of-bounds array access. This effectively prevents the pool from updating its weights, rendering the minimum variance strategy unusable under these conditions.

  2. Broken Core Functionality: Weight updates are a core component of the QuantAMM protocol. The inability to update weights based on the minimum variance rule disrupts the intended behavior of the pool and prevents it from adapting to market conditions as designed.

  3. Loss of LP Value: As a strategy pool, the QuantAMM pool is designed to rebalance the pool weights automatically to generate higher LP performance than standard CFMM pools, if the core strategy is broken, the LP value can no longer be guaranteed.

The provided Proof of Concept (PoC) test code, testVarianceOddAssetsAndVectorLambdaExpectRevert in QuantAMMVarianceTest, successfully demonstrates this vulnerability:

NOTE

please manually modify the test code before run the test, as to:

  • initialize int256[] memory priceData with length numAssets

  • initialize int256[] memory movingAverage with length numAssets * 2

  • initialize int256[] memory initialVariance with length numAssets

  • initialize int128[] memory lambda with length numAssets

since platform system will automatically overwrite these array length to empty when save the upload.

contract QuantAMMVarianceTest is Test, QuantAMMTestUtils {
// other test functions ...
// PoC test function
function testVarianceOddAssetsAndVectorLambdaExpectRevert() public {
// Initialize test data as to:
// 1. _poolParameters.lambda.length > 1
// 2. _poolParameters.numberOfAssets % 2 != 0
// (refer to the function `_calculateQuantAMMVariance` of the contract `QuantAMMVarianceBasedRule`)
uint256 numAssets = 5;
mockPool.setNumberOfAssets(numAssets);
int256[] memory priceData = new int256[]();
int256[] memory movingAverage = new int256[]();
int256[] memory initialVariance = new int256[]();
int128[] memory lambda = new int128[]();
for (uint256 i = 0; i < numAssets; i++) {
priceData[i] = PRBMathSD59x18.fromInt(1000);
movingAverage[i] = PRBMathSD59x18.fromInt(1000);
movingAverage[i + numAssets] = PRBMathSD59x18.fromInt(1000);
initialVariance[i] = 0;
lambda[i] = int128(uint128(0.5e18));
}
mockCalculationRule.setInitialVariance(address(mockPool), initialVariance, numAssets);
mockCalculationRule.setPrevMovingAverage(movingAverage);
// Call mock contract to test `QuantAMMVarianceBasedRule._calculateQuantAMMVariance`
// Odd assets and vector lambda should revert with the array out-of-bounds error
// Traces info will be included in the submission report
vm.expectRevert();
mockCalculationRule.externalCalculateQuantAMMVariance(
priceData, movingAverage, address(mockPool), lambda, numAssets
);
}
}
  • The test sets up the conditions for the vulnerability: an odd number of assets (numAssets = 5) and a vector lambda with length equal to the number of assets.

  • It then calls the vulnerable function externalCalculateQuantAMMVariance (which internally calls _calculateQuantAMMVariance) and asserts that the call will revert using vm.expectRevert().

The PoC test is executed with the specified command:

forge test --mc QuantAMMVarianceTest --mt testVarianceOddAssetsAndVectorLambdaExpectRevert -vvvv

The PoC test result confirms the expected revert with a panic: array out-of-bounds access (0x32) error, validating the presence of the vulnerability:

Ran 1 test for test/foundry/rules/base/Variance.t.sol:QuantAMMVarianceTest
[PASS] testVarianceOddAssetsAndVectorLambdaExpectRevert() (gas: 344214)
Traces:
[344214] QuantAMMVarianceTest::testVarianceOddAssetsAndVectorLambdaExpectRevert()
├─ [22355] MockPool::setNumberOfAssets(5)
│ └─ ← [Stop]
├─ [32608] MockCalculationRule::setInitialVariance(MockPool: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], [0, 0, 0, 0, 0], 5)
│ └─ ← [Stop]
├─ [245471] MockCalculationRule::setPrevMovingAverage([1000000000000000000000 [1e21], 1000000000000000000000 [1e21], 1000000000000000000000 [1e21], 1000000000000000000000 [1e21], 1000000000000000000000 [1e21], 1000000000000000000000 [1e21], 1000000000000000000000 [1e21], 1000000000000000000000 [1e21], 1000000000000000000000 [1e21], 1000000000000000000000 [1e21]])
│ └─ ← [Stop]
├─ [0] VM::expectRevert(custom error 0xf4844814)
│ └─ ← [Return]
├─ [21199] MockCalculationRule::externalCalculateQuantAMMVariance([1000000000000000000000 [1e21], 1000000000000000000000 [1e21], 1000000000000000000000 [1e21], 1000000000000000000000 [1e21], 1000000000000000000000 [1e21]], [1000000000000000000000 [1e21], 1000000000000000000000 [1e21], 1000000000000000000000 [1e21], 1000000000000000000000 [1e21], 1000000000000000000000 [1e21], 1000000000000000000000 [1e21], 1000000000000000000000 [1e21], 1000000000000000000000 [1e21], 1000000000000000000000 [1e21], 1000000000000000000000 [1e21]], MockPool: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], [500000000000000000 [5e17], 500000000000000000 [5e17], 500000000000000000 [5e17], 500000000000000000 [5e17], 500000000000000000 [5e17]], 5)
│ └─ ← [Revert] panic: array out-of-bounds access (0x32)
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 9.05ms (2.26ms CPU time)
Ran 1 test suite in 134.35ms (9.05ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

The inability to update weights based on the minimum variance rule under these conditions constitutes a high-severity issue as it breaks core protocol functionality and potentially impacts the intended behavior and performance of QuantAMM pools.

Tools Used

Manual Review

Recommendations

The core issue stems from the inconsistent handling of the locals.nMinusOne variable when dealing with an odd number of assets and a vector lambda. To resolve this, the following code change should be implemented:

Correctly Decrement locals.nMinusOne for Vector Lambda:

Inside the _calculateQuantAMMVariance function, within the else block (where _poolParameters.lambda.length > 1), add the following code snippet before the main loop, similar to how it is done in the scalar lambda (_poolParameters.lambda.length == 1) branch:

if (_poolParameters.lambda.length == 1) {
// update states ...
} else {
+ if (locals.notDivisibleByTwo) {
+ unchecked {
+ --locals.nMinusOne;
+ }
+ }
for (uint i; i < locals.nMinusOne; ) {
// update states ...
unchecked {
i += 2;
++locals.storageIndex;
}
}
if (locals.notDivisibleByTwo) {
unchecked {
++locals.nMinusOne;
locals.convertedLambda = int256(_poolParameters.lambda[locals.nMinusOne]);
locals.oneMinusLambda = ONE - locals.convertedLambda;
}
// update states ...
}
}

This change ensures that locals.nMinusOne is correctly decremented when the number of assets is odd, aligning the loop termination condition and index calculations with the intended logic. By doing so, the loop will iterate the correct number of times, and the subsequent accesses to array elements using locals.nMinusOne will remain within bounds.

This single modification effectively addresses the root cause of the out-of-bounds access and ensures the proper functioning of the minimum variance update rule when using a vector lambda with an odd number of assets.

Updates

Lead Judging Commences

n0kto Lead Judge 10 months ago
Submission Judgement Published
Validated
Assigned finding tags:

finding_calculateQuantAMMVariance_revert_when_vector_lambda_and_odd_asset_number

Likelihood: Medium/High, odd asset number + lambda is a vector. Impact: Medium/High, DoS the update.

Support

FAQs

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

Give us feedback!