Arbitrage recapture
Read what a correction is, when it runs, where its profit comes from, how it splits, and whose contract does what in the path.
What a correction is, when it runs, where its profit comes from, how that profit splits, and which contract in the path belongs to whom.
A Hookr pool drifts from the other venues its pair trades on, and someone closes the gap. Arbitrage recapture is a lane that closes it inside the swap that opened it and pays the proceeds back to the market instead of to whoever was fastest. A partner contract does the trading. Hookr's part of the path is an adapter, a set of frozen fields on the pool, and a kernel that calls the partner and cannot be broken by it.
The lane is not part of the default root. It runs only on a pool opened on a recapture root, and it is not offered in the Hookr app yet.
The Roots That Run It
Hookr V2 has more than one root hook. HookrModularHookV6 at 0xb3cA29cF721380CEe8b8e4755F3865Ebc68Fe8cC is the default root: five native rules and nothing else. The arbitrage-recapture root at 0xb914f955294799de4b891bd2EA8AF628Fa1c68CC runs the same five rules over the same module set, plus the correction lane. Its kernel id is 0xd8b6c165b82efc3b7498081f071ea4f2476e2fb9c61b2e614aba2a016f94555a and its root profile sealed on 2026-09-12.
Both addresses are mined to the same flag word, 0x28cc. The recapture root asks the PoolManager for no permission the default root does not already hold, and the correction needs none of its own: the kernel returns a zero delta on the correction's nested callbacks and the executor settles its own leg. The returns-delta bits in that flag word are there for the five native rules.
A pool names exactly one root in PoolKey.hooks, at creation, permanently. Earlier recapture roots exist, their pools are open and trading, and each carries its own kernel id and its own sealed profile. Nothing upgrades a pool from one root to another. Pool lifecycle covers the rest of what freezes at creation.
What a Correction Is
A correction is one bounded call from the swap kernel out to a partner executor, made while the swap is still in flight. The executor looks at this pool's price and at a venue it has registered for the same pair, trades the difference if there is one worth trading, and reports back a gross figure in the quote currency: realizedProfitQuote. That figure is the whole proceeds of the lane. Everything else on this page is about who receives it and what happens when the call does not go well.
The executor decides whether to trade and how large to trade. Hookr decides nothing about the trade itself.
Both Phases of One Swap
The current recapture root runs the lane twice: once in beforeSwap, before the swap is priced, and once in afterSwap, after it settles. One swap can therefore produce two corrections and two events, distinguished by the indexed phase argument each one carries.
The two chances answer different situations. A gap that was already open when the transaction arrived is a beforeSwap correction; a gap the trader's own order has opened is an afterSwap one. The distinction is not theoretical: the third recapture root could not correct in beforeSwap at all, because its kernel refused a correction on a quote-denominated swap, which an ordinary buy is. Every correction it ever ran was in afterSwap, and an arbitrageur who moved the reference venue first kept the gap.
The executor is not told which of the two it is in. The adapter forwards three arguments and the phase is not among them, so an executor that would price a pre-existing gap differently from one the trader just opened cannot tell them apart from the call it receives. The phase is a Hookr-side fact: it selects the correction bytes the kernel passes and it is the indexed phase on the event.
The beforeSwap half also has a precondition the afterSwap half does not. The kernel sizes it only for a swap carrying the canonical full-fill price limit for its direction, so a router that sets its own sqrtPriceLimitX96 for slippage protection takes the post-swap half alone and nothing is emitted for the half that did not run. HookrModularHookV6WthV5 carries the line.
An indexer that deduplicates corrections by transaction hash halves what it measures. Events for indexers covers the streams a pool produces.
The Reference Venue
A correction needs somewhere to trade against. The partner executor keeps its own registry of venues per pair, read through getTokenPools(address,address) (selector 0xeb1af5d2) and registeredPools(bytes32). For HOOKR against ETH that registry holds one venue: a hookless Uniswap v4 pool at fee 2,500 and tick spacing 25.
A pair with no registered venue has no gap to measure and nothing to trade against. Every swap on such a pool runs exactly as it would on the default root. A newly launched token is in that state by construction, so opening on a recapture root is a bet on a later listing rather than a feature that works on the first day.
Where the Profit Goes
realizedProfitQuote splits five ways, and which of two splits applies is decided by how the swap reached the pool.
| Recipient | Through the Hookr router | Any other route |
|---|---|---|
| The market's correction creator | 40% | 40% |
| The trader whose swap ran | 20% | nothing |
| The triggering pool's in-range LPs | 20% | 40% |
| WTH | 10% | 10% |
| Hookr | 10% | 10% |
The published policy library fixes all five rates and states the rounding rule in the same place:
uint16 internal constant CREATOR_BPS = 4_000;
uint16 internal constant TRADER_BPS = 2_000;
uint16 internal constant TRIGGER_POOL_LP_BPS = 2_000;
uint16 internal constant WTH_BPS = 1_000;
uint16 internal constant HOOKR_BPS = 1_000;Those are the rates. The rounding is a weaker claim than it looks and is worth stating carefully: HookrWthFeePolicyV2.allocate floors each named share and assigns the residual to Hookr, but nothing calls it. No Hookr contract in the correction path computes a payout at all, and allocate's own isValid guard rejects the fallback column, since it demands traderBps == 2000 and triggerPoolBps == 2000. The waterfall the executor actually applies is its own.
The CorrectionAttemptSucceeded event carries only the gross figure. The five legs are not separate events and have to be derived from it, so a dashboard showing them is computing them rather than reading them, and should say so rather than presenting a derived leg as reconciled to the wei.
Why the Route Changes the Split
A rebate needs somebody to pay it to. HookrKernelRouterV3 at 0xf0E528c39f33F565876cbaa7e0DFaCa38Df966E9 carries an authenticated payer and recipient in hookData, and that envelope is the only way a Hookr hook ever learns which wallet is behind a swap. A swap arriving through the Universal Router, an aggregator or any other contract leaves that recipient at the zero address, because the caller is a contract and a rebate paid to it would not reach the person who swapped. The trader's share then goes to the triggering pool's in-range liquidity providers instead.
This is the ordinary case for external flow, not an edge case. A router integrator picking a path is deciding on their user's behalf whether that user is rebated, and neither route is wrong: one pays the trader, the other pays the pool. Swapping and quoting covers what each route needs.
Swaps the Root Refuses
Before it corrects, the current recapture root's beforeSwap staticcalls the partner executor's MEV view and reverts the whole swap when that call succeeds and answers true. It accepts both spellings the executor has shipped, checkV3PoolsMev (0x6075521e) and checkV3PoolsMEV (0x85c30352).
A true answer means a registered v3 pool for this pair is locked for re-entrancy, so the caller is executing inside that pool's callback: it has already moved the reference venue in this transaction and is closing its other leg here. Refusing that swap is the one thing this root adds over the root before it. A revert from this check is the design working, not a fault, and a router that aggregates across several venues inside one transaction can meet it without doing anything wrong.
The check sits in the hook rather than in the adapter for a reason worth stating plainly. The adapter call happens inside the kernel's try/catch, where any revert becomes a skipped correction. A refusal raised there would be swallowed and the swap would settle.
The Trust Boundary
A recapture pool calls a contract Hookr did not write, inside the swap, in both phases. Five facts bound what that contract can do.
The adapter holds nothing. Hookr's own contract in the path is HookrWthExecutorAdapterV1 at 0x28AF7A3645080e926a3101461e0Ec0594D42D806. It forwards the call. It holds no funds and decides no amounts.
The executor is the partner's. The contract that prices the arbitrage, trades it and settles the payouts is WTH's, at 0xc356cf51134e0DF02BFE880115DD8c66Ead45803. Its source is not verified on the explorer.
The binding is spent. The adapter was bound to that executor once, with setExecutorOnce, and a second call reverts ExecutorAlreadySet() (0xa1002dd1), including from the owner's own address. The adapter a pool freezes can never be pointed somewhere else.
The call fails open. The kernel wraps the partner call in a try/catch, so a revert on the executor's side becomes a skipped correction and an event rather than a failed swap. This is the one deliberate fail-open path in a Hookr pool. Everything else in the swap fails closed, and Threat model says which way each failure falls.
The code that runs the lane is published, and unaudited. The current root's hook, its swap kernel, its correction library and the adapter are all in the public contracts repository's source set, each with its own source commit and sha256 in the manifest. None of them is in the audit scope, and the partner executor is neither published nor audited. Audit scope covers what is.
What Hookr Does Not Enforce
A pool on a recapture root freezes five correction fields into its StackLimits, and two of them read like caps without being caps.
correctionMaxVolumeBps sits at 5000, which is MAX_CORRECTION_VOLUME_BPS, the registry's own permissive ceiling. correctionMinProfitQuote sits at 1 wei, the smallest value the registry accepts, since it rejects zero. Both fields are frozen because the registry demands them for any non-zero executor, and neither reaches the trading decision: WTH's executor interface takes no volume cap and no profit floor.
Hookr does not enforce a trade size and does not enforce a profit floor. Sizing, the profit threshold and the decision to trade at all belong to the executor.
What a Correction Leaves Behind
| Event | When |
|---|---|
CorrectionAttemptSucceeded(poolId, phase, planDigest, realizedProfitQuote) | a correction ran and realised profit |
CorrectionAttemptFailed(poolId, phase, payloadHash, reasonHash) | a fail-open outcome, carrying no value |
CorrectionAttemptSkipped(poolId, phase, payloadHash, reasonHash) | a fail-open outcome, carrying no value |
poolId and phase are indexed on all three, and planDigest on the successful one. A swap on a recapture pool that leaves no correction event never entered the lane at all, and a swap that leaves a Failed or a Skipped settled normally.
The One Quote It Serves
Native ETH is the only quote currency a correction lane can be admitted on. The registry requires, for any non-zero correctionExecutor, that the pool's quote is address(0) and that currency0 is that quote while currency1 is the subject, because the partner executor reads the pair that way round. A pool quoted in USDG, in HOOKR or in a tokenized stock cannot carry the lane at all, whatever root it names.
Recapture therefore serves any token paired with ETH. It does not serve any pair. That is a narrower statement than the rest of this documentation makes about the native rules, which do work on every quote currency, and it is narrower on purpose.
Where It Stands
The root is deployed and its profile is sealed. The Hookr app does not offer it in the launch flow yet. Routing allowlists are granted per hook address, so nothing the default root carries reaches this one; Hooklist and routing has the current state.
To open a market on it, read Open a market with arbitrage recapture.