Section 7 of 9

Build
+15 Shields

Time-Based Weight Adjustments

Time-Based Weight Adjustments

What You Are Building

The temporal function is the brain of the protocol. It takes price observations, smooths them with an exponential moving average, computes momentum signals, and outputs a new target weight vector. You will implement the four core primitives: EMA calculation, block multiplier computation, the momentum update rule, and guard rail hit detection.

In this section, you will implement:

  • Exponential Moving Average (EMA) for price smoothing
  • Block multiplier calculation for per-block weight interpolation
  • The momentum update rule: w_new = w_prev + kappa * (gradient - normFactor)
  • Last interpolation time calculation (when the first weight hits its guard rail)

EMA: Smoothing Price Noise

Raw price feeds are noisy. A single large trade can spike the price for one block, and if the temporal function reacts to that spike, it would shift weights based on noise rather than signal. The EMA smooths observations over time.

The formula:

newAvg = oldAvg * lambda + newPrice * (1 - lambda)

Here lambda controls the smoothing. A value of 0.94 means the EMA keeps 94% of its previous value and mixes in 6% of the new observation. Higher lambda means more smoothing (slower reaction). Lower lambda means faster reaction but more sensitivity to noise.

In fixed-point arithmetic (1e18 precision):

keepPortion = oldAvg * lambda / ONE
newPortion  = newPrice * (ONE - lambda) / ONE
newAvg      = keepPortion + newPortion

The Cyfrin audit of QuantAMM specifically examines the EMA implementation for precision loss, because small rounding errors compound over thousands of updates. In production, the lambda value is carefully chosen: too high and the pool never reacts to real trends; too low and it chases every noise spike.

Block Multipliers: Smooth Per-Block Changes

Once the temporal function computes a new target weight, the pool does not jump to it instantly. Instead, it computes a block multiplier:

multiplier = (targetWeight - currentWeight) / updateInterval

Between updates, the pool interpolates:

weight(block) = currentWeight + multiplier * (block - lastUpdateBlock)

This is a signed value (weights can decrease), stored as int256. The block multiplier tells the pool exactly how much each weight changes per block. At any given block, the pool can compute its current effective weights without reading any external data.

The Momentum Update Rule

QuantAMM's momentum strategy computes new weights like this:

  1. For each token, calculate the gradient (how far the current price is from the EMA):

    gradient_i = (price_i - priceEMA_i) / priceEMA_i
    

    A positive gradient means the price is above the moving average (upward momentum).

  2. Calculate the normalization factor (average gradient across all tokens):

    normFactor = sum(gradient_i) / numTokens
    

    Subtracting this ensures the total weight change sums to zero. Without it, momentum signals would inflate or deflate total weight, breaking the sum(weights) = 1 invariant.

  3. Apply the learning rate kappa:

    w_new_i = w_prev_i + kappa * (gradient_i - normFactor)
    

    Kappa controls how aggressively the pool reacts to momentum. A small kappa (0.001) means gentle weight shifts. A large kappa means aggressive rebalancing.

The key insight: the normFactor subtraction makes this a zero-sum game. If one token's weight increases, another's must decrease by the same total amount. This is mathematically necessary because weights must always sum to 1.

Update Intervals and Timing

Weight updates are not continuous. The updateInterval parameter sets the minimum number of blocks between updates. This serves two purposes:

  1. Gas efficiency: Computing new weights involves EMA updates, gradient calculations, normalization, clamping, and storage writes. Doing this every block would be prohibitively expensive. Typical intervals are 50 to 200 blocks.

  2. Manipulation resistance: An attacker who can trigger weight updates at will could time them to coincide with favorable price conditions. A minimum interval prevents rapid-fire updates.

Last Interpolation Time

Between updates, weights interpolate linearly via block multipliers. But this linear interpolation has a limit: at some point, a weight will hit its MIN_WEIGHT or MAX_WEIGHT bound. The calculateLastInterpolationBlock function finds exactly when this happens.

For each token with a nonzero block multiplier, calculate how many blocks until it hits the bound:

If multiplier > 0: blocksToMax = (MAX_WEIGHT - currentWeight) / multiplier
If multiplier < 0: blocksToMin = (currentWeight - MIN_WEIGHT) / abs(multiplier)

The minimum across all tokens gives the "last safe block." The pool must trigger a new weight update before reaching this block, or the interpolation would produce an out-of-bounds weight. In practice, keepers (or the next swap transaction) trigger the update well before this deadline.

Your Task

Implement all four TODOs in the starter code. The calculateEMA function is the foundation. The calculateBlockMultiplier enables smooth interpolation. The calculateMomentumWeights function ties together EMA, gradients, and kappa. Finally, calculateLastInterpolationBlock ensures the system knows when to trigger the next update.

Your Code

Solution.sol
Solidity
Loading editor...

Requirements

calculateEMA uses lambda for smoothing
calculateEMA blends old and new values
calculateEMA uses ONE for fixed-point division
calculateBlockMultiplier handles signed arithmetic
calculateBlockMultiplier divides by blocksRemaining
calculateMomentumWeights computes gradient from price vs EMA
calculateMomentumWeights applies normFactor
calculateMomentumWeights uses kappa learning rate
calculateLastInterpolationBlock checks guard rail bounds
calculateLastInterpolationBlock finds minimum across all tokens
triggerUpdate enforces updateInterval