QuantAMM

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

MinimumVarianceUpdateRule will revert with array out-of-bounds exception

Summary

Incorrect index management in QuantAMMStorage._quantAMMUnpack128Arraywill cause MinimumVarianceUpdateRulewill always revert with array out-of-bounds exception

Vulnerability Details

Root Cause

Bug in QuantAMMStorage._quantAMMUnpack128Array

We have the following functionality in QuantAMMStorage._quantAMMUnpack128Array

function _quantAMMUnpack128Array(
int256[] memory _sourceArray,
uint _targetArrayLength
) internal pure returns (int256[] memory targetArray) {
require(_sourceArray.length * 2 >= _targetArrayLength, "SRC!=TGT");
targetArray = new int256[](_targetArrayLength);
uint targetIndex;
uint sourceArrayLengthMinusOne = _sourceArray.length - 1;
bool divisibleByTwo = _targetArrayLength % 2 == 0;
for (uint i; i < _sourceArray.length; ) {
targetArray[targetIndex] = _sourceArray[i] >> 128;
unchecked {
++targetIndex;
}
if ((!divisibleByTwo && i < sourceArrayLengthMinusOne) || divisibleByTwo) {
targetArray[targetIndex] = int256(int128(_sourceArray[i]));
}
unchecked {
++i;
++targetIndex;
}
}
if (!divisibleByTwo) {
targetArray[_targetArrayLength - 1] = int256(int128(_sourceArray[sourceArrayLengthMinusOne]));
}
}

It can be observed that indices (i and targetIndex) have the following properties:

  • For each iteration, i is incremented by one until _sourceArray.length

  • targetIndex is incremented by two (i.e. i * 2) and should have upper-bound _targetArrayLength

However, the upper bound for targetIndex is breached when _targetArrayLength < _sourceArray.length * 2.

In the most cases, we encode/decode full array, so targetArrayLength == ceil(sourceArray.length / 2) and the above condition is not met.

But in MinimumVarianceUpdateRule, _targetArrayLength < _sourceArray.length * 2. Let's see:

Bug is triggered in MinimumVarianceUpdateRule.CalculateNewWeights

MinimumVarianceUpdateRule and other UpdateRules quantum-packs and stores movingAverages when calculating new weights:

locals.currMovingAverage = _quantAMMUnpack128Array(movingAverages[_pool], locals.numberOfAssets);
// and after calculation
/**
* For other rules that doesn't require prev average
* movingAverages[_pool][i] = updatedMovingAverages[i]
*/
if (locals.requiresPrevAverage) {
movingAverages[_pool] = _quantAMMPack128Array(locals.calculationMovingAverage);
}

For MinimumVarianceUpdateRule, we require prev average, so movingAverages length is numberOfAssets * 2

locals.intermediateMovingAverageStateLength = locals.numberOfAssets;
if (locals.requiresPrevAverage) {
unchecked {
locals.intermediateMovingAverageStateLength *= 2;
}
}
locals.calculationMovingAverage = new int256[](locals.intermediateMovingAverageStateLength);

So what's happening here is:

  • movingAverages length (unpacked) is numberOfAssets * 2

    • Which implies, when packed, it will have sourceArrayLength = numberOfAssets * 2 / 2 = numberOfAssets

  • But when unpacking, targetArrayLength = numberOfAssets, so we have sourceArrayLength = numberOfAssets and targetArrayLength = numberOfAssets

  • So targetArrayLength < 2 * sourceArrayLength

This will cause index OOB error in weight calculation.

POC

Scenario:

  • MinimumVarianceUpdateRule for 2 assets and 4 initial moving averages will revert on first CalculateNewWeightsattempt

  • MinimumVarianceUpdateRule for 2 assets and 2 initial moving averages will revert on second CalculateNewWeights attempt

    • Note: According to QuantAMMMinVariance.t.sol, looks like 2 initial moving averages is not a standard practice, but this example will demonstrate how targetArrayLength < 2 * sourceArrayLengthis reached

      • Initially, sourceArrayLength = 1 (due to initial 2 moving averages), targetArrayLength = 2(numAssets)

      • After first calculation, sourceArrayLength = 2(unpacked movingAverageLength = 4 to record prev moving averages)

      • In the second calculation, sourceArrayLength = 2and targetArrayLength = 2

Create a file MinimumVarianceUpdateRule.t.solunder test/foundrydirectory and run forge test MinimumVarianceUpdateRule.t.sol -vvv

