Storm insurance for Uniswap v4 LPs: a Chainlink CRE risk desk measures ETH volatility on four exchanges every 30 seconds, and a Uniswap v4 hook prices every swap from it.
TOKEN2049 Origins · Singapore, October 2026 · Main track and Chainlink "Best workflow with CRE"
Open the dashboard · Video demo · Deck · CRE evidence · CRE DevEx report · CRE friction log
The 4 February 2026 storm replayed with real Binance prices at the deployed parameters
(lab/out/replay-2026-02-04.json).
When ETH jumps on Binance, bots buy from a pool at the old price and the liquidity provider (LP) pays the gap. The fastest bot wins that race: when prices move, speed is what earns, and the LP pays for it. That loss is small when the market is calm and large in a storm, yet a pool charges the same fee in both. clim makes speed cost more when it matters: the fee follows the weather. Four exchanges act as four weather stations that must agree: a Chainlink CRE workflow reads them every 30 seconds and writes one volatility figure on-chain (from the CRE simulator for now: see What's live vs. simulated). On every swap, a Uniswap v4 hook turns that figure into the fee: the pair's usual tier when it is calm, a premium that rises with volatility in a storm. The model behind the fee makes a prediction anyone can check, how often the pool gets arbitraged, and clim publishes it and measures it. The live desk has already priced a real storm, not a replay: on 7 October 2026 ETH volatility reached 242% a year and the clim pool's fee 26.50 bp.
A liquidity provider is an insurer, and the fee is its premium. It is also the LP's only barrier against arbitrage: too low and the LP is picked off in a storm, too high and traders route to the pool next door.
Built solo in 36 hours at TOKEN2049 Origins, Singapore, 6-8 October 2026. Submitted to the main track and to Chainlink's "Best workflow with CRE", the only partner track we entered, on purpose: the product is built around it (the risk desk is the CRE workflow, and without a fresh CRE report the hook falls back to the safe fee). That track asks for a CRE workflow that is the orchestration layer of the product, connects a blockchain to an external API, and has run in simulation or on a DON. The main track weighs functionality and execution (30%), technical implementation (25%), innovation (20%), usefulness (15%) and the demo (10%).
- The idea: How it works · How the fee is computed · Results
- Chainlink: Why Chainlink CRE · Files that use Chainlink · What building it made visible
- Status: What's live vs. simulated · Can a pool that is already live use clim? · Roadmap
- On-chain: Deployed addresses · Chainlink CRE evidence
- Run it: Getting started · Tech stack · Repository structure
- The rest: What we tested and rejected · Limits · References · Team · License
flowchart LR
subgraph stations["Four weather stations"]
CB["Coinbase"]
KR["Kraken"]
BN["Binance"]
HL["Hyperliquid"]
end
DV["Deribit DVOL<br/>(diagnostic)"]
subgraph cre["Chainlink CRE risk desk, every 30 s"]
NODE["Each node: 1-min candles to USD,<br/>drop stale venues, quorum of 3,<br/>15-min realized volatility, dispersion"]
DON["DON consensus: median of each field<br/>(today: CRE simulator, one node)"]
end
stations --> NODE
DV --> NODE
NODE --> DON
DON -->|"signed report on a DON,<br/>today sent by the operator key<br/>via MockKeystoneForwarder"| RD["RiskDesk.sol<br/>sanity checks, volatility envelope,<br/>no setter for sigma or the fee"]
T["Trader"] -->|"swap"| PM["Uniswap v4 PoolManager"]
PM -->|"beforeSwap"| HK["ClimHook<br/>fee from volatility,<br/>safe fee if blind or degraded"]
HK -->|"reads state()"| RD
HK -->|"fee + OVERRIDE_FEE_FLAG"| PM
PM -->|"Swap events"| APP["Dashboard"]
RD -->|"RiskReported events"| APP
- Risk desk (Chainlink CRE, every 30 s). Each node converts the venues' one-minute ETH candles
to USD, drops any venue whose last closed candle is older than 120 s, requires 3 of the 4, and
computes the 15-minute realized volatility of the median price and the dispersion between venues.
On a DON, the nodes agree on the median of each field and sign one report. In this build the
workflow runs in the CRE simulator, on one node, and the operator key sends each report through
MockKeystoneForwarder, which checks no signature. Deribit's DVOL is a diagnostic only. RiskDesk.solreceives the report throughonReportfrom the Chainlink forwarder. It rejects reports that come less than 20 s after the previous one, from more than 30 s in the future, or from fewer than 3 venues, and it limits how far volatility can move between two reports (at most ×2 up, ×0.8 down, always between 10% and 1000% a year). It has no function that sets volatility or the fee: its owner picks the forwarder and workflow to trust. In this build the owner key is also the operator key (simOperator), the only sender whose simulated reports the live desk accepts, so that key can post any volatility inside the envelope, and the fee stays within the clamp (5 bp floor, 150 bp cap).disableSim()alone does not end this: the owner can point the desk at a forwarder it controls, so the owner key can post reports until ownership is renounced. On a DON, the owner would point the desk at theKeystoneForwarderand pin the expected workflow ID (ReceiverTemplate's identity checks), and only then calldisableSim()and renounce ownership (FAQ).ClimHook.sol(Uniswap v4, built on OpenZeppelin'sBaseOverrideFee). On every swap the PoolManager callsbeforeSwap; the hook readsRiskDesk.state()and returns the fee withOVERRIDE_FEE_FLAG. If the desk has been silent for longer than the kill delay (blind) or the venues disagree (degraded), the hook quotes at least the safe fee.- Dashboard (clim-zeta.vercel.app/app, reading Sepolia). From
RiskReportedandSwapevents it shows the desk, the clim pool's fee next to a fixed-fee twin pool and how often each one gets arbitraged (/app), plus the test-token faucet, the fee before a swap (/swap), liquidity for either pool (/lp) and every contract with its Etherscan and Sourcify links.
fee = clamp( eta × k × sigma × sqrt(blockTime / 2), floor, cap )
- sigma: the desk's latest volatility, per square-root second (15-minute realized volatility).
- sqrt(blockTime / 2): scales sigma to the typical price move during half a block, the time an arbitrageur waits on average. Sepolia blocks are 12 s.
- eta: how many standard deviations of that move the fee covers. With
eta = 1/P* - 0.824, a block gets arbitraged with probability P* once the fee is above the floor (Milionis, Moallemi and Roughgarden 2023; Nezlobin and Tassy 2025 for fixed block times); at the floor, in calm markets, fewer blocks get arbitraged. The fee is a dial on how often the LP lets itself be picked off. - k: a model-risk multiplier between 1 and 2 that can only make the fee more prudent. In this
build k = 1: the model check runs in the lab, and
RiskDeskalready clamps k to [1, 2], so turning it on is a workflow change. - floor is the pair's market fee tier, so in calm markets clim costs traders what the neighbouring pool costs.
On-chain it is integer arithmetic, without logarithms or square roots:
fee_pips = clamp(ceil(sigmaE9 × etaE4 × sqrtHalfDtE6 × kE4 / 1e17), feeMinPips, feeMaxPips)
(1 bp = 100 pips).
No keeper sets the fee: there is no admin call and no fee-update transaction. The fee is recomputed inside every swap:
sequenceDiagram
participant T as Trader
participant PM as PoolManager
participant H as ClimHook
participant D as RiskDesk
T->>PM: swap(key, params)
PM->>H: beforeSwap(sender, key, params)
H->>D: state()
D-->>H: tObs, sigmaE9, kE4, flags, seq
Note over H: fee = clamp(eta x k x sigma x sqrt(blockTime/2), floor, cap)<br/>blind or degraded: at least the safe fee
H-->>PM: selector, zero delta, fee + OVERRIDE_FEE_FLAG
Note over PM: swap priced at that fee, this swap only.<br/>The stored pool fee is never written.<br/>The Swap event records the fee paid.
PM-->>T: swap settled
The pool is created with the dynamic-fee flag (fee = 0x800000 in its PoolKey), which is what
lets the PoolManager take a per-swap fee from the hook. A fee returned with OVERRIDE_FEE_FLAG
(0x400000) applies to that swap only, and the fee each swap paid is in the fee field of the
PoolManager's Swap event.
| Parameter | Value | Meaning |
|---|---|---|
| P* | 30% | Target share of blocks that get arbitraged once the fee is above the floor |
eta (etaE4) |
2.5093 (25093) |
Fee in standard deviations of the half-block price move: 1/P* - 0.824 |
sqrt(blockTime/2) (sqrtHalfDtE6) |
2.449490 (2449490) |
Sepolia, 12 s blocks |
Floor (feeMinPips) |
5.00 bp (500) |
The pair's market fee tier |
Cap (feeMaxPips) |
150.00 bp (15000) |
Hard ceiling |
Safe fee (feeSafePips) |
30.00 bp (3000) |
Minimum fee when the desk is blind or degraded |
Kill delay (tauKillSec) |
180 s | A desk silent for longer than this is treated as blind |
Decided by: lab/scripts/decide_pstar.py: year/aggregator LP P&L: P*=0.2: 28.0 bp/yr, P*=0.3: 39.2 bp/yr; P*=0.3 wins 3 of 5 scenarios. These values are immutable constructor arguments of the deployed hook.
| ETH volatility (annualized) | Fee charged on every swap (healthy desk, k = 1) |
|---|---|
| 25% | 5.00 bp |
| 50% | 5.48 bp |
| 75% | 8.21 bp |
| 100% | 10.95 bp |
| 150% | 16.42 bp |
| 225% | 24.63 bp |
The fee sits at the 5.00 bp floor up to about 46% annualized volatility, then rises in proportion to volatility. If the desk is blind (silent for more than 180 s) or degraded (venues disagree by more than 25 bp), the fee is at least 30.00 bp.
Numbers computed by the lab at P* = 30%, floor 5 bp (lab output generated 2026-10-06T15:56:16Z).
| Feb 2026 storm | Oct 2026 calm | Oct 2025 to Oct 2026 (1-min data bridged) | |
|---|---|---|---|
| LP losses to arbitrage vs a fixed-fee pool, same average fee | -15.3% | -5.5% | -26.9% |
| LP losses to arbitrage vs a fixed-fee pool, same cost to traders | -2.9% | -2.3% | -12.7% |
| Share of blocks arbitraged, predicted / observed | 29.9% / 27.9% | 15.6% / 17.1% | 24.3% / 23.6% |
Replay of the 4 February 2026 storm (2026-02-04 12:00 to 16:00 UTC): volatility 34% → 296%, clim's fee 5 → 32.4 bp, LP losses to arbitrage -18.6% against a fixed 15 bp pool with the same average fee; share of the clim pool's blocks arbitraged, predicted / observed: 29.8% / 28.5%. The window was picked during design, at an earlier setting, around the sharpest rise in volatility of the storm, after comparing three candidate windows (lab/scratch/replay_pick*.py). At P* = 30% no rolling 4 h window of the storm does better (best -18.4%): the worst of the 92 is +3.1%, the median -2.1%, and 66 of 92 beat the fixed pool.
LP gain in the lab's one-year backtest (not a forecast): -0.10% to +0.70% of capital per year (-$981 to $7,024 a year per $1M of liquidity; full-range ETH LP vs a static 5 bp pool, 1-year replay, 5 retail scenarios). In the main scenario (an aggregator routes retail between clim and a deeper 5 bp pool), ETH gains +0.39% a year, about 53% of it earned in the five stormiest weeks of the year; the same scenario on an asset twice as volatile (the same year with every return doubled) gains +0.40%. It is insurance, not a steady yield.
Where the model is weak: it predicts how often arbitrage happens, not how much it costs: in the lab's backtests, losses to arbitrage run 1.29 to 1.33 times the model's estimate. In the two 1 s windows the observed share of arbitraged blocks lands within 10% of the prediction, but the gap is statistically significant (p < 0.0005, thresholds simulated from the model because arbitrage comes in clusters). A volatility measured inside the pool itself would capture 54% to 103% of the same gain (see "Why Chainlink CRE"; above 100% means the in-pool estimate did slightly better in one sample).
The two comparisons answer different questions: does charging at the right time beat a fixed fee (same average fee), and does it still win when volume grows with volatility (same cost to traders)? We always show both, and the 4 February replay window always next to the storm's rolling windows.
clim is built around CRE, which is why it is the only partner track we entered: the risk desk is the CRE workflow, and without a fresh report the hook falls back to the safe fee. The honest answer first: a volatility measured inside the pool itself would capture most of the gain (see Results). CRE is not what makes the number possible. It is what makes it trustworthy:
- Four exchanges must agree. Nobody can push the fee around with fake trades on the pool, and a venue that freezes or diverges is dropped or flags the desk as degraded.
- On a DON: one signed report, and no single key once the owner renounces. Reports come
through Chainlink's
KeystoneForwarder, which checks the DON's signatures: on a DON, once the expected workflow ID is pinned, simulation disabled and ownership renounced, no single key can write the desk (until then the owner key can re-point the forwarder). Today, in simulation, the operator key submits the reports throughMockKeystoneForwarder, so that one key is trusted to post the volatility, inside the envelope (see How it works). - One figure for many pools and chains. CRE can write the same report to other EVM chains and to Solana.
- Room for model control. The desk could compare its own prediction with what happens on-chain and raise k without anyone touching the hook, once the workflow sends k (k = 1 in this build, see the fee above).
A price feed with a 0.5% deviation threshold and a one-hour heartbeat is too coarse for this, and a pull oracle would let the swapper choose its report. Chainlink does list ETH realized-volatility Data Feeds, but their shortest window is 24 hours with a one-hour heartbeat (and the Sepolia one last updated on 2024-08-30): a storm that lasts an hour barely moves them (FAQ).
| Track requirement | In clim | Status |
|---|---|---|
| A CRE workflow that is the orchestration layer, core to the product | The fee is computed from the desk's report on every swap; without a fresh report the hook falls back to the safe fee | ✅ |
| A blockchain integrated with an external API | Six HTTP sources per node (Coinbase, Kraken, Binance, Hyperliquid, Deribit DVOL, Kraken USDT/USD), one report written to RiskDesk on Ethereum Sepolia |
✅ |
| A successful simulation with the CRE CLI, or a deployment | cre workflow simulate --broadcast every 30 s on Sepolia: to the live desk since 2026-10-06 16:26 UTC; to the replay desk, 459 reports from 17:02 to 20:58 UTC (its loop started at 16:57) |
✅ simulation |
| Beyond the requirement: deployment on a CRE DON | Not done. 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 (friction log row 6). The workflow is written with the CRE consensus API for a DON and has not run on one | 🛣️ cut, on the roadmap |
| File | What it does |
|---|---|
cre/project.yaml |
The CRE project: targets staging-settings (live simulation) and replay-settings, Sepolia RPC. A third target, production-settings, is a DON target with a placeholder desk address, never used because DON deploy access was not granted |
cre/risk-desk/workflow.yaml |
Workflow name, entry point and config file per target |
cre/risk-desk/main.ts |
The runner entry point |
cre/risk-desk/workflow.ts |
Cron and HTTP triggers, node-mode observation, consensus by median, runtime.report, EVMClient.writeReport and the read-back of RiskDesk.state() |
cre/risk-desk/venues.ts |
Venue requests, response parsers, concurrent fetch, replay requests |
cre/risk-desk/estimator.ts |
Normalization, staleness, quorum, per-minute median, 15-minute realized volatility, dispersion, tick |
cre/risk-desk/report.ts |
The report ABI, encoder and decoder |
contracts/src/RiskDesk.sol |
The CRE consumer: onReport through the forwarder, on-chain checks, state() for the hook |
contracts/src/receiver/ReceiverTemplate.sol |
Chainlink's receiver base, copied from smartcontractkit/cre-templates |
contracts/src/ClimHook.sol |
Reads the desk's report on every swap |
cre/README.md walks through one execution, the trust model and a sample run.
We logged CRE frictions as we built: 27 rows in the friction log, 25 of them hit while building and 2 from reading the docs only, summarized for the Chainlink team in the DevEx report. The three most useful to Chainlink:
- The sports-resolution starter's receiver template rejects every report once identity checks
are on. Its
ReceiverTemplate.sol(cre-templatesd0223f3) requires 62 bytes of metadata, butKeystoneForwarderandMockKeystoneForwarderpass 64 (workflow id 32, name 10, owner 20, report id 2), so with an expected workflow id, author or name set, every report revertsInvalidMetadataLength(64, 62). Reproduced with a forge test on real forwarder metadata; the other templates have no length check, and clim uses thecircuit-breakercopy. A candidate upstream fix: align the template with the docs, which already say 64 (friction log row 23). - A rejected report looks like a success. With
--broadcast, the simulator reportsSUCCESSwhenever the forwarder transaction is mined, andMockKeystoneForwarderdoes not revert when the consumer does. A forged report on Sepolia (0x34ee46a6…53d9) was mined with status 1,ReportProcessedfalse and noRiskReported. The docs already say the status is alwaysSUCCESSin simulation, but give the wrong reason: that the mock does not call the consumer'sonReport, when it does (friction log row 2). The workflow re-readsRiskDesk.state()after each write to tell the two apart (friction log row 9). - Simulation is one node, and it needs the network. The Sepolia mock forwarder checks no
signature, so
RiskDeskonly accepts simulated reports sent by the operator key (tx.origin), and the CLI validates its credentials against the CRE API on every run: 5 of the first 413 runs of our two 30 s loops (live and replay) on 2026-10-06 stopped there. On 2026-10-07 a network outage on our side stopped 27 live runs at that check, so the live desk went silent and the hook charged its 30 bp safe fee, as designed, for about 8 minutes (04:33:00 to 04:40:24 and 04:48:12 to 04:48:36 UTC, block times); one retail swap paid it. Later that day, with our network up, the check also timed out while the CRE API was slow (context deadline exceeded), and the live desk came within 13 s of blind mode (friction log rows 1 and 24).
| Piece | Status | Detail |
|---|---|---|
| Contracts on Sepolia | ✅ live | Both desks, both hooks, the clim pools and their fixed-fee twins, test tokens with a public faucet; clim's own contracts are verified on Sourcify (addresses) |
| A CRE report every 30 s | ✅ live, from simulation | cre workflow simulate --broadcast in a loop since 2026-10-06 16:26 UTC: 444 reports on the live desk (state().seq, read 2026-10-06 20:29 UTC). Observation time (tObs) to block: median 14 s, p90 25 s over 211 reports (last 600 blocks to about 18:40 UTC on 2026-10-06) |
| A real storm on the live desk | ✅ live | On 7 October 2026, real market data, not a replay: from 01:58 to 02:51 UTC, 100 of the live desk's 105 reports put the clim pool's fee above the 5 bp floor; volatility peaked at 242% a year and the fee at 26.50 bp (report observed at 02:11 UTC). Every one of the 105 swaps on the clim pool in that window paid exactly the formula's fee for the desk's state (RiskReported and Swap events read with cast) (/replay#latest) |
| DON consensus and signature checks | 🛣️ cut | The simulator runs one node and MockKeystoneForwarder checks no signature. 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 (friction log row 6). The workflow is written with the CRE consensus API for a DON and has not run on one |
| Model-risk multiplier k | 🛣️ off (k = 1) | The model check runs in the lab (lab/out/validation.json); RiskDesk already clamps k to [1, 2], so turning it on is a workflow change |
| The 4 February 2026 storm | ✅ replayed on Sepolia | Historical Binance prices, served at wall-clock speed by bots/src/replay-server.ts to a second CRE loop that writes to the replay desk (its REPLAY flag discloses it). Its loop started at 16:57 UTC on 2026-10-06 and wrote 459 reports, from 17:02 to 20:58 UTC; since then the replay desk is silent and its hook quotes the 30 bp safe fee |
| The market | simulated | Our own arbitrage and retail bots trade the clim pool and its fixed-fee twin. No mainnet pool; volume moving to cheaper pools is only approximated; just-in-time liquidity is not modeled. The arbitrage bot keeps one transaction in flight per pool, so on chain it lands fewer arbitrages than the model's arbitrageur would: on the replay it arbitraged 21.5% of the clim pool's blocks against 29.6% predicted, but counting the 90 blocks it skipped while a trade was pending, it saw the pool outside its no-arbitrage band in 349 of 1,186 blocks (29.4%) |
| Results | simulated | Lab backtests on historical Binance ETHUSDT data (lab/out/), not live P&L |
| Dashboard | ✅ live, reads Sepolia | https://clim-zeta.vercel.app: a guided path at /app (see it, verify it on Etherscan, try it) with the full dashboard one click away, real Sepolia swaps and liquidity at /swap and /lp (test tokens from the faucet), the 7 October live storm and the finished 4 February replay at /replay; numbers checked against cast reads |
Not by flipping a switch on an existing pool. Yes for a new pool. A pool that already has a dynamic fee can follow the desk only if its hook reads an outside source or lets a keeper post the fee. Details in docs/faq.md.
- A Uniswap v3 pool cannot:
UniswapV3Pool.feeisimmutable. - A Uniswap v4 pool's fee mode and hook are part of its
PoolKey, and thePoolKeyis the pool's identity. A static-fee pool cannot become dynamic and cannot gain a hook: you create a new pool and LPs move their liquidity. - A pool created with a dynamic fee gets its fee from its own hook, either per swap (
beforeSwapwithOVERRIDE_FEE_FLAG, what clim does) or stored (PoolManager.updateDynamicLPFee, which only that hook can call). - A DEX whose fee a keeper posts can use the risk desk without migrating: the keeper reads
RiskDesk.state(). A dynamic-fee v4 pool can too if its hook reads an external source or exposes a keeper path toupdateDynamicLPFee; a hook with fixed logic means a new hook and a new pool. - clim's own parameters are immutable. A different P* means a new hook and a new pool.
- Existing DEXs plug clim in, Fables on Robinhood Chain first, in shadow mode. clim is not a
new DEX. It is a risk desk (the CRE workflow and
RiskDesk) plus a hook pattern (ClimHook) that existing protocols plug in: a keeper that posts their fees readsRiskDesk.state(), or a new pool gets a hook likeClimHook(an existing hook only if it can read an outside contract), and either applies the fee rule with their own parameters. Fables comes first. Its fees are already presented as dynamic, and a keeper moves them: its crypto pools charge a flat fee plus a temporary override that the keeper posts between a floor and a cap (our pre-hackathon reading of public on-chain data, 2026-09-30). First, clim publishes its recommended fee next to the keeper's, without acting on it. Then the keeper readsRiskDesk.state(), applies the fee rule with Fables' own P*, floor and cap, and posts the result: no contract change on their side. This is our proposal to Fables, not an agreement: nothing runs on Robinhood Chain yet. We measured Fables' keeper on-chain before proposing it (lab/README.md): it is not a flat fee (it tracks volatility, and it had its 60 bp cap in place at the two scheduled US releases we studied, CPI on 11 September and FOMC on 16 September), and on the 11 September 2026 storm clim's rule lost about 10 % more to arbitrage than the keeper at the same average fee in our model, because the keeper posted its cap in the very second of the CPI release, which a 15-minute volatility measure cannot anticipate. What clim adds is a public, checkable rule fed by four exchanges through CRE in place of one private key (fully so once the desk runs on a DON and ownership is renounced, item 3; today clim's desk is also written by a single operator key, see How it works); the keeper's event overrides can sit on top of the desk's measured baseline, which is what the shadow mode would test. - One desk, many chains: Sepolia is the proof, other EVM chains are configuration. For the
workflow, another EVM chain that CRE supports is a new target: the chain name and the desk
address in its config, an RPC in
cre/project.yaml(a mainnet also needs theisTestnetflag inworkflow.tsmade configurable). The contracts deploy unchanged, with that chain's block time in the hook (the hook needs Uniswap v4 there; a keeper-driven DEX needs only the desk; sub-second chains first need a calibrated Δt, see Limits). The same CRE workflow can write the same report to aRiskDeskon every chain CRE supports, and an off-chain keeper can read a desk on any chain without a bridge. CRE lists Robinhood Chain as Robinhood Testnet only (docs, 2026-09-18), and writes to Solana. - DON deployment. Deploy access was not granted during the hackathon. Once it is, the workflow
runs on a DON and reports arrive through the
KeystoneForwarder. The owner pointsRiskDeskat that forwarder and pins the expected workflow ID (ReceiverTemplate's identity checks), and only then callsdisableSim()and renounces ownership or hands it to a timelocked multisig. - Fees that also read volume (a judge's suggestion). Today the fee reads volatility only.
Volume would enter through k, the multiplier the desk already sends with sigma in every report,
so the hook would not change. Each run, the workflow would read the clim pool's recent
Swapevents with CRE's EVM client and split that volume into arbitrage (swaps that push the pool's price toward the exchanges' median, the trades of Milionis, Moallemi and Roughgarden's arbitrageur) and uninformed flow. k would rise with the arbitrage share: a spread pays the market maker for trading with better-informed traders (Glosten and Milgrom 1985), and a market maker's quotes depend on volatility and on how order arrivals respond to the spread (Avellaneda and Stoikov 2008). In this build that can only raise the storm premium:RiskDeskclamps k to [1, 2], and the floor stays the pair's market tier. Cutting the fee to attract traders when the pool lacks volume would need a desk that accepts k below 1, so a new hook and new pools (the hook's desk address is immutable). It is not a sure win either: a higher DEX fee can increase volume, because it pays LPs to supply depth (Hasbrouck, Rivera and Saleh 2022). Before shipping, the lab would backtest it against the same fixed-fee pool, at the same average fee and at the same cost to traders, with the same prediction check: with k above 1, the predicted share of arbitraged blocks becomes 1/(k × eta + 0.824). Details in the FAQ.
Everything runs on Ethereum Sepolia (chain id 11155111). Each address links to Etherscan. The source of every contract clim deployed (both desks, both hooks, tETH, tUSD and the arbitrage router, Uniswap's unmodified PoolSwapTest) is verified on Etherscan (source and Read Contract tab) and on Sourcify.
| Contract | Address | Source | Role | |
|---|---|---|---|---|
| clim | RiskDesk (live) |
0xCDbfd6b9…334F |
Sourcify | receives the CRE report every 30 s |
ClimHook (live) |
0x89f04C14…9080 |
Sourcify | prices every swap of the live clim pool from the live desk | |
RiskDesk (replay) |
0x4b843dc3…4746 |
Sourcify | received the CRE reports of the 4 February 2026 storm, replayed (459 reports, the last at 20:58 UTC on 2026-10-06); flagged REPLAY | |
ClimHook (replay) |
0xf6E4CEC9…D080 |
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 | ||
PoolSwapTest |
0x9B6b46e2…6eEe |
test swap router, used by the retail bot | ||
PoolModifyLiquidityTest |
0x0C478023…1B0A |
test liquidity router: the pools' seed liquidity and the /lp positions; it does not tie a position to its owner (see Limits) |
||
| Chainlink | MockKeystoneForwarder |
0x15fC6ae9…9F88 |
delivers cre workflow simulate --broadcast reports; checks no signature |
|
KeystoneForwarder |
0xF8344CFd…4482 |
delivers DON-signed reports; not used: the DON deployment was cut | ||
| Test tokens and bots | tETH |
0xcB249894…5A19 |
Sourcify | test ETH with a public faucet |
tUSD |
0xce3171cB…4C14 |
Sourcify | test USD with a public faucet | |
PoolSwapTest (arbitrage) |
0x70856584…ce51 |
Sourcify | the arbitrage bot's own router, so its swaps can be told apart | |
| Operator | 0x53aB240f…5A82 |
deployer, owner of both desks, the live desk's simOperator (the only accepted sender of its simulated reports), test-token owner |
||
| Operator (replay) | 0xbDE42818…B4c8 |
the replay desk's simOperator: sent all 459 of its simulated reports through MockKeystoneForwarder (simulation still on) |
| Pool | LP fee | PoolId |
|---|---|---|
| clim pool (live) | dynamic: set by ClimHook on every swap |
0x49cad216…d1e6 |
| fixed-fee twin (live) | 5.11 bp, fixed | 0x0e4aeceb…abf0 |
| clim pool (replay) | dynamic: set by ClimHook on every swap |
0xc91db9c4…b60f |
| fixed-fee twin (replay) | 15.03 bp, fixed | 0x5ccceb29…c1f4 |
The canonical set, with full addresses, pool keys and pool ids, lives in shared/deployments/sepolia.json.
The first simulated report
(0x60465523…1cb6),
a forged report the desk ignored and a sample run with its transaction are in
cre/README.md. The full list of reports and the workflow's own log
lines from a few simulate runs are generated at submission:
Collected 2026-10-07T10:09:52.005Z from Sepolia blocks 11853147 to 11862296 (RiskReported events).
riskDesks.live 0xCDbfd6b9C0b97A8eE31706c6CDE5E54B4954334F: 2025 reports written by the CRE workflow. Latest 5:
| seq | Observed (UTC) | Volatility applied (annualized) | Venues | Dispersion | Transaction |
|---|---|---|---|---|---|
| 2025 | 2026-10-07 10:09:24 | 111.8% | 4 | 3 bp | 0x86c9…33fe |
| 2024 | 2026-10-07 10:08:55 | 109.3% | 4 | 1 bp | 0x96a8…f5da |
| 2023 | 2026-10-07 10:08:25 | 60.6% | 4 | 1 bp | 0x4d36…c727 |
| 2022 | 2026-10-07 10:07:55 | 30.3% | 4 | 2 bp | 0xf652…99fc |
| 2021 | 2026-10-07 10:07:25 | 30.3% | 4 | 2 bp | 0xc18e…4818 |
riskDesks.replay 0x4b843dc3A7ec6202d2337cdeF8C67a0F10f24746: 459 reports written by the CRE workflow. Latest 5:
| seq | Observed (UTC) | Volatility applied (annualized) | Venues | Dispersion | Transaction |
|---|---|---|---|---|---|
| 459 | 2026-10-06 20:58:37 | 143.7% | 4 | 0 bp | 0xe602…1b89 |
| 458 | 2026-10-06 20:58:07 | 143.7% | 4 | 0 bp | 0xa8c5…6215 |
| 457 | 2026-10-06 20:57:38 | 143.7% | 4 | 0 bp | 0x0734…c98b |
| 456 | 2026-10-06 20:57:06 | 143.7% | 4 | 0 bp | 0x3fa3…bce3 |
| 455 | 2026-10-06 20:56:38 | 137.8% | 4 | 0 bp | 0xa9d4…c008 |
Log lines of a few cre workflow simulate --broadcast runs (the workflow's own [USER LOG] lines, as the loop recorded them; not the CLI's full output) are in docs/evidence.
You need Foundry, Node.js 22, Bun 1.3.9, the CRE CLI, uv (Python) and, for anything that writes on-chain, a Sepolia RPC URL and funded testnet keys.
git clone --recurse-submodules https://github.com/DVB-ANS/clim && cd clim
bun install # root workspace: shared and bots
(cd contracts && forge test) # contracts: unit and fuzz tests; without SEPOLIA_RPC_URL in contracts/.env
# (see .env.example) the 3 Sepolia fork tests skip: 73 passed, 2 skipped (75 total)
# (forge counts the 2-test fork suite, skipped in setUp, as one)
(cd lab && uv sync && uv run pytest) # lab: backtests and model checks; a fresh clone has no lab/data/
# (gitignored): 92 passed, 8 skipped (the 7 tests that read it, and
# the Fables figure test, which needs matplotlib)
# CRE risk desk: one simulated run without a transaction (needs `cre login`)
(cd cre/risk-desk && bun install)
(cd cre && cre workflow simulate risk-desk --non-interactive --trigger-index 0 --target staging-settings)
# Dashboard, a standalone npm project (Next.js): copy shared/, lab/out and the FAQ in, then run
(cd app && npm ci && npm run sync && npm run dev)To run the live loop yourself (a CRE report every 30 s with --broadcast, plus the arbitrage and
retail bots), put the operator key in cre/.env (CRE_ETH_PRIVATE_KEY) and the bot keys in
bots/.env (see bots/.env.example), then, one terminal each:
(cd bots && bun run cre-loop --pair live)
(cd bots && bun run arb --pair live)
(cd bots && bun run noise --pair live)
(cd bots && bun run status --pair live --watch)docs/runbook.md covers funding, the 4 February replay and the security demos.
| Layer | Technology |
|---|---|
| Chain | Ethereum Sepolia (chain id 11155111), 12 s blocks |
| Contracts | Solidity 0.8.26 (EVM cancun), Foundry 1.4.2, forge-std v1.17.0 |
| Uniswap | Uniswap v4 (v4-core d153b04, v4-periphery 7ebd04b), OpenZeppelin uniswap-hooks v1.2.1 (BaseOverrideFee), OpenZeppelin Contracts 5.5.0 |
| Chainlink | CRE CLI v1.37.0, @chainlink/cre-sdk 1.23.0 (TypeScript compiled to WASM), ReceiverTemplate from cre-templates d0223f3, MockKeystoneForwarder and KeystoneForwarder |
| Bots and shared code | Bun 1.3.9, TypeScript 5.9.3, viem 2.57.3 |
| Lab | Python 3.12 with uv, numpy 2.5.3, pytest |
| Dashboard | Next.js 16, React 19, wagmi 2 and RainbowKit 2, viem, Recharts, Tailwind CSS 4, on Vercel |
| Submission tooling | Node.js 22 (node:test), pptxgenjs 4.0.1, matplotlib, ffmpeg |
contracts/ Foundry: RiskDesk.sol (the CRE consumer), ClimHook.sol (the hook), ClimFeeMath,
Chainlink's ReceiverTemplate (src/receiver/), test tokens, deploy scripts, tests
cre/ the CRE project (project.yaml) and the simulation loop script
risk-desk/ the workflow, TypeScript: triggers, venues, estimator, report, write
bots/ Bun and viem: arbitrage bot, retail bot, CRE loop with receipts, replay server
lab/ Python: backtests, the P* decision, model validation; out/ holds the JSON results
shared/ read by every part: deployments/sepolia.json, params.json, abis/
app/ Next.js dashboard (https://clim-zeta.vercel.app): landing, /app, /swap, /lp,
/replay, /lab, /how, /credits; reads the desks' and pools' events on Sepolia
docs/ faq.md, runbook.md, the CRE feedback (feedback/), the day-by-day sessions,
the design spec and plans (superpowers/), the submission tooling (submission/)
submission/deck/v2/ the deck from Figma: slide images (png/), texts and speaker notes (slides.json)
A first design charged a directional toll against a reference price from the desk. We dropped it
during design, at an earlier setting (exploration scripts in lab/scratch/bt2.py and bt3.py; not
recomputed at the final P*): the reference is at least 30 s old, so an arbitrageur trades against
the stale reference at the low fee; splitting a swap gets around it; in simulation a symmetric fee
did better at equal tracking error; and a directional fee on a Chainlink oracle already exists
(MSpits/DynamicFeeHook). Implied volatility (Deribit DVOL) stays a diagnostic: realized volatility
drives the fee.
- Latency. Volatility is computed from closed one-minute candles, so a price move shows up in a report only once its candle has closed (up to a minute), then waits for the next report (every 30 s) and for that report's block (observation time to block: median 14 s, p90 25 s in simulation). The first move of a sudden jump is arbitraged at the old fee.
- Modest average gain, optimistic severity. The value is concentrated in storms, and the model predicts how often arbitrage happens, not how much each one costs (see Results).
- Simulation and our own market. See What's live vs. simulated.
- Sources. Binance answers HTTP 451 to US IP addresses. A quorum of 3 out of 4 venues survives one missing venue, not two.
- Cost on mainnet. Publishing every 30 s on Ethereum would cost an estimated $60k to $110k a year in gas at 0.12 to 0.22 gwei and $2,700 per ETH: 175,253 gas per report (the median of 1,359 live Sepolia reports through the mock forwarder, to 7 October) times about 1.05 million reports a year is about 184 ETH a year per gwei of gas price (about $500k a year at 1 gwei), before the DON forwarder's signature checks. Production would publish on deviation plus a heartbeat, or on an L2.
- Fast chains. With sub-second blocks the formula sits at the floor until an effective Δt (the arbitrageurs' reaction time) is calibrated.
- Test liquidity router. The pools' seed liquidity and the positions added at
/lpgo through Uniswap'sPoolModifyLiquidityTestrouter on Sepolia, which does not tie a position to its owner: anyone who knows the pool key and the salt can remove it (clim's seed positions use salt 0; checked with aneth_callfrom an unrelated address). Harmless on a testnet with test tokens, and the reason production would use the v4PositionManager. The live stack watches the pools' liquidity, and the operator can restore it. - Vercel checkpoint. Under heavy automated traffic, Vercel may briefly answer non-browser clients with a security checkpoint instead of the page (seen during our audit; the site answered 200 again afterwards).
- Not audited. Testnet only.
- Milionis, Moallemi, Roughgarden, Zhang (2022). Automated Market Making and Loss-Versus-Rebalancing. arXiv:2208.06046
- Milionis, Moallemi, Roughgarden (2023). Automated Market Making and Arbitrage Profits in the Presence of Fees. arXiv:2305.14604
- Nezlobin, Tassy (2025). Loss-Versus-Rebalancing under Deterministic and Generalized Block-Times. arXiv:2505.05113
- Campbell, Bergault, Milionis, Nutz (2025). Optimal Fees for Liquidity Provision in Automated Market Makers. arXiv:2508.08152
- Loesch, Hindman, Richardson, Welch (2021). Impermanent Loss in Uniswap v3. arXiv:2111.09192
- Kupiec (1995). Techniques for Verifying the Accuracy of Risk Measurement Models. The Journal of Derivatives 3(2), 73-84. doi:10.3905/jod.1995.407942
- Basel Committee on Banking Supervision (1996). Supervisory framework for the use of "backtesting" in conjunction with the internal models approach to market risk capital requirements.
- Glosten, Milgrom (1985). Bid, Ask and Transaction Prices in a Specialist Market with Heterogeneously Informed Traders. Journal of Financial Economics 14(1), 71-100.
- Avellaneda, Stoikov (2008). High-frequency trading in a limit order book. Quantitative Finance 8(3), 217-224.
- Hasbrouck, Rivera, Saleh (2022). The Need for Fees at a DEX: How Increases in Fees Can Increase DEX Trading Volume. Working paper.
- Uniswap v4 core (
LPFeeLibrary,PoolManager), OpenZeppelin uniswap-hooks (BaseOverrideFee), Chainlink CRE documentation and cre-templates (ReceiverTemplate).
Built solo by a member of DeVinci Blockchain (DVB), Paris.
| Role | ||
|---|---|---|
| Sofiane Ben Taleb | Design, contracts, CRE workflow, lab, dashboard | GitHub · LinkedIn |
MIT © 2026 Sofiane Ben Taleb, for clim's own code. Third-party code in this repo keeps its own license:
contracts/src/receiver/andcontracts/test/fixtures/sports-resolution/: copied unmodified from smartcontractkit/cre-templatesd0223f3. MIT, © 2025 SmartContract;IERC165.solin each folder is OpenZeppelin Contracts v5.4.0's interface as the template ships it, reformatted (MIT, © 2016-2025 Zeppelin Group Ltd). Each folder has itsLICENSE.- The
contracts/lib/submodules keep their own licenses: forge-std is MIT or Apache-2.0, OpenZeppelin's code and Uniswap v4-periphery are MIT, and Uniswap v4-core is MIT for the interfaces and libraries clim's own contracts import, BUSL-1.1 forPoolManagerand some internal libraries, and UNLICENSED for itssrc/testrouters. The local tests deployPoolManagerand, through v4-core'sDeployers, compile solmate (AGPL-3.0-only, a submodule of v4-core). clim's arbitrage router on Sepolia is v4-core's unmodifiedPoolSwapTest, compiled from the submodule. Its own files (PoolSwapTest,PoolTestBase) are among those UNLICENSED test routers, which Uniswap publishes for testing and deploys itself on Sepolia (0x9B6b46e2c869aa39918Db7f52f5557FE577B6eEe). It also includes four of those BUSL-1.1 libraries (Lock,CurrencyReserves,NonzeroDeltaCount,Position), used here on testnet only, a non-production use that BUSL-1.1 permits. clim's own deployed contracts (both desks, both hooks, tETH and tUSD) compile only MIT sources. app/includes third-party components and npm dependencies with their own licenses: the adapted components and the non-permissive packages are described inapp/THIRD_PARTY_NOTICES.md(also at /credits), and every npm package's license text is inapp/public/third-party-licenses.txt(also at /third-party-licenses.txt). Some are not permissive: two React Bits components are MIT + Commons Clause, the MetaMask SDK (through wagmi and RainbowKit) is under ConsenSys' license, andua-parser-js2 (through RainbowKit) is AGPL-3.0-or-later.- Other npm dependencies keep their own licenses, including
@chainlink/cre-sdk(BUSL-1.1).
