HookrModularHookV6WthV5
Read about the live arbitrage-recapture root hook: its sealed profile, the correction lane in both phases, and the swap it refuses.
Deployed at 0xb914f955294799de4b891bd2EA8AF628Fa1c68CC on chain 4663. Its source is published in the public contracts repository, as src/HookrModularHookV6WthV5.sol together with the swap kernel it extends, src/HookrSwapKernelV5Wth.sol, and the library it links, src/libraries/HookrModularCorrectionLibV3.sol. There is no verified source on the explorer for it yet and no ABI route on this site, so every value on this page was read off the deployed contract, off 0x5b7f1A117A83aaac15B0698Aa4eB9D53fE5f5BA3, or off that published source, on 2026-09-12. See What Is and Is Not Published.
Inherits: HookrSwapKernelV5Wth, published alongside it
The current arbitrage-recapture root hook. One address serving every pool opened on it, running the same module the default root runs, plus a correction lane and one refusal.
A pool names exactly one root in PoolKey.hooks. Pools on this root and pools on 0xb3cA29cF721380CEe8b8e4755F3865Ebc68Fe8cC are the same kind of pool, admitted by the same coordinator against the same registry, with the same five native rules frozen in the same module. What this root adds is a step in the swap kernel: a correction executor is handed the pool and the swap, once before the swap is priced and once after it settles. Arbitrage recapture describes the mechanism; this page is the contract.
Identity
The same two pure getters every contract in the graph carries.
function contractName() external pure returns (string memory); // "HookrModularHookV6WthV5"
function contractVersion() external pure returns (string memory); // "6.0.0"The name is the one the deployed contract answers with. Three earlier recapture roots answer "HookrModularHookV6Wth" or "HookrModularHookV6WthV4", and two of them answer the same string as each other, so the name does not identify a root. The address does, and so does the kernel id. See The Superseded Roots.
Constants
| Name | Type | Value |
|---|---|---|
REQUIRED_FLAGS | uint160 | 0x28cc (10444) |
MEV_CHECK_GAS_STIPEND | uint256 | 200,000 |
KERNEL_FAMILY_ID | bytes32 | keccak256("HOOKR_SWAP_DELTA_V1") |
DYNAMIC_FEE_FLAG | uint24 | 0x800000 |
REQUIRED_FLAGS is the same fourteen-bit word the default root is mined to. A pool on this root asks the PoolManager for exactly the permissions a pool on the default root asks for: the correction needs none of its own. See Hook permissions.
MEV_CHECK_GAS_STIPEND is the gas this root forwards to the partner's MEV view, and nothing else. It is not the gas a correction costs.
Wiring, Read Back From the Chain
Four immutables, each readable at the root's own address. All four hold the same values the default root holds.
| Read | Value |
|---|---|
poolManager() | 0x8366a39CC670B4001A1121B8F6A443A643e40951 |
stackRegistry() | 0x5b7f1A117A83aaac15B0698Aa4eB9D53fE5f5BA3 |
coordinator() | 0x53A192A3fCeE94Da77916B461E0cCa2Dd6402442 |
accountingKernel() | 0x530523DEcC9523dDF8A87842fFBBD24a59C7E060 |
This is the fact most worth checking first, because it bounds what a second root can be. A recapture root does not bring its own registry, its own coordinator or its own accounting kernel. It delegates every non-swap selector to the same published HookrSwapAccountingKernelV3 the default root delegates to, at the same address, and the registry admits its pools under the same rules. What differs is the swap kernel compiled into the root itself, and the profile sealed against its kernel id.
The Sealed Profile
The root's kernel id is 0xd8b6c165b82efc3b7498081f071ea4f2476e2fb9c61b2e614aba2a016f94555a. rootProfile(kernelId) on 0x5b7f1A117A83aaac15B0698Aa4eB9D53fE5f5BA3 returns it sealed, and returns the same module set the default root's profile returns:
| Field | Value | Same as the default root |
|---|---|---|
isSealed | true | yes |
allowsExceptionalInstances | false | yes |
moduleCount | 1 | yes |
moduleSetHash | 0x8fafd562dd2b8dbb0d67a4d8cbc40c5cd2177836b0788028f8e855a8d96d8304 | yes |
routerIntegrationId | 0xc9ddc18b51d83cdafa6da985c67b4db41895dcde37b719597bdda9db6396c455 | yes |
quoterIntegrationId | 0x6e5a5b6aa6d141ccaf3b9f25f60e87919fc7a724cc845d146581eb49c079a9a9 | yes |
correctionExecutorIntegrationId | non-zero | no; the default root's is zero |
rootProfileModuleAt(kernelId, 0) returns 0x961091565aaf4eaa4996296fda9772957e61d18d90b965111a59ded2bbb5e774, the same HookrNativeMechanicsBlockV2 registration every default-root pool carries. The five native rules on this root are not a reimplementation: they are the same module, at the same address, under the same catalog registration.
The profile's correctionExecutorIntegrationId resolves through integration(integrationId) to implementation 0x28AF7A3645080e926a3101461e0Ec0594D42D806, registered under integrationKind keccak256("HOOKR_KERNEL_INTEGRATION_CORRECTION_EXECUTOR") in the HOOKR_SWAP_DELTA_V1 family at version 1. That adapter is the only correction executor a pool on this root may name. See HookrWthExecutorAdapterV1.
sealRootProfile succeeds once per kernel, so this root can never acquire a different executor. A different executor is a different root.
Callbacks
beforeSwap and afterSwap are handled in the root. Every other selector is forwarded to 0x530523DEcC9523dDF8A87842fFBBD24a59C7E060 by DELEGATECALL, exactly as on the default root, so beforeInitialize, beforeAddLiquidity, syncBaseFee, inFlight and previewQuoteTax are served at this address too and behave identically. See HookrModularHookV6 for that path.
On the two swap callbacks the sequence is: resolve and check the pool's frozen stack, authenticate the caller against the pool's trusted router and quoter at their registered code hashes, delegate the module walk to the accounting kernel, then run the correction lane. The module walk is what it is on the default root. The lane is what follows.
The Correction Lane
The lane runs in both phases. One swap can therefore produce two corrections, and they are separate attempts with separate outcomes, not one attempt reported twice.
| Phase | Constant | Value | When |
|---|---|---|---|
beforeSwap | HookrArbTypesV3.PHASE_BEFORE_SWAP | 1 | after the fee override is computed, before the swap is priced |
afterSwap | HookrArbTypesV3.PHASE_AFTER_SWAP | 2 | after the swap settles |
The value is the indexed phase on all three events, so an indexer separates the two without decoding anything. The two constants are published, in src/libraries/HookrArbTypesV3.sol.
An attempt needs two things: a non-zero trigger base amount derived from the swap, and a non-zero correctionExecutor in the pool's frozen StackLimits. HookrSwapKernelV5Wth.sol:213 and :249 call the lane on the trigger amount alone, and _tryCorrection at :435-449 returns early only when that executor is the zero address. Anything past those two conditions emits one of the three events.
A correction payload is not a third condition, and its optionality is the reason this root exists. _prepare at :263-304 decodes a payload only from the pool's registered router or quoter, each checked at its frozen code hash, so a swap arriving from the Universal Router, an aggregator or any other contract reaches the executor with empty correction bytes and a zero recipient, and HookrModularCorrectionLibV3.sol:82-87 gives it the fallback split, which :88-92 overrides only for an authenticated recipient. The kernel's own comment at :208-211 says why: requiring a payload in beforeSwap is what let an arbitrageur move the reference venue first and close here second. An integrator who reads external flow as unable to correct is describing the earlier kernel, not this root.
The call to the executor is wrapped in try/catch. A revert on the partner's side is caught, the lane's transient state is cleared, and a CorrectionAttemptFailed is emitted carrying keccak256 of the revert data. The swap proceeds. This is the one deliberate fail-open path in the system, and The correction trust boundary is where it is argued rather than only stated.
The Pre-Swap Half Needs the Canonical Price Limit
There is one precondition on the beforeSwap half that no other page states. _preSwapTriggerBase returns zero unless the swap carries the canonical full-fill price limit for its direction, which HookrSwapKernelV5Wth.sol:323 checks and :576-578 defines as MIN_SQRT_PRICE_LIMIT when zeroForOne and MAX_SQRT_PRICE_LIMIT otherwise. A zero trigger amount skips the pre-swap attempt and emits nothing at all.
So a router or aggregator that sets its own sqrtPriceLimitX96 for slippage protection, which is an ordinary thing to do, takes the afterSwap half only and leaves no event to show that the other half did not run. The afterSwap half is sized from the settled delta and carries no equivalent condition.
Gas Is the Caller's Problem on This Root
The library this root links has no gas gate. HookrModularCorrectionLibV2, which the default root links, skipped with SKIP_LOW_GAS whenever gasleft() was at or below EXECUTOR_GAS_STIPEND + DISPATCH_GAS_RESERVE (HookrModularCorrectionLibV2.sol:57 and :80). HookrModularCorrectionLibV3 declares SKIP_LOW_GAS at :55 and never references it, and its own note at :30-36 states the consequence plainly: dispatch does not check the gas remaining before calling the executor, so a swap sent with a fixed limit too small to cover the correction can run out of gas and revert.
That is deliberate. Gating on a fixed floor let any caller take the arbitrage by sending just under it. What it means for a router is that under-gassing a swap on this root is not a skipped correction, it is a failed swap, and Routing a correcting pool is where the sizing belongs.
While an attempt is in flight the executor re-enters the PoolManager against this same pool. The root recognises that nested callback by transient state, checks the sender and the pool id against what it stored, and returns a zero delta from it: BeforeSwapDeltaLibrary.ZERO_DELTA from the nested beforeSwap and 0 from the nested afterSwap. A nested callback that does not match reverts ReentrantCallback. The executor settles its own trade; the root moves no value on the correction's behalf.
The Refusal in beforeSwap
Before it corrects, this root asks the partner's executor whether the swap is arriving from inside a reference venue, and refuses the swap outright when the answer is a clean true. A clean true means a registered v3 pool for this pair is locked for re-entrancy, so the caller is inside that pool's callback: an arbitrage bot that moved the reference venue first and is closing its leg here.
It reaches the executor the same way everything else does, through the binding. The pool's frozen correctionExecutor is the adapter; the root staticcalls wthExecutor() on it with a 30,000 gas stipend, caches the answer in transient storage for the rest of the transaction, and staticcalls the MEV view on the address that comes back, with a 200,000 gas stipend. A zero adapter or a zero executor skips the whole step.
Two spellings are tried in order, and either one answering true is a refusal:
checkV3PoolsMev(address,address) 0x6075521e
checkV3PoolsMEV(address,address) 0x85c30352A call that reverts, returns nothing, returns a short answer or returns false is not an answer and does not refuse anything; the second spelling is tried after the first fails to answer.
Both spellings are therefore tried on almost every swap, and the page a reader is most likely to get backwards is this one. HookrSwapKernelV5Wth.sol:365-366 is _mevViewSaysTrue(bound, MEV_CHECK_SELECTOR, core) || _mevViewSaysTrue(bound, MEV_CHECK_SELECTOR_ALT, core), and || short-circuits only on true. The live executor 0xc356cf51134e0DF02BFE880115DD8c66Ead45803 carries checkV3PoolsMev, so on the ordinary not-locked swap the first spelling answers false and the alternate is tried anyway. Costing a swap on this root means costing three staticcalls on the partner: wthExecutor() at a 30,000 stipend, then both MEV spellings at 200,000 each.
A refusal reverts:
error MevCallbackRefused(address, address); // 0xcbd9472bThe two arguments are the address pair the view was asked about. This is the only Hookr revert a caller can hit for a reason outside its own transaction, so a router that aggregates across venues in one call should treat it as a route to drop rather than a bug. Routing a correcting pool says what to do with it.
The check is in the root and not in the adapter on purpose. The adapter is called from inside the kernel's try/catch, where a revert becomes a skipped correction rather than a failed swap, so a refusal expressed there would be swallowed by design. In the root it is a refusal, which is the point.
This is the one behaviour that separates this root from the fourth. It is also the reason ordinary MEV does not behave on this root as it does on any other v4 pool: Threat model scopes that.
Events
The three the lane emits, identical in name, argument types and indexing to the ones HookrSwapKernelV3 declares and the default root publishes in its ABI. The default root never emits them, because no pool on it carries an executor. This root does.
event CorrectionAttemptSucceeded(
PoolId indexed poolId, uint8 indexed phase, bytes32 indexed planDigest, uint256 realizedProfitQuote
);
event CorrectionAttemptFailed(PoolId indexed poolId, uint8 indexed phase, bytes32 payloadHash, bytes32 reasonHash);
event CorrectionAttemptSkipped(PoolId indexed poolId, uint8 indexed phase, bytes32 payloadHash, bytes32 reasonHash);PoolId is a bytes32 in the ABI. Events for indexers carries the topic0 of each one and the rest of what an indexer needs.
Every other event a pool emits is emitted by the same contracts that emit it on the default root, at the same addresses: the module's own events at 0xD700492b504ba5A72D7de28dDe11Cd7985a7F1ae, the accounting kernel's at the root's address, the router's at 0xf0E528c39f33F565876cbaa7e0DFaCa38Df966E9.
Errors
Declared in HookrSwapKernelV5Wth.sol and recovered from the deployed runtime bytecode, which agree. There is still no published ABI for this root, so an integrator decoding a revert works from the selectors rather than from a JSON artifact.
| Error | When |
|---|---|
MevCallbackRefused(address, address) | the partner's MEV view answered true for this pair, so the swap is refused in beforeSwap |
InvalidStack() | the pool has no initialized stack for this root, the PoolKey disagrees with it, or a trusted integration's code hash has changed |
ReentrantCallback() | a nested callback arrives outside the correction lane's expected state |
InvalidPayload() | a trusted caller's correction payload is malformed |
AccountingKernelCodeChanged(bytes32 expected, bytes32 actual) | the accounting kernel's runtime code hash differs from the one pinned at construction |
DelegateCallFailed() | a delegation fails with no revert data |
InvalidWiring() | a constructor argument is zero or has no code; unreachable after deployment |
MevCallbackRefused, InvalidStack, ReentrantCallback, AccountingKernelCodeChanged, DelegateCallFailed and InvalidWiring are declared at HookrSwapKernelV5Wth.sol:134-140. InvalidPayload() is not the kernel's: it is declared at HookrCorrectionPayloadV2.sol:11 and surfaces through the kernel when a trusted caller's envelope is malformed.
The accounting kernel's own reverts surface unchanged through the fallback, exactly as they do on the default root. Errors lists those, generated from the published ABIs.
What Hookr Does Not Enforce
A pool on this root freezes correctionMaxVolumeBps and correctionMinProfitQuote in its StackLimits, and neither is a limit this root applies. The registry requires the fields to be present and inside its bounds; nothing downstream reads them as a cap, because the executor's interface takes neither number. Live pools carry 5000 bps, the registry's permissive maximum, and 1 wei, its smallest legal floor. Do not describe either as a cap Hookr enforces. The fields are documented under HookrStackRegistryV2.
What Is and Is Not Published
Named here because a reviewer needs to know exactly where the published set stops.
Published in the public contracts repository, and checked into its source manifest with a per-file source commit and sha256: src/HookrModularHookV6WthV5.sol, src/HookrSwapKernelV5Wth.sol, src/libraries/HookrModularCorrectionLibV3.sol, src/HookrWthExecutorAdapterV1.sol, src/interfaces/IWthArbitrageExecutorV1.sol and src/interfaces/IHookrExecutionClockV1.sol. The interface, request types, payload encoding and fee-policy preimage the lane commits to were already published, as IHookrArbExecutorV3, HookrArbTypesV3, HookrCorrectionPayloadV2 and HookrWthFeePolicyV2.
Not published, and each of these matters:
- The partner executor's source.
0xc356cf51134e0DF02BFE880115DD8c66Ead45803is WTH's contract and the published set stops at the adapter's boundary. What it does is observable: every correction emits on this root, and the payout proportions reproduce from the constants above. - A verified explorer entry for this root. The rebuild from the published source is the evidence today; the root's own verification attempt is recorded as pending. Treat the source manifest and the rebuild as the check, not the explorer.
- The three superseded roots' sources. Only the current root's contracts are exported. A reader working on a pool that names one of the other three has the deployed bytecode and nothing else.
HookrArbSysBlockClockV1, the execution clock the adapter reads. It is not in the export.
One further caveat is worth reading before relying on the manifest as third-party provenance: see The correction trust boundary.
The Superseded Roots
Three earlier recapture roots are still deployed, still sealed, and still hold open pools that trade. None of them was paused or withdrawn, so an indexer or a router that filters on one root address will miss them.
They do not all still correct, and the table below is the difference. Counting recapture across roots as though all four lanes run over-counts the two that do not.
| Root | Address | What it does not do |
|---|---|---|
| Current | 0xb914f955294799de4b891bd2EA8AF628Fa1c68CC | |
| Fourth | 0xE5429dB8f63912E632E86733905667AaEb6ea8cC | does not refuse a swap arriving from inside a registered v3 pool's callback |
| Third | 0xa99902a2922014bBe2Bf2dCF15742ac5104828Cc | does not correct in beforeSwap, only after a swap settles |
| First | 0xc7c516CD5546bCB2592Fe3f8aa91C2A4bA3768CC | corrects on neither phase in practice: an attempt needs more gas than an ordinary swap carries |
Every one of them carries the same 0x28cc flag word, shares the same coordinator, registry and accounting kernel, and seals the same single module. Each has its own kernel id and its own adapter, bound to its own partner executor: read the adapter from the registry rather than assuming, with rootProfile(kernelId).correctionExecutorIntegrationId and then integration(integrationId).implementation.
A root is superseded, not retired. Nothing on chain marks one, and nothing stops a swap on a pool that names one.
Address Mining
The address satisfies address & 0x3fff == 0x28cc, mined the same way the default root's is, through HookrReleaseCreate2FactoryV1 at 0xBc54e888C1A5B71744B035DfeC5611B4DC69ccDe. Two roots sharing a flag word share their permissions and nothing else: allowlisting is per hook address, and Hooklist and routing is where that stands.