QuantAMM

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

Weights May Not Sum to One at a Given Block, Breaking Pool Invariant and Violating Assumptions

Summary:

In the QuantAMMWeightedPool contract, the dynamic calculation of weights via the calculateBlockNormalisedWeight function does not ensure that the sum of normalized weights equals one (in 18 decimal fixed-point format) at every block. This oversight can lead to situations where, at a specific block, the weights do not sum to one, violating the assumption that "At any given block the pool is a fixed weighted balancer pool." This may result in incorrect swap calculations, deviation from expected pool behavior, and potential financial discrepancies for users.


Root Cause:

The calculateBlockNormalisedWeight function computes each token's weight individually based on the last known weight, a multiplier, and the elapsed time since the last update. However, it does not include a normalization step to adjust all weights in proportion so that their sum equals one after the individual weights have been updated. Over time, due to rounding errors or cumulative adjustments, the total weight may drift away from the expected total of FixedPoint.ONE (1e18), causing the pool to violate the fixed-sum weight invariant.

Vulnerable Code:

In QuantAMMWeightedPool.sol:

function calculateBlockNormalisedWeight(
int256 weight,
int256 multiplier,
uint256 timeSinceLastUpdate
) internal pure returns (uint256) {
int256 multiplierScaled18 = multiplier * 1e18;
if (multiplier > 0) {
return uint256(weight) + FixedPoint.mulDown(uint256(multiplierScaled18), timeSinceLastUpdate);
} else {
return uint256(weight) - FixedPoint.mulUp(uint256(-multiplierScaled18), timeSinceLastUpdate);
}
}

In _getNormalizedWeights function, after calculating individual weights, there is no normalization step:

function _getNormalizedWeights() internal view virtual returns (uint256[] memory) {
// ...
// Individual weights are calculated using calculateBlockNormalisedWeight
// ...
// No code to adjust weights so that their sum equals FixedPoint.ONE (1e18)
return normalizedWeights;
}

Attack Path:

  1. Weight Drift Over Time:

    • Due to the absence of normalization, individual weights may increase or decrease based on multipliers and time, causing the total weight sum to deviate from one.

    • Small rounding errors can accumulate, especially over prolonged periods or with frequent updates.

  2. Invariant Violation:

    • At a given block, the weights do not sum to one, causing the pool to no longer represent a proper weighted balancer pool.

    • This violates the fundamental assumption that "At any given block the pool is a fixed weighted balancer pool."

  3. Incorrect Swap Calculations:

    • Swap functions rely on correct weights summing to one to compute exchange rates accurately.

    • Deviations can lead to incorrect exchange rates, unfair trades, and potential financial loss for users or arbitrage opportunities for attackers.

  4. Potential for Exploitation:

    • Malicious actors might exploit the deviation to perform arbitrage trades.

    • Users may receive less value than expected when swapping or withdrawing liquidity.


Proof of Concept (PoC):

Consider a pool with two tokens, where initial weights are both 0.5e18 (representing 50% each), summing to 1e18.

Assume that due to multipliers and elapsed time, the weights adjust as follows:

  • After Updates:

    • Token A weight: 0.5000001e18

    • Token B weight: 0.5000001e18

  • Total Weight Sum: 1.0000002e18

The total weights now exceed 1e18. This leads to the pool invariant being broken.

Alternatively, if weights adjust to:

  • After Updates:

    • Token A weight: 0.499999e18

    • Token B weight: 0.499999e18

  • Total Weight Sum: 0.999998e18

The total weights are now less than 1e18, again violating the pool's invariant.

This discrepancy can cause the pool to calculate incorrect prices for swaps, leading to users receiving incorrect amounts.


Recommendation:

Implement a normalization step after calculating the adjusted weights to ensure that the sum of all weights equals FixedPoint.ONE (1e18). This can be done by calculating the total weight sum of the individually adjusted weights and then scaling each weight proportionally.

Modified Code:

Update the _getNormalizedWeights function as follows:

function _getNormalizedWeights() internal view virtual returns (uint256[] memory) {
uint256 totalTokens = _totalTokens;
uint256[] memory normalizedWeights = new uint256[]();
uint256 totalWeight;
uint40 multiplierTime = uint40(block.timestamp);
uint40 lastInterpolationTime = poolSettings.quantAMMBaseInterpolationDetails.lastPossibleInterpolationTime;
if (block.timestamp >= lastInterpolationTime) {
multiplierTime = lastInterpolationTime;
}
uint256 timeSinceLastUpdate = uint256(
multiplierTime - poolSettings.quantAMMBaseInterpolationDetails.lastUpdateIntervalTime
);
// Calculate individual weights
for (uint i = 0; i < totalTokens; i++) {
// ... existing logic to calculate normalizedWeights[i] ...
normalizedWeights[i] = _calculateCurrentBlockWeightForIndex(i, timeSinceLastUpdate);
totalWeight += normalizedWeights[i];
}
// Normalize weights to ensure they sum to FixedPoint.ONE
if (totalWeight != FixedPoint.ONE) {
for (uint i = 0; i < totalTokens; i++) {
normalizedWeights[i] = normalizedWeights[i].mulDown(FixedPoint.ONE).divDown(totalWeight);
}
}
return normalizedWeights;
}
function _calculateCurrentBlockWeightForIndex(uint256 index, uint256 timeSinceLastUpdate) internal view returns (uint256) {
// Logic to calculate the current block weight for a specific index
// This function can wrap the logic from existing code for individual weight calculation
}

By normalizing the weights after calculation, we ensure that at every block, the sum of the weights is exactly 1e18

Updates

Lead Judging Commences

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

invalid_sum_of_weights_can_exceeds_one_no_guard

According the sponsor and my understanding, sum of weights does not have to be exactly 1 to work fine. So no real impact here. Please provide a PoC showing a realistic impact if you disagree. This PoC cannot contains negative weights because they will be guarded per clampWeights.

Support

FAQs

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

Give us feedback!