QuantAMM

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

Time Variable Race Condition in Weight Calculations

Summary

A race condition vulnerability exists in the QuantAMMWeightedPool contract where weight calculations can be manipulated during weight updates due to non-atomic loading of timing variables. This vulnerability could allow attackers to profit from inconsistent weight calculations through sandwich attacks.

Description

The heart of the vulnerability lies in how the computeBalance function handles timing variables for weight calculations. Instead of loading timing variables atomically (in a single storage read), it performs two separate storage reads which can become inconsistent during weight updates:

function computeBalance(
uint256[] memory balancesLiveScaled18,
uint256 tokenInIndex,
uint256 invariantRatio
) external view returns (uint256 newBalance) {
// First critical storage read
uint40 lastInterpolationTime = poolSettings.quantAMMBaseInterpolationDetails.lastPossibleInterpolationTime;
// Time calculation using the first read
if (block.timestamp >= lastInterpolationTime) {
multiplierTime = lastInterpolationTime;
}
// Second critical storage read - creates vulnerable window
uint256 timeSinceLastUpdate = uint256(
multiplierTime - poolSettings.quantAMMBaseInterpolationDetails.lastUpdateIntervalTime
);
// Weight calculation using potentially inconsistent timing variables
return WeightedMath.computeBalanceOutGivenInvariant(
balancesLiveScaled18[tokenInIndex],
_getNormalizedWeight(tokenInIndex, timeSinceLastUpdate, _totalTokens),
invariantRatio
);
}

This implementation contrasts sharply with the more secure approach used in the onSwap function, which loads timing variables atomically:

// Secure atomic loading of variables in onSwap
QuantAMMBaseInterpolationVariables memory variables = poolSettings.quantAMMBaseInterpolationDetails;
uint40 multiplierTime = uint40(block.timestamp);
if (block.timestamp >= variables.lastPossibleInterpolationTime) {
multiplierTime = variables.lastPossibleInterpolationTime;
}
uint256 timeSinceLastUpdate = uint256(multiplierTime - variables.lastUpdateIntervalTime);

The vulnerability can be exploited through a sophisticated sandwich attack that takes advantage of the timing inconsistency. Here's a detailed breakdown of the attack process:

Preparation Phase

  • Attacker monitors the mempool for weight update transactions

  • They analyze the current pool state and calculate optimal attack parameters

  • Prepare front-running and back-running transactions

Attack Execution

  1. Front-running (Pre-update):

    • Deploy transaction with higher gas price than weight update

    • Execute swap using old, known weights

    • Transaction must complete before weight update

  2. Weight Update Occurs:

    • Pool contract updates weights and timing variables

    • Creates vulnerable window for exploitation

  3. Back-running (Exploitation):

    • Execute second transaction immediately after weight update

    • Exploit calculation discrepancy during vulnerable window

    • Profit from temporary mispricing

Lets try this with an example:

Initial Pool Configuration:

Pool Parameters:
- Liquidity: 1000 ETH and 1000 USDC
- ETH Market Price: $2000
- Initial ETH Weight: 50%
- Target ETH Weight: 45%
- Weight Change Multiplier: -0.0001
- Update Interval: 500 blocks

Front-run Transaction (T1):

Initial State:
- lastUpdateIntervalTime: 1000
- lastPossibleInterpolationTime: 2000
- Current Weight: 50%
Action:
- Swap 100 ETH → USDC
- Calculation: 100 ETH * $2000 * 0.5 = ~200,000 USDC
- Transaction Cost: ~0.1 ETH (high gas price for front-running)

Weight Update Occurs (T2):

New State:
- lastUpdateIntervalTime: Updates to 1500
- lastPossibleInterpolationTime: Updates to 2500
- Base Weight: Updates to 45%

Back-run Transaction (T3) - Exploiting Calculation Error:

During Vulnerable Window:
- Reads new lastPossibleInterpolationTime (2500)
- But reads old lastUpdateIntervalTime (1000)
- Incorrect timeSinceLastUpdate = 1500 - 1000 = 500
- Erroneous Weight Calculation: 45% + (-0.0001 * 500) = 40%
Action:
- Swap 200,000 USDC → ETH
- Gets ~105 ETH due to manipulated weight

Net Attack Results:

Initial: 100 ETH
Final: 105 ETH
Gross Profit: 5 ETH (~$10,000)
- Gas Costs: ~0.3 ETH
Net Profit: 4.7 ETH (~$9,400)

Impact

  • Direct profit opportunity for attackers

  • Proportional loss for liquidity providers

  • Scales with pool size and liquidity

  • Affects core pool mechanics

  • Compromises weight adjustment mechanism

  • Creates systemic instability in pool operations

Recommendation

Mitigation for this weight calculation race condition is to modify the computeBalance function to load all timing variables atomically using a single struct read, mirroring the safer implementation found in the onSwap function.

Updates

Lead Judging Commences

n0kto Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

Informational or Gas / Admin is trusted / Pool creation is trusted / User mistake / Suppositions

Please read the CodeHawks documentation to know which submissions are valid. If you disagree, provide a coded PoC and explain the real likelyhood and the detailed impact on the mainnet without any supposition (if, it could, etc) to prove your point.

Support

FAQs

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

Give us feedback!