Summary
UpdateRules based on GradientBasedRulewill revert/produce incorrect result when if numAssets >= 4 and lambda.length > 1. It's due to a simple mistake in updating intermediateGradientStates.
Vulnerability Details
Root Cause
We have the following in QuantammGradientBasedRule.sol:156
intermediateGradientStates[_poolParameters.pool][i] = _quantAMMPackTwo128(
locals.intermediateGradientState[i],
locals.secondIntermediateValue
);
unchecked {
i += 2;
++locals.storageArrayIndex;
}
IntermediateGradientStates is a mapping to store quantum packed gradient values. If something is quantum packed, it means it will shrink rougly by half in size.
So IntermediateGradientStates[pool].length <= ceil(numAssets / 2)
In the above codebase, i is incrased up to numAssets - 2. So if numAssets >= 4, intermediateGradientStates[_poolParameters.pool][i] will panic with array out of bound error (2 > 1)
Indeed, i should be replaced by locals.storageArrayIndex
PS: Interestingly, for numAssets = 5, array OOB error is not observed, because storage length is ceil(5/2) = 3and iis increased up to 3. So there won't be OOB error, but due to the wrong usage of storage index, intermediate values will be wrong.
POC
Put the following content to QuantAMMMomentum.sol and run forge test QuantAMMMomentum --match-test testRevertWithFourAssetsAndVectorLambdas -vvv
function testRevertWithFourAssetsAndVectorLambdas() public {
uint256 numAssets = 4;
int256[][] memory parameters = new int256[][]();
parameters[0] = new int256[]();
parameters[0][0] = PRBMathSD59x18.fromInt(1);
parameters[1] = new int256[]();
parameters[1][0] = PRBMathSD59x18.fromInt(1);
parameters[2] = new int256[]();
parameters[2][0] = PRBMathSD59x18.fromInt(1);
parameters[3] = new int256[]();
parameters[3][0] = PRBMathSD59x18.fromInt(1);
int256[] memory previousAlphas = new int256[]();
previousAlphas[0] = PRBMathSD59x18.fromInt(1);
previousAlphas[1] = PRBMathSD59x18.fromInt(2);
previousAlphas[2] = PRBMathSD59x18.fromInt(3);
int256[] memory prevMovingAverages = new int256[]();
int256[] memory movingAverages = new int256[]();
movingAverages[0] = 0.9e18;
int128[] memory lambdas = new int128[]();
lambdas[0] = int128(0.7e18);
lambdas[1] = int128(0.7e18);
lambdas[2] = int128(0.7e18);
lambdas[3] = int128(0.7e18);
int256[] memory prevWeights = new int256[]();
prevWeights[0] = 0.2e18;
prevWeights[1] = 0.2e18;
prevWeights[2] = 0.2e18;
prevWeights[3] = 0.2e18;
int256[] memory data = new int256[]();
int256[] memory expectedResults = new int256[]();
* vm.expectRevert doesn't work for internal test functions, so just copied runInitialUpdate and inserted vm.expectRevert
*/
mockPool.setNumberOfAssets(numAssets);
vm.startPrank(owner);
rule.initialisePoolRuleIntermediateValues(address(mockPool), prevMovingAverages, previousAlphas, numAssets);
vm.expectRevert(stdError.indexOOBError);
rule.CalculateUnguardedWeights(prevWeights, data, address(mockPool), parameters, lambdas, movingAverages);
vm.stopPrank();
}
Impact
All gradient based rules (Antimomentum, ChannelFollowing, DifferenceMomentum, PowerChannel etc) won't work with >=4 assets and lambdas parameters
If numAssets is 4, 6, 8, and lambdas.length > 1, update weights will revert with index out of bound error
For numAssets 5, 7 and lambdas.length > 1, weights calculation will be incorrect because wrong index is used for intermediate value storage
Tools Used
Foundry
Recommendations
Apply the following patch
diff --git a/pkg/pool-quantamm/contracts/rules/base/QuantammGradientBasedRule.sol b/pkg/pool-quantamm/contracts/rules/base/QuantammGradientBasedRule.sol
index e6bbcdc..f1f6d9f 100644
--- a/pkg/pool-quantamm/contracts/rules/base/QuantammGradientBasedRule.sol
+++ b/pkg/pool-quantamm/contracts/rules/base/QuantammGradientBasedRule.sol
@@ -153,7 +153,7 @@ abstract contract QuantAMMGradientBasedRule is ScalarRuleQuantAMMStorage {
locals.finalValues[locals.secondIndex] = locals.mulFactor.mul(locals.secondIntermediateValue);
- intermediateGradientStates[_poolParameters.pool][i] = _quantAMMPackTwo128(
+ intermediateGradientStates[_poolParameters.pool][locals.storageArrayIndex] = _quantAMMPackTwo128(
locals.intermediateGradientState[i],
locals.secondIntermediateValue
);