Section 9 of 10
Multi-Asset Pool + Guard Rails
Multi-Asset Pool + Guard Rails
What You Are Building
With weight storage and the temporal function engine in place, you will now build the full multi-asset pool. This contract handles swaps across 2 to 8 tokens using a weighted product invariant with time-dependent weights. You will also implement the three-level guard rail system that protects LPs from manipulation.
In this section, you will implement:
- Weighted product invariant:
product(balance_i ^ weight_i) = k onSwap()using time-dependent weights from the interpolation engine- Velocity constraint (epsilon max) that scales all weight deltas proportionally
- Trade size limit to prevent draining reserves in a single swap
- The full
performUpdate()flow: prices to weights to multipliers
The Weighted Product Invariant
The pool invariant generalizes the constant product formula. For N tokens with weights w_i:
product(balance_i ^ w_i) = k
When all weights are equal (1/N each), this reduces to the standard constant product. When weights differ, the pool favors holding more of higher-weighted tokens.
Computing balance ^ weight requires fractional exponents in fixed-point arithmetic. The standard approach (used by Balancer V2 and QuantAMM) is the log-exp identity:
x^y = exp(y * ln(x))
This converts the power function into a natural log and an exponential, both of which can be computed with Taylor series. Precision matters enormously: small rounding errors in the power function compound across 8 tokens and thousands of swaps. The Cyfrin audit of QuantAMM devotes significant attention to the precision bounds of these mathematical operations.
For this teaching implementation, you will use a simplified _pow() helper. It is less precise than the production LogExpMath library, but demonstrates the concept clearly.
Swap Math with Dynamic Weights
The weighted product swap formula gives the output amount for a given input:
amountOut = balanceOut * (1 - (balanceIn / (balanceIn + amountIn)) ^ (wIn / wOut))
The exponent wIn / wOut is what makes this different from constant product. When both weights are 0.5, the exponent is 1.0, and the formula reduces to x * y = k swap math. When weights differ, the pool prices tokens asymmetrically.
Critically, the weights used in this formula come from _getCurrentWeights(), which interpolates between previous and target weights based on the current block number. This means the pool's pricing changes smoothly over time, even between weight updates.
The Three Levels of Guard Rails
QuantAMM implements three distinct protection mechanisms, each operating at a different layer:
Level 1: Absolute Weight Guard Rail. Applied during performUpdate(). No weight can move more than absoluteWeightGuardRail (e.g., 5%) from its previous value in a single update. Even if the momentum signal says to shift from 30% to 60%, the clamped result would be 35%. This prevents sudden portfolio rebalancing that would create large arbitrage opportunities.
Level 2: Velocity Constraint (Epsilon Max). Also applied during performUpdate(). This limits the per-block rate of weight change. Even after absolute clamping, if the resulting block multiplier would change weights too fast, all multipliers are scaled proportionally. The key design decision: scale ALL deltas, not just the offending one. This preserves the relative direction of the weight update vector. If you only clamp the largest delta, you distort the intended rebalancing direction.
Level 3: Trade Size Limit. Applied in onSwap(). No single trade can swap more than maxTradeSizeRatio (e.g., 30%) of a token's reserves. This prevents an attacker from draining a token's reserves in a single transaction, which would amplify the price impact of the swap. Combined with the weight interpolation, this makes multi-block manipulation strategies significantly harder.
Multi-Block MEV Protection
Traditional MEV attacks (sandwiching, frontrunning) target a single transaction. Temporal function AMMs face an additional threat: multi-block MEV. An attacker who controls several consecutive blocks could:
- Manipulate the price feed used by the temporal function
- Trigger a weight update based on the manipulated price
- Trade at the resulting distorted prices
- Restore the price after profiting
The three guard rail levels work together to mitigate this. The absolute guard rail limits how much damage a single manipulated update can do. The velocity constraint limits how fast the manipulation takes effect. And the trade size limit prevents extracting the full value of the distortion in a single trade.
Block-by-block interpolation is the final defense. Even if an attacker triggers a favorable weight update, the weights change gradually over the update interval. The attacker cannot profit from the full weight shift in a single block.
The performUpdate() Flow
The full update flow ties everything together:
- Fetch prices from the oracle for all tokens
- Compute raw target weights using the temporal function (momentum, mean reversion, etc.)
- Apply absolute guard rail: clamp each weight within the guard rail distance from its previous value
- Normalize: ensure the clamped weights sum to 1e18
- Calculate block multipliers: signed per-block weight changes
- Apply velocity constraint: scale all multipliers if any exceeds epsilon max
- Store: update prevWeights, targetWeights, multipliers, lastUpdateBlock, targetBlock
Your Task
Implement all four TODOs in the starter code. calculateInvariant provides the weighted product math. onSwap uses interpolated weights for pricing and enforces the trade size limit. applyVelocityConstraint implements Level 2 protection. performUpdate orchestrates the full update flow with all three guard rail levels.