QuantAMM

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

function _calculateQuantAMMGradient has the updated intermediate gradient states stored at incorrect positions in the intermediateGradientStates array.

Summary

https://github.com/Cyfrin/2024-12-quantamm/blob/a775db4273eb36e7b4536c5b60207c9f17541b92/pkg/pool-quantamm/contracts/rules/base/QuantammGradientBasedRule.sol#L141C17-L164C14

https://github.com/Cyfrin/2024-12-quantamm/blob/a775db4273eb36e7b4536c5b60207c9f17541b92/pkg/pool-quantamm/contracts/rules/base/QuantammGradientBasedRule.sol#L80C17-L101C14

The updated intermediate gradient states are stored at incorrect positions in the intermediateGradientStates array. This misalignment leads to corruption of the stored gradient states. Subsequent unpacking will yield incorrect values.

Vulnerability Details

https://github.com/Cyfrin/2024-12-quantamm/blob/a775db4273eb36e7b4536c5b60207c9f17541b92/pkg/pool-quantamm/contracts/rules/base/QuantammGradientBasedRule.sol#L141C17-L164C14

https://github.com/Cyfrin/2024-12-quantamm/blob/a775db4273eb36e7b4536c5b60207c9f17541b92/pkg/pool-quantamm/contracts/rules/base/QuantammGradientBasedRule.sol#L80C17-L101C14

In the _calculateQuantAMMGradient function, the code handles two scenarios:

  1. When the pool uses a scalar lambda (same lambda for all assets).

  2. When the pool uses per-asset lambdas (different lambda for each asset).

The function attempts to optimize gas usage by processing two assets at a time and packing their intermediate gradient states into a single storage slot using the _quantAMMPackTwo128 function.

However, in the per-asset lambda scenario (when _poolParameters.lambda.length > 1), there is a critical indexing error when storing the updated intermediate gradient states back into the intermediateGradientStates mapping.

Specific Issue in the Code:

In the else block (handling per-asset lambdas), the code for storing the packed intermediate gradient states is:

unchecked {
locals.secondIndex = i + 1;
convertedLambda = int256(_poolParameters.lambda[locals.secondIndex]);
oneMinusLambda = ONE - convertedLambda;
locals.mulFactor = oneMinusLambda.pow(THREE).div(convertedLambda);
}
locals.secondIntermediateValue =
convertedLambda.mul(locals.intermediateGradientState[locals.secondIndex]) +
(_newData[locals.secondIndex] - _poolParameters.movingAverage[locals.secondIndex]).div(
oneMinusLambda
);
locals.finalValues[locals.secondIndex] = locals.mulFactor.mul(locals.secondIntermediateValue);
intermediateGradientStates[_poolParameters.pool][i] = _quantAMMPackTwo128(
locals.intermediateGradientState[i],
locals.secondIntermediateValue
);
unchecked {
i += 2;
++locals.storageArrayIndex;
}
}

Here, i is used as the index into the intermediateGradientStates array. However, i increments by 2 in each iteration of the loop (since we process two assets at a time). This indexing is incorrect because the intermediateGradientStates array is intended to store packed gradient states, and its length should be approximately half of the number of assets (plus one if the number of assets is odd).

In the corresponding if block (handling scalar lambda), the correct index locals.storageArrayIndex is used:

unchecked {
locals.secondIndex = i + 1;
}
locals.secondIntermediateValue =
convertedLambda.mul(locals.intermediateGradientState[locals.secondIndex]) +
(_newData[locals.secondIndex] - _poolParameters.movingAverage[locals.secondIndex]).div(
oneMinusLambda
);
locals.finalValues[locals.secondIndex] = locals.mulFactor.mul(locals.secondIntermediateValue);
intermediateGradientStates[_poolParameters.pool][locals.storageArrayIndex] = _quantAMMPackTwo128(
locals.intermediateGradientState[i],
locals.secondIntermediateValue
);
// the storage array is tracked separately
unchecked {
i += 2;
++locals.storageArrayIndex;
}
}

The locals.storageArrayIndex is correctly incremented by 1 in each loop iteration, which aligns with the storage array's size.

The updated intermediate gradient states are stored at incorrect positions in the intermediateGradientStates array.

Example:

Suppose we have a pool with 5 assets (numberOfAssets = 5) and per-asset lambdas. During the loop:

  • First Iteration (i = 0):

    • Process assets 0 and 1.

    • Store the packed gradient state at index i = 0 (expected storage index should be 0).

  • Second Iteration (i = 2):

    • Process assets 2 and 3.

    • Store the packed gradient state at index i = 2 (expected storage index should be 1).

  • Outcome:

    • The storage indices 0 and 2 are used, leaving storage index 1 unused.

    • The gradient state of assets 2 and 3 is stored at the wrong index, causing misalignment.

    • Inconsistent storage leads to incorrect unpacking in future calculations.

Impact

Subsequent calls to _calculateQuantAMMGradient will use corrupted intermediate gradient states, leading to erroneous gradient computations and potential divergence from expected behavior.

Tools Used

Manual Review

Recommendations

In the per-asset lambda (else) block, the code should use locals.storageArrayIndex instead of i when storing the packed intermediate gradient states.

// Corrected indexing when storing packed values in the per-asset lambda case
intermediateGradientStates[_poolParameters.pool][locals.storageArrayIndex] = _quantAMMPackTwo128(
locals.intermediateGradientState[i],
locals.secondIntermediateValue
);

// Ensure storageArrayIndex is incremented appropriately
unchecked {
i += 2;
++locals.storageArrayIndex;
}

Updates

Lead Judging Commences

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

finding_gradient_rules_more_than_3_assets_and_1_lambda_will_DoS_the_update

Likelihood: Medium/High, assets>4, lambdas > 1. Impact: Medium/High, DoS update but pool works fine. Pool with 5 assets will use incorrect weights.

Support

FAQs

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

Give us feedback!