QuantAMM

QuantAMM
49,600 OP
View results
Submission Details
Severity: medium
Invalid

Weight Update Overshooting After Interval Time

Summary

The weight adjustment mechanism in the pool has a potential issue when updates do not occur exactly after the updateInterval. If weights are not updated exactly at the updateInterval, they may move away from the intended target values. This deviation creates an arbitrage opportunity for users and could result in financial losses for the protocol.

Vulnerability Details

The weight adjustment logic relies on updates occurring at regular intervals (updateInterval). When weights are updated precisely at updateInterval, they correctly converge toward the target values. If there is a delay in updating the weights, the pool weights can diverge significantly from the target weights. This is especially problematic when:

  • The lastInterpolationTime exceeds the updateInterval.

  • The multiplier for weight adjustments is large.

When these conditions occur, the weights can deviate substantially, creating opportunities for arbitrage until the weights are updated again.

multiplierTime is adjusted to lastInterpolationTime if the current timestamp exceeds it. However, no similar check is applied for the updateInterval. This omission allows multiplierTime to extend beyond the updateInterval, leading to incorrect weight calculations.

/// @notice gets the normalised weights for the pool
function _getNormalizedWeights() internal view virtual returns (uint256[] memory) {
uint256 totalTokens = _totalTokens;
uint256[] memory normalizedWeights = new uint256[]();
uint40 multiplierTime = uint40(block.timestamp);
uint40 lastInterpolationTime = poolSettings.quantAMMBaseInterpolationDetails.lastPossibleInterpolationTime;
if (block.timestamp >= lastInterpolationTime) {
//we have gone beyond the first variable hitting the guard rail. We cannot interpolate any further and an update is needed
multiplierTime = lastInterpolationTime;
}

https://github.com/Cyfrin/2024-12-quantamm/blob/a775db4273eb36e7b4536c5b60207c9f17541b92/pkg/pool-quantamm/contracts/QuantAMMWeightedPool.sol#L429

Test:

contract QuantAMMWeightedPoolWeightTest is QuantAMMWeightedPoolContractsDeployer, BaseVaultTest {
using CastingHelpers for address[];
using ArrayHelpers for *;
uint256 internal daiIdx;
uint256 internal usdcIdx;
MockChainlinkOracle chainlinkOracle1;
MockIdentityRule mockRule;
// Maximum swap fee of 10%
uint64 public constant MAX_SWAP_FEE_PERCENTAGE = 10e16;
QuantAMMWeightedPoolFactory internal quantAMMWeightedPoolFactory;
function setUp() public override {
int216 fixedValue = 1000;
uint delay = 3600;
super.setUp();
(address ownerLocal, address addr1Local, address addr2Local) = (vm.addr(1), vm.addr(2), vm.addr(3));
owner = ownerLocal;
addr1 = addr1Local;
addr2 = addr2Local;
// Deploy UpdateWeightRunner contract
vm.startPrank(owner);
updateWeightRunner = new MockUpdateWeightRunner(owner, addr2, false);
chainlinkOracle = _deployOracle(fixedValue, delay);
updateWeightRunner.addOracle(OracleWrapper(chainlinkOracle));
int216 fixedValue1 = 1000;
int216 fixedValue2 = 1001;
chainlinkOracle1 = new MockChainlinkOracle(fixedValue1, 0);
updateWeightRunner.addOracle(chainlinkOracle1);
mockRule = new MockIdentityRule();
vm.stopPrank();
quantAMMWeightedPoolFactory = deployQuantAMMWeightedPoolFactory(
IVault(address(vault)),
365 days,
"Factory v1",
"Pool v1"
);
vm.label(address(quantAMMWeightedPoolFactory), "quantamm weighted pool factory");
(daiIdx, usdcIdx) = getSortedIndexes(address(dai), address(usdc));
}
function test_WeightAfterIntervalPassed() public {
uint16 updateInterval = 10;
QuantAMMWeightedPoolFactory.NewPoolParams memory params = _createPoolParams(updateInterval);
(address quantAMMWeightedPool, ) = quantAMMWeightedPoolFactory.create(params);
vm.startPrank(owner);
updateWeightRunner.setApprovedActionsForPool(address(quantAMMWeightedPool), 9);
vm.stopPrank();
int256[] memory initialWeights = new int256[]();
initialWeights[0] = 0.5e18;
initialWeights[1] = 0.5e18;
initialWeights[2] = 0;
initialWeights[3] = 0;
// Set initial weights
mockRule.setWeights(initialWeights);
updateWeightRunner.performUpdate(address(quantAMMWeightedPool));
uint256[] memory calcWeights2 = QuantAMMWeightedPool(quantAMMWeightedPool).getNormalizedWeights();
vm.warp(block.timestamp + updateInterval);
//@audit new target weights
int256[] memory targetWeights = new int256[]();
targetWeights[0] = 0.7e18;
targetWeights[1] = 0.3e18;
mockRule.setWeights(targetWeights);
updateWeightRunner.performUpdate(address(quantAMMWeightedPool));
vm.warp(block.timestamp + updateInterval);
// @audit If weights are checked after only the updateInterval has passed,
// the weights will be close to the target values.
uint256[] memory normalizedWeights = QuantAMMWeightedPool(quantAMMWeightedPool).getNormalizedWeights();
assertEq(normalizedWeights[0] == uint256(targetWeights[0]), true);
assertEq(normalizedWeights[1] == uint256(targetWeights[1]), true);
//@audit if the update is delayed, the weights may move away from the target values, overshooting
vm.warp(block.timestamp + 1);
uint256[] memory normalizedWeights2 = QuantAMMWeightedPool(quantAMMWeightedPool).getNormalizedWeights();
assertEq(normalizedWeights2[0] == 0.72e18, true);
assertEq(normalizedWeights2[1] == 0.28e18, true);
}
function _createPoolParams(
uint16 updateInterval
) internal returns (QuantAMMWeightedPoolFactory.NewPoolParams memory retParams) {
PoolRoleAccounts memory roleAccounts;
IERC20[] memory tokens = [address(dai), address(usdc)].toMemoryArray().asIERC20();
uint32[] memory weights = new uint32[]();
weights[0] = uint32(uint256(0.5e18));
weights[1] = uint32(uint256(0.5e18));
int256[] memory initialWeights = new int256[]();
initialWeights[0] = 0.5e18;
initialWeights[1] = 0.5e18;
uint256[] memory initialWeightsUint = new uint256[]();
initialWeightsUint[0] = 0.5e18;
initialWeightsUint[1] = 0.5e18;
uint64[] memory lambdas = new uint64[]();
lambdas[0] = 0.0000000005e18;
address[][] memory oracles = new address[][]();
oracles[0] = new address[]();
oracles[0][0] = address(chainlinkOracle1);
retParams = QuantAMMWeightedPoolFactory.NewPoolParams(
"Pool With Donation",
"PwD",
vault.buildTokenConfig(tokens),
initialWeightsUint,
roleAccounts,
MAX_SWAP_FEE_PERCENTAGE,
address(0),
true,
false, // Do not disable unbalanced add/remove liquidity
ZERO_BYTES32,
initialWeights,
IQuantAMMWeightedPool.PoolSettings(
new IERC20[](2),
IUpdateRule(mockRule),
oracles,
updateInterval,
lambdas,
0.2e18,
0.2e18,
0.2e18,
new int256[][](),
address(0)
),
initialWeights,
initialWeights,
3600,
0,
new string[][]()
);
}
}

Impact

Deviations from target weights can cause imbalances in the pool and create arbitrage oppurtunities

Tools Used

Manual

Recommendations

Enforce Interval Check: Introduce a constraint to ensure multiplierTime does not exceed the updateInterval. This will prevent weights from overshooting their targets.

Updates

Lead Judging Commences

n0kto Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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

Give us feedback!