How clim works
1. Four weather stations must agree (Chainlink CRE)
Every 30 seconds a Chainlink CRE workflow pulls one-minute ETH candles from Coinbase, Kraken, Binance and Hyperliquid, plus Deribit's DVOL. A venue whose last candle is older than 120 s is dropped; with fewer than 3 venues there is no report. The workflow computes the median price per minute and the 15-minute realized volatility (RV15), takes the median of every field with CRE's consensus API and writes one report to RiskDesk.onReport.
What runs today: CRE's simulator, on one node. Our operator key sends each report through MockKeystoneForwarder, which checks no signature, and RiskDesk accepts simulated reports from that key only. The workflow is written with CRE's consensus API for a Chainlink DON, whose nodes would sign their median report and deliver it through the KeystoneForwarder, but it has not run on one: DON deploy access, requested on 6 October, was not granted during the hackathon, so the DON deployment was cut.
RiskDesk only stores the number after checks: reports at least 20 s apart, at most 30 s in the future, at least 3 sources, σ between 10% and 1000% a year, and σ may move only between 0.8× and 2× its previous value per report (volatility rises fast, falls slowly). Dispersion between venues above 25 bp flags the report as degraded.
No function sets σ or the fee. The owner chooses which forwarder and which workflow to trust, and in this build the owner key is also the live desk's simulation operator. So until it disables simulation (disableSim(), which cannot be undone) and renounces ownership, it can post reports itself, inside these same bounds (the fee stays between 5 and 150 bp): through the mock forwarder while simulation is on, and after that through any forwarder it sets. On a DON, the owner would point the desk at the KeystoneForwarder, pin clim's workflow ID (the forwarder checks the nodes' signatures, not which workflow sent the report), call disableSim(), then renounce ownership: only reports the nodes sign for clim's workflow would count.
3. When the stations go quiet
If the desk has not reported for 180 s, the hook is blind and quotes at least 30 bp until a fresh report lands. A degraded report (venues disagree) also lifts the fee to at least 30 bp.
Contracts on Sepolia
Ethereum Sepolia (chain id 11155111). Every address links to Etherscan. The source of every contract clim deployed (its six own contracts and the arbitrage bot's router) is verified on Etherscan and Sourcify, so Etherscan's Read Contract tab calls RiskDesk.state() and ClimHook.quoteFee() with no wallet.
| Contract | Address | Source | Role |
|---|---|---|---|
| clim | |||
RiskDesk (live) | 0xCDbfd6b9…334F | Etherscan · Sourcify | receives the CRE report every 30 s |
ClimHook (live) | 0x89f04C14…9080 | Etherscan · Sourcify | prices every swap of the live clim pool from the live desk |
RiskDesk (replay) | 0x4b843dc3…4746 | Etherscan · Sourcify | received the CRE reports of the 4 February 2026 storm, replayed; flagged REPLAY |
ClimHook (replay) | 0xf6E4CEC9…D080 | Etherscan · Sourcify | prices every swap of the replay clim pool from the replay desk |
| Uniswap v4 | |||
PoolManager | 0xE03A1074…3543 | – | the v4 singleton; calls the hook on every swap |
StateView | 0xE1Dd9c3f…7E4C | – | pool state reads (prices, liquidity, positions) |
PoolSwapTest | 0x9B6b46e2…6eEe | – | test swap router: /swap and the retail bot |
PoolModifyLiquidityTest | 0x0C478023…1B0A | – | test liquidity router: /lp |
| Chainlink | |||
MockKeystoneForwarder | 0x15fC6ae9…9F88 | – | delivers the reports of cre workflow simulate --broadcast; checks no signature |
KeystoneForwarder | 0xF8344CFd…4482 | – | delivers DON-signed reports; not used: the DON deployment was cut |
| Test tokens and bots | |||
tETH | 0xcB249894…5A19 | Etherscan · Sourcify | test ETH with a public faucet |
tUSD | 0xce3171cB…4C14 | Etherscan · Sourcify | test USD with a public faucet |
PoolSwapTest (arbitrage) | 0x70856584…ce51 | Etherscan · Sourcify | the arbitrage bot's own router, so its swaps can be told apart |
| Pool (tETH / tUSD, tick spacing 60) | Pool id | Created | LP fee |
|---|---|---|---|
| Pool V · clim (live) | 0x49cad216…d1e6 | Initialize tx | dynamic: set by ClimHook on every swap |
| Pool S · static twin (live) | 0x0e4aeceb…abf0 | Initialize tx | 5.11 bp, fixed |
| Pool V · clim (replay) | 0xc91db9c4…b60f | Initialize tx | dynamic: set by ClimHook on every swap |
| Pool S · static twin (replay) | 0x5ccceb29…c1f4 | Initialize tx | 15.03 bp, fixed |
The full addresses, pool keys and pool ids are in shared/deployments/sepolia.json.
Frequently asked questions
Questions about clim, with precise answers. At TOKEN2049 Origins on 6 October, a Chainlink mentor asked two of them: how the fee is changed and computed (the first two answers) and whether a pool that is already live can switch to clim (the third). A Chainlink judge suggested the last one at the booth on 7 October 2026. The others are the questions we expect from judges.
Code references point to the clim repository and to Uniswap v4 core.
How is the fee computed?
On every swap:
fee_pips = clamp( ceil(sigmaE9 × etaE4 × sqrtHalfDtE6 × kE4 / 1e17), feeMinPips, feeMaxPips )
sigmaE9: the risk desk's latest ETH volatility, per square-root second, times 10^9 (an annual volatility divided by √31,536,000). The desk measures it as the 15-minute realized volatility of the median one-minute price of four exchanges.etaE4: η × 10^4, with η = 1/P* − 0.824. Under the arbitrage model of Milionis, Moallemi and Roughgarden (2023), corrected for fixed block times by Nezlobin and Tassy (2025), a block is arbitraged with probability 1/(η + 0.824). Choosing η therefore chooses P*, the share of blocks the LP lets arbitrageurs take.sqrtHalfDtE6: √(blockTime/2) × 10^6. Sepolia has 12 s blocks, so 2,449,490.kE4: a model-risk multiplier from the desk, between 1 and 2 (10,000 to 20,000). It can only make the fee more prudent.feeMinPipsis the pair's market fee tier (500 pips = 5 bp for ETH/USDC),feeMaxPipsa hard cap. 1 bp = 100 pips; Uniswap caps LP fees at 1,000,000 pips.
Two safety rules sit on top:
- Blind: if the desk never reported, or its last report is older than
tauKillSec, the fee is at leastfeeSafePips. - Degraded: if the venues disagree by more than 25 bp, the fee is at least
feeSafePips.
The README shows the deployed values and the fee at several volatility levels.
Who changes the fee, and how?
Nobody, and no transaction does. The fee is recomputed inside each swap:
- A trader calls
PoolManager.swapon the clim pool. - Because the pool's
PoolKeynamesClimHookas its hook and the hook has thebeforeSwappermission, the PoolManager callsClimHook.beforeSwap. - The hook reads
RiskDesk.state()(one external view call), computes the fee as above and returns it withOVERRIDE_FEE_FLAG(0x400000). This is OpenZeppelin'sBaseOverrideFeepattern. - The PoolManager uses that fee for this swap only. In v4-core
Pool.swap:lpFee = params.lpFeeOverride.isOverride() ? params.lpFeeOverride.removeOverrideFlagAndValidate() : slot0Start.lpFee(). The fee stored in the pool'sslot0is not written. - The
Swapevent'sfeefield records the fee that swap paid (LP fee plus protocol fee; the protocol fee is zero on our Sepolia pools).
What moves the fee over time is the desk: every 30 s the Chainlink CRE workflow writes a new report to RiskDesk, and the next swap reads it. On a DON that report is signed by the DON; in this build it comes from the one-node CRE simulator, sent by the operator key through MockKeystoneForwarder (see "Is the CRE consensus real in your demo?").
Can a pool that is already live switch to clim?
It depends on how the pool was created.
- A Uniswap v3 pool: no.
UniswapV3Pool.feeisimmutable; the factory owner can only enable new fee tiers or set the protocol's share of fees, never change what an existing pool charges. - A static-fee Uniswap v4 pool: no. A pool is identified by
PoolId = keccak256(abi.encode(PoolKey)), and thePoolKeycontains the fee and the hook address. A pool whose key has a fixed fee (for example 500) can never take a per-swap fee: the PoolManager only honours a hook's fee override whenkey.fee == DYNAMIC_FEE_FLAG(0x800000). It cannot gain a hook either, and a pool cannot be initialized twice (PoolAlreadyInitialized). The path is a new pool withfee = 0x800000andhooks = ClimHook, and LPs move their liquidity to it (remove from the old pool, add to the new one). Uniswap v2 and v3 pools have no hooks at all. - A dynamic-fee v4 pool with its own hook: it depends on that hook. Uniswap v4 gives a dynamic-fee pool's hook two ways to set the fee: per swap (
beforeSwapreturning the fee withOVERRIDE_FEE_FLAG, what clim does) or stored (PoolManager.updateDynamicLPFee(key, fee), which reverts unlessmsg.senderis the pool's hook). If the existing hook can read an external source or exposes a keeper path toupdateDynamicLPFee, it can follow clim's risk desk without migrating. If its logic is fixed, a new hook means a new pool. - A DEX with its own AMM and a keeper-set fee (for example on an L2): its keeper reads
RiskDesk.state(), or the same CRE workflow writes the report to its chain, and the DEX applies its own fee rule. The lightest first step is "shadow mode": publish the recommended fee next to the live one before switching. Example: Fables (Robinhood Chain) already has a keeper post temporary fee overrides between a floor and a cap on top of a flat fee (our pre-hackathon reading of public on-chain data, 2026-09-30); that keeper could readRiskDesk.state()with no contract change. - clim's own pools: the parameters are immutable. P*, the floor, the cap, the safe fee and the kill delay are constructor arguments of
ClimHook. Changing them means deploying a new hook (a new address mined for its permission bits) and a new pool. That is deliberate: an LP knows the risk profile it joined, and no owner can change the rule.
What can change on a live clim pool without any migration: the volatility (every report), the multiplier k (sent by the desk, bounded to [1, 2]), the forwarder address and expected workflow id on RiskDesk (owner, to move from the simulation forwarder to the production KeystoneForwarder), and disableSim(), which permanently stops accepting simulated reports.
| Who | On a clim pool | On a static-fee v4 pool |
|---|---|---|
| A swapper | Nothing: the fee depends on neither direction, size nor pool state | Nothing |
| The CRE workflow (signed reports from a DON once deployed; today one simulated node, sent by the operator key) | The volatility and the flags, through its reports, bounded by the envelope | Not applicable |
The RiskDesk owner | Which forwarder and workflow are trusted; no function sets the volatility, the fee or the parameters. In this build the owner key is also the operator key, the only accepted sender of the live desk's simulated reports, so it can post any volatility inside the envelope; that lasts until ownership is renounced, not just until disableSim(), because the owner can re-point the forwarder (see "Can the owner change the fee?") | Not applicable |
The ClimHook deployer | Nothing after deployment: every parameter is immutable | Not applicable |
| The PoolManager owner (protocol fee controller) | The protocol fee only (at most 1,000 pips per direction; the controller is 0x0 on Sepolia) | The same |
The protocol fee is a separate dial: it is taken on top of the LP fee and goes to the protocol, not to LPs.
Why not measure volatility inside the pool, on-chain?
It would capture most of the gain: in our lab, a volatility computed from the pool's own prices gets most of the improvement (the exact share is in the README's Results section). We still use a desk because the number must be hard to manipulate and easy to share:
- a pool-internal estimate can be pushed by trading against the pool itself; four exchanges that must agree cannot;
- the pool only sees itself, and an untraded pool shows no volatility at all;
- one figure (signed by the DON once deployed on one) can serve many pools and chains;
- the model check (predicted against observed arbitrage) runs off-chain, next to the data.
Why Chainlink CRE, and not Data Feeds or Data Streams?
- Data Feeds. The ETH/USD price feed on Ethereum mainnet updates on a 0.5 % deviation or a one-hour heartbeat: a price, not a volatility, and too coarse to build a 15-minute estimate from. Chainlink also lists ETH realized-volatility feeds, but their shortest window is 24 hours with a one-hour heartbeat, they are not listed on Ethereum mainnet, and the Sepolia ETH-USD 24hr feed (
0x31D04174D0e1643963b38d87f26b0675Bb7dC96e) last updated on 2024-08-30. A storm that starts and ends within the hour barely moves a 24-hour number. - Data Streams. Pull-based: the user fetches a signed report off-chain and submits it for verification. For a fee, that would let the swapper choose which fresh report to bring. Its report schemas cover prices and other market data, not a 15-minute multi-venue realized volatility.
- CRE runs our own computation where the data lives (USD normalization, freshness, quorum, RV15, dispersion), lets every node fetch independently, and delivers one report on-chain every 30 s that any pool, keeper or chain can read. On a DON that report is the nodes' median, signed; in this build the one-node CRE simulator writes it through
MockKeystoneForwarder(next question).
Is the CRE consensus real in your demo?
Not in simulation, and we say so. cre workflow simulate runs a single node, and on Sepolia the --broadcast path goes through MockKeystoneForwarder, which does not verify signatures. We compensate in two ways: RiskDesk accepts simulated reports only when tx.origin is our operator key (the demo shows a forged report being rejected; that key is also the desk's owner, so in simulation it is the one trusted sender, see "Can the owner change the fee?"), and the workflow is written with the CRE consensus API (median of each field) for a DON, but it has not run on one: DON deploy access was requested on 6 October 2026 (22:56 SGT) and not granted during the hackathon (still "Not enabled" on 7 October), so the DON deployment was cut. On a DON, reports would arrive through the production KeystoneForwarder; the owner would point RiskDesk at it and pin the expected workflow ID (ReceiverTemplate's identity checks), and only then call disableSim() and renounce ownership. From then on no single key can write the desk.
Is it profitable?
For LPs, modestly and unevenly: it is insurance. The README's Results section gives the lab numbers at the deployed parameters (both comparisons, and the backtest gain per year of capital). As a business, not as a cut of hook fees: the credible path is a risk desk offered as a service to DEXs.
Why Ethereum Sepolia? Why not Solana?
We need Uniswap v4 hooks and Chainlink CRE on the same chain. Sepolia has both, and its 12 s blocks make the fee formula produce readable fees. Solana has no Uniswap v4 hooks (we would have to write our own AMM program), and Meteora already ships volatility-based fees there. CRE can write to Solana, so the same desk can publish its report there later: one desk, many chains.
What if an exchange goes down or reports a wrong price?
A venue whose last closed one-minute candle is older than 120 s is dropped. With fewer than 3 venues there is no report, and if the desk stays silent past the kill delay the hook quotes the safe fee. If the venues disagree by more than 25 bp, the desk flags itself as degraded and the hook quotes at least the safe fee. Volatility can move at most ×2 up and ×0.8 down between two reports, so one bad report cannot swing the fee arbitrarily.
Can the owner change the fee?
Not through any setter, but in this build the owner key does send the reports. ClimHook has no owner and no setter, and RiskDesk has no function that sets volatility or the fee. RiskDesk's owner (OpenZeppelin Ownable, through Chainlink's ReceiverTemplate) can choose which forwarder and which workflow to trust (setForwarderAddress, setExpectedWorkflowId, setExpectedAuthor, setExpectedWorkflowName), call disableSim(), and transfer or renounce ownership.
In this hackathon deployment the live desk's owner key is also its simOperator, the operator key that sends every simulated report. While simMode is on, RiskDesk accepts a report through MockKeystoneForwarder only when tx.origin is that key, and the report itself declares the volatility, k, the number of venues and their dispersion. So today that one key can post any volatility it likes, inside the bounds below. (The replay desk has the same owner but its own simOperator key.) A malicious owner could also point the desk at a forwarder it controls, or at address 0, which removes the sender check, and then feed its own volatility. disableSim() does not prevent that, so the owner key can post reports until ownership is renounced.
Every report, whoever sends it, stays bounded:
- volatility moves at most ×2 up and ×0.8 down per report, and always stays between 10% and 1000% a year;
- reports must be at least 20 s apart (by observation time), at most 30 s in the future, and declare at least 3 venues;
- k is clamped to [1, 2];
- the fee stays between the 5 bp floor and the 150 bp cap.
That still leaves room: from a calm 12% a year, 7 reports, a few minutes in all, reach the 1000% ceiling (about 109 bp at k = 1, the 150 bp cap at k = 2), and a report that declares more than 25 bp of dispersion flags the desk as degraded, which lifts the fee to at least the 30 bp safe fee.
On a DON, once the expected workflow ID is pinned, simulation disabled and ownership renounced, no single key can write the desk: RiskDesk then accepts only the DON's signed reports. The owner switches to the KeystoneForwarder and pins the expected workflow ID (ReceiverTemplate's identity checks) before it calls disableSim() (irreversible) and renounces ownership (or hands it to a timelocked multisig). The hackathon deployment keeps an owner because it must switch forwarders; DON deploy access was not granted during the hackathon, so that switch has not happened.
Why not set the fee from volume too?
A Chainlink judge suggested it at the booth on 7 October 2026: the fee should also read the pool's own volume, to attract traders when the pool lacks volume, and rest on established methods. It is on the roadmap, not in this build.
What is built. The fee reads volatility only. Volume is not an input to the workflow, to RiskDesk or to ClimHook, and the workflow sends k = 1 in every report.
How volume would enter, with no hook change. Through k, the model-risk multiplier the desk already sends with the volatility in every report. RiskDesk clamps it to [1, 2] (K_MIN_E4 = 10_000, K_MAX_E4 = 20_000), and ClimHook multiplies the fee by it.
- Each run, the workflow reads the clim pool's recent
Swapevents from the PoolManager (for example over the 15 minutes of the volatility estimate) with CRE's EVM client (EVMClient.filterLogs, inside the 15 EVM reads a run may make). - It splits that volume in two: arbitrage, the swaps that move the pool's price toward the median price of the four exchanges, which the workflow already computes (the trades of the arbitrageur in Milionis, Moallemi and Roughgarden's model), and the rest, uninformed flow.
- It raises k as the arbitrage share grows. In Glosten and Milgrom (1985), a spread exists to pay the market maker for trading with better-informed traders, and it widens as their share of the flow grows. In Avellaneda and Stoikov (2008), a market maker's optimal quotes depend on volatility and on how order arrivals respond to the quoted spread: clim's fee already has the first input, and the pool's own flow is how the desk would learn the second.
- k then carries this volume term and the model-risk term together, inside the same [1, 2] bound. How fast k rises with the arbitrage share is a parameter the lab would fit.
What it cannot do in this build. It can only raise the fee. k never goes below 1, so a volume term can make the storm premium more prudent, never cheaper. It changes nothing while the formula sits at the 5 bp floor: the floor holds up to about 46% annualized volatility at k = 1, and up to about 23% at k = 2. Cutting the fee to attract traders when the pool lacks volume, the judge's goal, needs a desk that accepts k below 1: a new RiskDesk, and since ClimHook's desk address is immutable, a new hook and new pools. The floor stays the pair's market tier in every version. That direction is not a sure win either: Hasbrouck, Rivera and Saleh (2022, "The Need for Fees at a DEX: How Increases in Fees Can Increase DEX Trading Volume") show that a higher fee can raise a DEX's volume, because it pays LPs to supply the depth that lowers traders' price impact. A fee cut aimed at volume can shrink liquidity, and volume with it.
What it would take to ship. The lab would backtest the volume term the way it tested the volatility rule: against the same fixed-fee comparator, at the same average fee and at the same cost to traders (the README's two comparisons), and with the same prediction check, the predicted against the observed share of arbitraged blocks. With k above 1 the prediction becomes 1/(k × η + 0.824): about 17% at k = 2, against 30% at k = 1. On Sepolia our arbitrage bot trades through its own router, so its swaps can be told apart, and the split of step 2 can be checked against them first.