myelin
Robinhood Chain
Docs: How the fee is set

How the fee is set

Four steps, all inside the swap transaction: remember, read, decide, remember again. No data comes from outside the pool.

The loop

  1. Before the swap (beforeSwap): the hook turns the pool’s memory and the block’s timestamp into eight integers.
  2. The model reads the eight integers and returns one number: how far the price is likely to move in the next minute.
  3. The policy turns that number into a fee between a floor and a cap, and the hook sets it for this swap, setting aside the Treasury’s share.
  4. After the swap (afterSwap): if the price really moved, the hook folds the move (the new price, the time that passed) into its memory. Either way it emits an event with everything it used, and mints the Treasury’s share.

1. What the pool remembers

The memory of a pool is one storage slot of 208 bits. From it, and from the current block’s timestamp, the hook computes eight integers. Nothing else is used: no price feed, no off-chain data and not the size of your swap.

InputNameWhat it measures
a_lastLast move|Δtick| of the last observation, capped at 4096. 0 until there is one.
ewFastFast averageMoving average of |Δtick| over observations, weight 1/4 each. Reacts within a few observations.
ewMidMedium averageThe same average with weight 1/16. The rule-based baseline reads only this one.
ewSlowSlow averageThe same average with weight 1/64: the long-run level of movement.
ewSignedDriftMoving average of the signed Δtick (weight 1/4): which way the price is leaning.
streakStreakConsecutive observations that moved the price the same way, from 0 to 16.
dtNowQuiet timeBits in the seconds since the last observation, at the current block: 0 for none, 1 for one second, 2 for 2–3 s, up to 12 for 34 minutes or more.
dtEwmaMove tempoMoving average of the seconds between observations (weight 1/8).

The averages are exponential moving averages with whole-number arithmetic, in fixed point with eight fractional bits (so a stored 256 means 1.0 tick). Each observation updates them with one shift, ew ← ew + ((x·256 − ew) >> k), which is why the state machine fits in a slot and costs little gas.

When the memory changes

Not on every swap. After a swap the hook compares the pool’s tick with the tick of the last observation. If they are less than 3 ticks apart, or the block is not later than the last observation’s, nothing is written, not even a timestamp: the swap is not an observation. Otherwise the move since the last observation (which may span several swaps) and the time since it are folded into the averages, and this swap becomes the last observation.

So a swap of one wei, a flicker between two adjacent ticks and a burst of swaps inside one second leave the memory as it was, and small moves add up until they reach 3 ticks. At most one observation happens per second, and each needs a real move of the price of more than 2 ticks. The two clocks (dtNow and dtEwma) run from the last observation, not from the last swap.

Why it ignores small moves

The first version of the memory counted every swap. A swap of one wei moves neither the price nor the tick, yet it aged the memory by a whole step: 64 of them in one second took a fee from 0.1011% to the 0.4127% cap, for the price of a wei and some gas. Now the cheapest way found to push the fee to its cap takes a median of 68 ticks of real price movement over about 21 seconds (see Risks). The price of this: in a deep pool most swaps are not observations, so the model sees the pool through fewer events.

2. What the model says

The model is a neural network: 8 → 16 → 8 → 1, 289 parameters, whole numbers only. Its output pred is ln(1 + movement) × 256, where movement is the total tick movement it expects over the next 60 seconds. So pred = 521 means about e^(521/256) − 1 ≈ 6.7 ticks, and pred = 0 means no movement. More about it on The model.

3. What the policy does

The policy is a floor, a slope above a threshold, and a cap. In whole numbers, with fees in hundredths of a basis point (so 3,000 is 0.30%):

fee = clamp( feeMin + ((slope × max(0, pred − threshold)) >> slopeShift), feeMin, feeMax )

These are the numbers this deployment uses. They are immutable values of the hook; nobody can change them.

ParameterValueMeaning
feeMin516 (0.0516%)The floor: what a calm pool charges.
feeMax4127 (0.4127%)The cap: no fee is ever higher.
threshold274Below this model output the fee stays at the floor. It is about 1.9 ticks of expected movement.
slope63 ÷ 2^4 = 3.94How fast the fee rises above the threshold: units of fee per unit of model output.
feeFallback1032 (0.1032%)Used if the model call ever fails, so a swap never reverts because of the model.
protocolShare100000 of 1,000,000 (10%)The part of every fee that goes to the Treasury.

A worked example: pred = 521. Above the threshold by 521 − 274 = 247. Then 63 × 247 = 15561, shifted right by 4 is 972. Add the floor 516: a fee of 1488, or 0.1488%.

4. The split

The fee the policy chooses is the total the trader pays. The hook takes floor(fee × 100000 ÷ 1,000,000) of it for the Treasury (so 10%) and applies the rest as the pool’s fee, which goes to liquidity providers. The two add up to the policy’s fee, up to rounding.

5. After the swap

The hook reads the pool’s new tick and decides whether the swap is an observation (above). If it is, it updates the averages and the run length and stores the new memory; if not, it stores nothing. Either way it emits one event, Reflex, with the eight inputs it used, the model output, the fee and the new tick. That event is what the Live page draws, and what Verify replays to check every swap.

A real swap, taken apart

What it costs

One prediction costs 21,890 gas. The whole hook (reading the memory, predicting, applying the policy, the event, updating the memory and minting the Treasury’s share) adds about 60,000 gas to a swap that leaves the memory alone and about 76,600 to one that writes it, measured in the contract tests. At the gas prices of Robinhood Chain that is a fraction of a cent.

Why a model, and not a formula?

A formula on one input (say, a moving average of recent price moves) is the obvious alternative, and we tested it: the simple rule is the baseline in our backtest. The network sees eight inputs at once, including the direction of recent moves and how quickly they arrive, and predicts the next minute clearly better (a test error of 0.79 against 1.03). Whether that is worth the extra complexity for liquidity providers is what the backtest results discuss.