SUMMARY
The onSwap function https://github.com/Cyfrin/2024-12-quantamm/blob/a775db4273eb36e7b4536c5b60207c9f17541b92/pkg/pool-quantamm/contracts/QuantAMMWeightedPool.sol#L249 in QuantAMMWeightedPool allows an attacker to exploit static price due to a flaw in how weights are calculated to drain the pool.
This breaks the core TFMM principle of continuous weight adjustment
The current implementation creates a "frozen" state where weights become static which breaks the core TFMM principle of continuous weight adjustment, Instead of continuous adjustment, it gets stuck using old weight values Weight Interpolation freeze once `block.timestamp` passes `lastPossibleInterpolationTime`, the weights stop updating
The way weights are calculated during swaps, specifically when the lastPossibleInterpolationTime has been reached can be exploited, the swaps function calculates the output amount base on token balances and weights, the function adjusts token weights dynamically using interpolation based on the time since the last update. It distinguishes between tokens stored within the same index range to computes weights accordingly using _getNormalizedWeight https://github.com/Cyfrin/2024-12-quantamm/blob/a775db4273eb36e7b4536c5b60207c9f17541b92/pkg/pool-quantamm/contracts/QuantAMMWeightedPool.sol#L275C1-L277C3 and _getNormalisedWeightPair https://github.com/Cyfrin/2024-12-quantamm/blob/a775db4273eb36e7b4536c5b60207c9f17541b92/pkg/pool-quantamm/contracts/QuantAMMWeightedPool.sol#L266. this calculations is an correct implementation of the WeightedMath library. Any logical flaw in computeOutGivenExactIn or computeInGivenExactOut could lead to imbalanced pool states.
Here is how the error occur
The weights in the pool do not dynamically adjust according to the balances, once the lastPossibleInterpolationTime https://github.com/Cyfrin/2024-12-quantamm/blob/a775db4273eb36e7b4536c5b60207c9f17541b92/pkg/pool-quantamm/contracts/QuantAMMWeightedPool.sol#L262 has been reached, the pricing becomes out of sync with the current market conditions.
Once lastPossibleInterpolationTime is reached and no weight updates have been set, the timeSinceLastUpdate value becomes static and all swaps in the block use the same weights, regardless of the pool's balances, because in both EXACT_OUT and EXACT_IN https://github.com/Cyfrin/2024-12-quantamm/blob/a775db4273eb36e7b4536c5b60207c9f17541b92/pkg/pool-quantamm/contracts/QuantAMMWeightedPool.sol#L279C1-L282C14, the function only checks if amountGivenScaled18 does not exceed maxTradeSizeRatio to calculates the output amount using WeightedMath.computeOutGivenExactIn https://github.com/Cyfrin/2024-12-quantamm/blob/a775db4273eb36e7b4536c5b60207c9f17541b92/pkg/pool-quantamm/contracts/QuantAMMWeightedPool.sol#L284.
This is an incorrect implementation of WeightedMath library. Any logical flaw in computeOutGivenExactIn or computeInGivenExactOut https://github.com/Cyfrin/2024-12-quantamm/blob/a775db4273eb36e7b4536c5b60207c9f17541b92/pkg/pool-quantamm/contracts/QuantAMMWeightedPool.sol#L279C1-L282C14 could lead to imbalanced pool states, relying on the WeightedMath library for correctness is wrong, the onSwap function uses view functions to get the static weights, which means that the prices are static. the _calculateCurrentBlockWeight function https://github.com/Cyfrin/2024-12-quantamm/blob/a775db4273eb36e7b4536c5b60207c9f17541b92/pkg/pool-quantamm/contracts/QuantAMMWeightedPool.sol#L379 use to compute the normalized weight based on the token's weight, block multiplier, and time elapsed sincelast update. The block multiplier is obtained from the tokenWeights array by indexing into it with tokenIndex + tokensInTokenWeight, and timeSinceLastUpdate is use to calculate the interpolated value, so multiple swap within same block will have static values.
The primary feature of QuantAMM weighted pools is to periodically update weights based on various trading rules set during pool creation which means its a Liquidity Bootstrapping Pools (LBPs) https://docs.balancer.fi/concepts/explore-available-balancer-pools/liquidity-bootstrapping-pool.html
These weights are updated byUpdateWeightRunner contract https://github.com/Cyfrin/2024-12-quantamm/blob/main/pkg/pool-quantamm/contracts/UpdateWeightRunner.sol, which provides new weights and multipliers, to computes the lastTimestampThatInterpolationWorks, the time up to which the pool will interpolate weights, based on guardrails https://github.com/Cyfrin/2024-12-quantamm/blob/a775db4273eb36e7b4536c5b60207c9f17541b92/pkg/pool-quantamm/contracts/UpdateWeightRunner.sol#L530C1-L533C48 this value will be a future time, the key is that the block.timestamp is used to calculate time from when the _calculateMultiplerAndSetWeights was triggered by the UpdateWeightRunner.performUpdate function, and if that has not been called recently the value will be stale which lead to incorrect weight calculations during multiple swaps in same block, and Balancer v3 allow anyone to execue token swaps across multiple pools in a single transaction through Batch swaps which is described here in Multi-hop swaps: Batch Router.
Attack Scenario:
Take Flash Loan
Execute multiple swaps in a single transaction, ensuring each swap stays below the maxTradeSizeRatio to exploits the pool's imbalance as the weights remain static.
Repay the flash loan and retain the profit to drain the liquidity due to the static prices.
Proof of Concept: Add this test to pkg/pool-quantamm/test/foundry/QuantAMMWeightedPool2Token.t.sol
TRACE
Ran 1 test for test/foundry/QuantAMMWeightedPool2Token.t.sol:QuantAMMWeightedPool2TokenTest
[PASS] testSameBlockSwaps() (gas: 12599596)
Logs:
Amount out for swap 1 296293263168334860000
Amount out for swap 2 303682014650759192223
Amount out for swap 3 311331321648637882861
Amount out for swap 4 319253117717497526821
Amount out for swap 5 327460011298897813504
Amount out for swap 6 335965331295606543230
Amount out for swap 7 344783176235231146524
Amount out for swap 8 353928467345918219684
Amount out for swap 9 363417005900702243511
Amount out for swap 10 373265535223801452985
Traces:
Impact
Due to the fact that multiple trades can be performed with the same price, an attacker can drain the pool.
Because the UpdateWeightRunner may not update the weights immediately, the same exploit can be performed over multiple blocks until the weights are correctly updated, potentially draining the LP.
Introduce a mechanism similar to Balancer's lastChangeBlock to track when token's balance was last modified to be aware of recent changes in the pool's state, introduce limits on the number of swaps per block
Introduce a logic to calculate timeSinceLastUpdate every time in _calculateCurrentBlockWeight https://github.com/Cyfrin/2024-12-quantamm/blob/a775db4273eb36e7b4536c5b60207c9f17541b92/pkg/pool-quantamm/contracts/QuantAMMWeightedPool.sol#L370C1-L381C6 to calculates weights based on a fixed timeSinceLastUpdate
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.