pragma solidity ^0.8.26;
import "forge-std/Test.sol";
import "../../contracts/rules/MinimumVarianceUpdateRule.sol";
contract MinimumVarianceUpdateRuleTesting is Test {
MinimumVarianceUpdateRule rule;
function setUp() external {
rule = new MinimumVarianceUpdateRule(address(this));
}
function testRevertWithOOBFourMovingAverages() external {
// Same parameters in QuantAMMMinVariance.t.sol
int256[][] memory parameters = new int256[][]();
parameters[0] = new int256[]();
parameters[0][0] = 0.9e18;
int256[] memory prevWeights = new int256[]();
prevWeights[0] = 0.5e18;
prevWeights[1] = 0.5e18;
int256[] memory data = new int256[]();
data[0] = PRBMathSD59x18.fromInt(3);
data[1] = PRBMathSD59x18.fromInt(4);
int256[] memory variances = new int256[]();
variances[0] = PRBMathSD59x18.fromInt(1);
variances[1] = PRBMathSD59x18.fromInt(1);
int256[] memory prevMovingAverages = new int256[]();
prevMovingAverages[0] = PRBMathSD59x18.fromInt(1);
prevMovingAverages[1] = PRBMathSD59x18.fromInt(1) + 0.5e18;
prevMovingAverages[2] = PRBMathSD59x18.fromInt(1);
prevMovingAverages[3] = PRBMathSD59x18.fromInt(1) + 0.5e18;
uint64[] memory lambda = new uint64[]();
lambda[0] = 0.95e18;
lambda[1] = 0.5e18;
uint64 epsilonMax = 0.2e18;
uint64 absoluteGuardRail = 0.2e18;
rule.initialisePoolRuleIntermediateValues(vm.addr(1), prevMovingAverages, variances, 2);
// 4-initial moving averages will revert on the first attempt
vm.expectRevert(stdError.indexOOBError);
rule.CalculateNewWeights(prevWeights, data, vm.addr(1), parameters, lambda, epsilonMax, absoluteGuardRail);
}
function testRevertWithOOBTwoMovingAverages() external {
// Same parameters in QuantAMMMinVariance.t.sol
int256[][] memory parameters = new int256[][]();
parameters[0] = new int256[]();
parameters[0][0] = 0.9e18;
int256[] memory prevWeights = new int256[]();
prevWeights[0] = 0.5e18;
prevWeights[1] = 0.5e18;
int256[] memory data = new int256[]();
data[0] = PRBMathSD59x18.fromInt(3);
data[1] = PRBMathSD59x18.fromInt(4);
int256[] memory variances = new int256[]();
variances[0] = PRBMathSD59x18.fromInt(1);
variances[1] = PRBMathSD59x18.fromInt(1);
int256[] memory prevMovingAverages = new int256[]();
prevMovingAverages[0] = PRBMathSD59x18.fromInt(1);
prevMovingAverages[1] = PRBMathSD59x18.fromInt(1) + 0.5e18;
uint64[] memory lambda = new uint64[]();
lambda[0] = 0.95e18;
lambda[1] = 0.5e18;
uint64 epsilonMax = 0.2e18;
uint64 absoluteGuardRail = 0.2e18;
rule.initialisePoolRuleIntermediateValues(vm.addr(1), prevMovingAverages, variances, 2);
// 2-initial moving averages will revert on the second attempt
rule.CalculateNewWeights(prevWeights, data, vm.addr(1), parameters, lambda, epsilonMax, absoluteGuardRail);
vm.expectRevert(stdError.indexOOBError);
rule.CalculateNewWeights(prevWeights, data, vm.addr(1), parameters, lambda, epsilonMax, absoluteGuardRail);
}
}

Impact

Pools with MinimumVarianceUpdateRule will not have their weights updated

Tools Used

Foundry, Python, Differential Testing

Recommendations

Two options:

  • In UpdateRule.sol, truncate movingAveragesup to numberOfAssetswhen requiresPrevAverageis true, or

  • In QuantAMMStorage._quantAMMUnpack128Array, use targetIndexas a loop variable, instead of i

Updates

Lead Judging Commences

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

finding_CalculateNewWeights_DoS_when_requiresPrevAverage_is_true

Likelihood: Medium, all rules using previous average. Impact: High, DoS CalculateNewWeights

Support

FAQs

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

Give us feedback!