
Poincaré
A Uniswap v4 hook that knows when a trend is real, then leans its curve against it. No oracle.
Demo
About
Poincaré is a Uniswap v4 hook that decides when a price trend is genuinely real, and only then bends its bonding curve to defend liquidity providers. THE PROBLEM. LPs don't lose money because markets are volatile. They lose it because markets have direction. That loss has a name, Loss-Versus-Rebalancing, and the identity behind it says the lever is the curve's shape, not the fee. Two price paths with identical step sizes leak completely differently: the one that chops ends where it started and barely costs anything, while the one that trends is arbitraged the whole way down. So the right response is to reshape the curve, but only while a real trend is actually happening. That last clause is the hard part, and it is the project. THE ENGINE. Deciding "has a regime change started, and how fast can I be sure without crying wolf" is a solved problem in statistics called Quickest Change Detection. Poincaré runs a two-sided CUSUM detector on the pool's own log-returns, sampled once per block from pre-swap reserves. CUSUM is asymptotically optimal under Lorden's minimax criterion, meaning it minimises worst-case detection delay for any false-alarm rate you tolerate. Crucially it fires at a data-dependent stopping time rather than after a fixed number of blocks, so there is no countdown for an attacker to precompute. A directional-efficiency gate confirms the move is a march rather than a thrash, and a Huber clip bounds any single block's influence. Thresholds are derived from a target false-alarm rate measured on the pair's own heavy-tailed returns, never hand-picked. THE ACTUATOR. Once a trend is confirmed, flow pushing with it pays a small, capped, rate-limited spread that goes straight to the LPs it would otherwise have been taken from. Flow trading against the trend, and all flow in calm markets, pays nothing and trades at the plain constant-product price. We originally built the stronger version, direction-dependent curve depth, found a buy-shallow/sell-deep round trip that drains the pool, and shipped the spread instead because it is arb-safe by construction. NO EXTERNAL DEPENDENCIES. No oracle, keeper, AVS, relayer or cross-chain service. The only input is the pool's own reserve-implied price. Adding a feed would reintroduce exactly the trust and latency assumptions the detector exists to remove. BUILT DURING THIS HACKATHON: THE DETECTOR LAB. The detector's parameters decide what counts as a trend, so nobody should take that calibration on trust. The Lab replays the pool's real recorded history under any parameters you choose, against the deployed configuration as a control, using an exact BigInt port of the on-chain Cusum, DirectionalSignal and ControlLaw libraries. Replaying the live parameters reproduces the trace the hook actually emitted with ZERO WEI of deviation across 362 real Unichain Sepolia blocks, and that capture is committed as a fixture and asserted in CI so the port cannot drift from the contracts without failing a build. Drop the threshold and watch it fire inside chop; drop the directional gate and watch the duty cycle explode. Each panel is also narrated in plain English by a Gemini call routed through a Supabase edge function, with a deterministic local fallback so it degrades rather than breaks. DOES IT WORK. Over 12 months of real Binance ETH/USDC 4h closes, against a symmetric volatility-scaled fee sized to cost traders exactly the same (matched to 0.01%), Poincaré retained +23,706 USDC of LP value against the baseline's +18,184, which is 6.73 against 5.17 of LP value per unit of trader cost. On raw LVR reduction the symmetric baseline edges us, 4.41% against 4.27%, and we publish that too. The first run of that study tripped the equal-friction assertion, which caught us spending 40% more of our traders' money than the baseline; we retuned and re-ran rather than shipping the flattering number. STATUS. Live on Unichain Sepolia with a demo WETH/USDC pool, a faucet and a web app. 131 Foundry tests plus 40 TypeScript tests, invariants at 128k randomised calls per flavour with zero reverts, and quotes that match execution to the wei even in a fresh block. Selected as a winner at the Uniswap Hook Incubator 10, then selected by the Uniswap Foundation Security Fund, which sponsored a security review by Olympix: nine findings, none high severity, all fixed, each with a regression test that fails on the pre-fix code. An independent external audit is still required before mainnet, and we say so. The curve is the actuator. The detector is the contribution.
How it's made
STACK. Solidity 0.8.30 with Foundry. Uniswap v4-core and v4-periphery, OpenZeppelin's uniswap-hooks BaseCustomCurve for settlement, Solady FixedPointMathLib for lnWad, hookmate for router and address constants. Frontend is Vite + React + TypeScript + Tailwind v4 with wagmi, viem and RainbowKit, backed by Supabase (Postgres, Realtime, Edge Functions on Deno). Deployed to Unichain Sepolia against the canonical v4 PoolManager, with the hook address CREATE2-mined so the permission flags are encoded in it. THE HOOK. It is a custom-curve hook, so it bypasses v4's native x*y=k entirely. beforeSwap returns a BeforeSwapDelta that consumes the full specified amount and supplies the computed counter-amount; the native tick-liquidity path is reverted. Reserves are the hook's own ERC-6909 claim balances rather than a tracked variable, so they stay consistent through take/settle/sync inside the unlock pattern. Liquidity is hook-owned with the LP share token being the hook's own ERC-20, and MINIMUM_LIQUIDITY is burned to a dead address on the first mint as a first-depositor guard. THE DETECTOR PIPELINE. Four small pure libraries, each independently unit-tested before the hook was wired: PriceLib turns reserves into a WAD price and a signed log-return (the only ln per block, taken at the hook boundary so it stays off the pricing hot path); DirectionalSignal keeps two O(1) exponentially-weighted accumulators for directional efficiency D and a live volatility estimate sigma-hat, deliberately not a ring buffer because a fixed window has a hard edge an attacker knows the timing of; Cusum runs the two-sided recursion with updateCapped so a sustained drift can never overflow and revert a swap; ControlLaw maps evidence to a bounded, rate-limited kappa. Detector state is packed into 4 storage slots (int128 statistic pair, int128/uint128 accumulator pair, uint64 kappa plus trend plus block number), which took the per-sampled-block cost to about 96k gas including the trace event. The int128 downcasts are safe because sMax <= int128.max is enforced at construction and the EWMA is bounded by the lnWad domain over (1 - lambda). THE ARCHITECTURAL TRICK WORTH STEALING. All detector logic lives in one pure view, _projectDetector. The swap path calls it and persists the result; previewSpread and previewDetector expose the same projection read-only; the Lens quotes through those. Because the Lens sees the projected once-per-block sample the next swap would take, a quote taken in a fresh block before anyone has swapped still matches execution to the wei. One function, so the two paths cannot drift. THE THING WE GOT WRONG FIRST. The brief called for direction-dependent curve depth. We built it, then found a concrete buy-shallow / sell-deep round trip that drains the pool, because depth asymmetry moves the mid-price rather than just the spread. The shipped design puts the asymmetry in the slope instead: a non-negative directional spread on a single symmetric base, which is unprofitable to round-trip by construction. The deep symmetric base also has two obvious parameterisations that are both unsafe (offsets recomputed from current reserves are drainable in two swaps; fixed absolute offsets shift the mid on every ratio deposit), so offsets are anchored at the first deposit and scale only with LP share supply. BUILT THIS HACKATHON: THE OFF-CHAIN PORT. The Detector Lab needed an exact TypeScript reimplementation of three Solidity libraries. Everything is BigInt in WAD, mulDiv floors like FullMath and only ever touches non-negative magnitudes with signs reapplied, and every clamp and >= vs > is copied verbatim, because boundary cases are where a reimplementation silently drifts. It reproduces the live trace with zero wei of deviation across 362 real blocks, verified against a committed fixture in CI. HACKY BITS AND REAL BUGS FOUND. - toWad used toFixed(18), which prints a float's BINARY expansion: (0.07).toFixed(18) is "0.070000000000000007", so parameters arrived several wei off. Fixed by going through the shortest round-tripping decimal string with exponent forms expanded by hand. - The detector runs on price = reserve1/reserve0 while the UI charts USDC/WETH, so the hook's "up" is a falling chart. The inversion is confined to one constant and one boundary, and the replay works in hook orientation so it can be diffed against raw event data. - detector_samples stores numerics for charting, and an 18-decimal value loses its low digits through a JS float. Wei-exact parity was impossible until we added a nullable jsonb column carrying the event's raw integers as decimal strings, with the reader falling back to floats for older rows. - The DetectorSample event carries D and sigma-hat but not the EWMA accumulators, so the replay seeds them by inverting those two. Exact for ewmaTV, exact in magnitude for ewmaNet, with only the sign inferred. - Enabling adaptive mode against a pool deployed with sigmaFloor = 0 divided by zero on a cold start, which is a configuration the hook's own constructor would refuse. Two clicks from a white screen; now guarded at both ends. - supabase-js turns any non-2xx edge-function response into a FunctionsHttpError and leaves the JSON body unread on the Response, so a real error message surfaced as "returned a non-2xx status code". We read it off error.context. - Gemini 3.x reasons before answering and those thought tokens count against maxOutputTokens, so a budget sized for three sentences got spent on reasoning and truncated the reply mid-word. Raised the ceiling, and now reject any candidate whose finishReason isn't STOP so a truncated answer can never reach the cache. - The narration cache had no invalidation path, so one bad answer would be served forever. Keys are now namespaced by a CACHE_VERSION constant. TESTING. 131 Foundry tests plus 40 TypeScript. Two invariant flavours (plain and full-feature) at 128k randomised calls each with zero reverts, asserting ghost-accounting equality to the wei and that the constant-product invariant never decreases. Manipulation sims run end-to-end against the real PoolManager: a fake-trend round trip loses money, a single-block flash spike moves the detector by exactly zero, and a sigma-inflation attack on the adaptive mode burns value every block. The 12-month study forks Sepolia and replays real Binance closes through three pools. Its equal-friction assertion is the reason our headline number is honest: the first run tripped it at 28.5%, catching that we were spending 40% more of our traders' money than the baseline we were claiming to beat. No oracle, AVS, keeper, relayer or cross-chain dependency, because the detector's entire security argument is that its only input is the pool's own price. The hackathon work uses Supabase (Postgres + Edge Functions) as the backend and the Gemini API for narration, with the key held as an edge-function secret since anything a Vite app can read at runtime ships in the bundle.