QuantAMM

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

Rules that require previous average will suffer a permanent DoS of updates from the `UpdateWeighRunner`

Summary

Rules that require the previous average will suffer a permanent DoS upon performing any update

Vulnerability Details

When a pool is created, the _setInitialMovingAverages is executed with _initialMovingAverages having the double of elements as tokens registered in the pool. We can see that in the tests for the only rule that require previous averages: QuantAMMMinVariance.t.sol

function testCorrectUpdateWithLambdaPointFiveAndTwoWeights() public {
// Set the number of assets to 2
mockPool.setNumberOfAssets(2);
...
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;
int128[] memory lambda = new int128[]();
lambda[0] = 0.7e18;
// Initialize pool rule intermediate values
rule.initialisePoolRuleIntermediateValues(
address(mockPool),
prevMovingAverages,
variances,
mockPool.numAssets()
);
...
}

In this test we can see that there are 2 tokens in the pool and the movingAverages will store 4 elements, the double.
The function that stores the data in the movingAverages storage variable is the following:

function _setInitialMovingAverages(
address _poolAddress,
int256[] memory _initialMovingAverages,
uint _numberOfAssets
) internal {
uint movingAverageLength = movingAverages[_poolAddress].length;
if (movingAverageLength == 0 || _initialMovingAverages.length == _numberOfAssets) {
//should be during create pool
movingAverages[_poolAddress] = _quantAMMPack128Array(_initialMovingAverages);
} else {
revert("Invalid set moving avg");
}
}

This function will essentially pack 2 subsequent elements into a single slot and then create an array of these packed slots.

This data is accessed and modified during CalculateNewWeights function that is executed during each update from the updateWeightRunner.

function CalculateNewWeights(
int256[] calldata _prevWeights,
int256[] calldata _data,
address _pool,
int256[][] calldata _parameters,
uint64[] calldata _lambdaStore,
uint64 _epsilonMax,
uint64 _absoluteWeightGuardRail
) external returns (int256[] memory updatedWeights) {
...
locals.currMovingAverage = _quantAMMUnpack128Array(movingAverages[_pool], locals.numberOfAssets);
...
}

In it, we can see that to retrieve the data from the movingAverages storage variable, it uses the _quantAMMUnpack128Array by passing the number of assets of the pool.
However, let's see what happens when there is the double of data stored:

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;
}
}
...
}

Continuing with the previous example of the test where there are 2 tokens in the pool and 4 moving averages stored, the array of moving averages would look like this:

0 1
[ 1e18 | 1.5e18, 1e18 | 1.5e18]

In this function the new targetArray will be of 2 elements.
The loop will sweep with i values 0, 1 because the _sourceArray has also 2 elements.
In the first iteration when i starts being 0, it assigns the targetArray[0] to 1e18 and targetArray[1] to 1.5e18.
However, in the second iteration, targetIndex is 2 so it tries to set targetArray[2] but is out of bounds, so it reverts

Proof of Concept

  1. A new pool gets created with a rule that requires the previous averages and initialisePoolRuleIntermediateValues gets called
    The _setInitialMovingAverages saves the array of int256 that contain the double of moving averages of int128.

  2. When the updateWeightRunner will try to perform an update, the CalculateNewWeights function will be called

  3. The update will fail because the _quantAMMUnpack128Array function will fail due to an out of bounds error as explained above

Coded Proof of Concept

This bug would have been caught simply by calling the CalculateNewWeights function in any of the tests, however in all of them it uses a custom function called CalculateUnguardedWeights that does not revert but it is only implemented in the mock implementation.
As an example this test uses the same values as testCorrectUpdateWithLambdaPointFiveAndTwoWeights but it simply calls the CalculateNewWeights function:

function testPOCDoSWhenCalculateWeights() public {
// Set the number of assets to 2
mockPool.setNumberOfAssets(2);
// Define parameters and inputs
int256[][] memory parameters = new int256[][]();
parameters[0] = new int256[]();
parameters[0][0] = 0.5e18;
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.7e18;
uint64 epsilonMax = 0.5e18;
uint64 absoluteWeightGuardRail = 0.4e18;
// Initialize pool rule intermediate values
rule.initialisePoolRuleIntermediateValues(
address(mockPool),
prevMovingAverages,
variances,
mockPool.numAssets()
);
vm.expectRevert();
rule.CalculateNewWeights(prevWeights, data, address(mockPool), parameters, lambda, epsilonMax, absoluteWeightGuardRail);
}

The result of this test is a failure with an out of bound error.

[Revert] panic: array out-of-bounds access (0x32)

However, if the previous moving averages would not have the double of elements just the same as the tokens in the pool it does not revert:

function testPOCWorksWhenNoDuplicatedArrays() public {
// Set the number of assets to 2
mockPool.setNumberOfAssets(2);
// Define parameters and inputs
int256[][] memory parameters = new int256[][]();
parameters[0] = new int256[]();
parameters[0][0] = 0.5e18;
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);
uint64[] memory lambda = new uint64[]();
lambda[0] = 0.7e18;
uint64 epsilonMax = 0.5e18;
uint64 absoluteWeightGuardRail = 0.4e18;
// Initialize pool rule intermediate values
rule.initialisePoolRuleIntermediateValues(
address(mockPool),
prevMovingAverages,
variances,
mockPool.numAssets()
);
rule.CalculateNewWeights(prevWeights, data, address(mockPool), parameters, lambda, epsilonMax, absoluteWeightGuardRail);
}

This test passes successfully

Impact

High, the pool will not be able to get any update

Tools Used

Manual review

Recommendations

Adjust the _quantAMMUnpack128Array such that when the targetArray gets filled, just return it instead of continuing the loop.

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.