QuantAMM

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

`QuantammGradientBasedRule::_calculateQuantAMMGradient` uses incorrect storage index when storing packed gradients, causing array out-of-bounds error during gradient unpacking and breaking weight updates for pools with 4+ tokens using vector lambda

Summary

The QuantAMMGradientBasedRule::_calculateQuantAMMGradient function incorrectly accesses intermediateGradientStates[_poolParameters.pool][i] using index i instead of locals.storageArrayIndex. This results in an array out-of-bound revert, disrupting the pool's weight update functionality for pools with 4 or more tokens that utilize vector lambdas

Vulnerability Details

In the function QuantammGradientBasedRule::_calculateQuantAMMGradient, the locals.intermediateGradientState is assigned to an int256[] array corresponding to the number of tokens in the pool after unpacking intermediateGradientStates[_poolParameters.pool].

During the loop for pools with more than 4 tokens and using vector lambdas (lambdas with length != 1), the function incorrectly uses the index i instead of locals.storageArrayIndex to store the packed gradient value. Since i increments by 2 in each loop, while locals.storageArrayIndex increments by 1, this causes an out-of-bounds revert.

function _calculateQuantAMMGradient(
...) {
...
// Assigned to int[] array corresponding to the number of tokens in the pool
@> locals.intermediateGradientState = _quantAMMUnpack128Array(
intermediateGradientStates[_poolParameters.pool],
_poolParameters.numberOfAssets
);
...
// It will cause out-of-bound error here using `i` as index
@> intermediateGradientStates[_poolParameters.pool][i] = _quantAMMPackTwo128(
locals.intermediateGradientState[i],
locals.secondIntermediateValue
);
unchecked {
@> i += 2; // @ i + 2 each loop
@> ++locals.storageArrayIndex; // @ only increase by 1 each loop
}
}
...

The pool's update weightage functionality is disrupted because the call reverts whenever the UpdateWeightRunner invokes UpdateRule::CalculateNewWeights() to compute new weights.

Proof of concept

Include this code block in UpdateWeightRunner.t.sol file. This issue affects any strategy which inherits `QuantAMMGradientBasedRule`. Using PowerChannel rule here as an example

// import PowerChannel rule
import "../../contracts/rules/PowerChannelUpdateRule.sol";
function testUpdateFailsWithFourTokensVectorLambda() public {
// Deploys `PowerChannel` strategy as an example, any strategy inherits `QuantAMMGradientBasedRule` contract will have this error.
PowerChannelUpdateRule rule = new PowerChannelUpdateRule(address(updateWeightRunner));
// 1. Setup pool with 4 tokens
int256[] memory initialWeights = new int256[]();
initialWeights[0] = 0.25e18;
initialWeights[1] = 0.25e18;
initialWeights[2] = 0.25e18;
initialWeights[3] = 0.25e18;
// Set initial weights
mockPool.setInitialWeights(initialWeights);
mockPool.setPoolRegistry(9);
// Set approval
vm.startPrank(owner);
updateWeightRunner.setApprovedActionsForPool(address(mockPool), 9);
vm.stopPrank();
// 2. Setup oracles
int216 fixedValue = 1000;
uint256 delay = 3600;
chainlinkOracle = deployOracle(fixedValue, delay);
chainlinkOracle1 = deployOracle(fixedValue, delay);
chainlinkOracle2 = deployOracle(fixedValue, delay);
chainlinkOracle3 = deployOracle(fixedValue, delay);
// Add oracles in updateWeightRunner
vm.startPrank(owner);
updateWeightRunner.addOracle(OracleWrapper(chainlinkOracle));
updateWeightRunner.addOracle(OracleWrapper(chainlinkOracle1));
updateWeightRunner.addOracle(OracleWrapper(chainlinkOracle2));
updateWeightRunner.addOracle(OracleWrapper(chainlinkOracle3));
vm.stopPrank();
// 3. Setup vector lambdas (one per token)
vm.startPrank(address(mockPool));
address[][] memory oracles = new address[][]();
oracles[0] = new address[]();
oracles[0][0] = address(chainlinkOracle);
oracles[1] = new address[]();
oracles[1][0] = address(chainlinkOracle1);
oracles[2] = new address[]();
oracles[2][0] = address(chainlinkOracle2);
oracles[3] = new address[]();
oracles[3][0] = address(chainlinkOracle3);
// Vector lambda (one per token)
uint64[] memory lambda = new uint64[]();
lambda[0] = 0.5e18;
lambda[1] = 0.5e18;
lambda[2] = 0.5e18;
lambda[3] = 0.5e18;
// Initialize rule with gradient values
int256[] memory initialGradients = new int256[]();
for (uint i = 0; i < 4; i++) {
initialGradients[i] = 1e18;
}
// Intialise pool intermediate values
rule.initialisePoolRuleIntermediateValues(address(mockPool), new int256[](4), initialGradients, 4);
// Update pool's rule in UpdateWeightRunner
updateWeightRunner.setRuleForPool(
IQuantAMMWeightedPool.PoolSettings({
assets: new IERC20[](0),
rule: IUpdateRule(rule),
oracles: oracles,
updateInterval: 1,
lambda: lambda, // Vector lambda
epsilonMax: 0.2e18,
absoluteWeightGuardRail: 0.2e18,
maxTradeSizeRatio: 0.2e18,
ruleParameters: new int256[][](),
poolManager: addr2
})
);
vm.stopPrank();
vm.startPrank(addr2);
// Forcefully adjust the last run time to activate an update
updateWeightRunner.InitialisePoolLastRunTime(address(mockPool), 1);
vm.stopPrank();
vm.warp(block.timestamp + 10000000);
@> updateWeightRunner.performUpdate(address(mockPool));
// [FAIL: panic: array out-of-bounds access (0x32)]
}

This test will fail due to the array out-of-bounds access error.

Impact

This issue disrupts the main functionality of updating token weightage in pool, making this pool essentially a static weightage pool, for the pool with 4 tokens or more and using vector lambdas. As a result, the pool cannot benefit from strategy-based updates to dynamically adjust weights.

Tools Used

  • Manual

  • Foundry

Recommendations

Change the index i to locals.storageArrayIndex

function _calculateQuantAMMGradient(
...) {
...
- intermediateGradientStates[_poolParameters.pool][i] = _quantAMMPackTwo128(
+intermediateGradientStates[_poolParameters.pool][locals.storageArrayIndex] = _quantAMMPackTwo128(
locals.intermediateGradientState[i],
locals.secondIntermediateValue
);
}
...
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!