Open the app

HookrWthExecutorAdapterV1

Read about the Hookr contract a correcting pool names: its one-shot binding, its views and constants, and what it does not hold.

Deployed at 0x28AF7A3645080e926a3101461e0Ec0594D42D806 on chain 4663. Its source is published in the public contracts repository as src/HookrWthExecutorAdapterV1.sol, with its own source commit and sha256 in the source manifest; there is no verified source on the explorer yet and no ABI route on this site. Every value on this page was read off the deployed contract or off 0x5b7f1A117A83aaac15B0698Aa4eB9D53fE5f5BA3 on 2026-09-12, and every selector was recovered from the deployed runtime bytecode and checked against that source.

Version: "1.1.0"

The contract standing between a Hookr root hook and a partner's correction executor. It names one executor, forever, and holds nothing.

This is the contract a pool on a recapture root actually freezes in StackLimits.correctionExecutor. The partner's own executor is never named by a pool and never registered with Hookr; the adapter is. Shipping a correction executor is the same wiring described as a procedure for a partner team.

Identity

function contractName() external pure returns (string memory);   // "HookrWthExecutorAdapterV1"
function contractVersion() external pure returns (string memory); // "1.1.0"

Four adapters at this name and version are deployed on chain 4663, one per recapture root, each bound to a different partner executor. The name does not identify one. Resolve the adapter a given root uses from the registry: rootProfile(kernelId).correctionExecutorIntegrationId, then integration(integrationId).implementation.

The Binding

One call, and it is already spent.

function setExecutorOnce(address executor) external; // 0x76e5397a
function wthExecutor() external view returns (address); // 0x477a9d7c

wthExecutor() on this adapter returns 0xc356cf51134e0DF02BFE880115DD8c66Ead45803, and setExecutorOnce reverts ExecutorAlreadySet(), selector 0xa1002dd1, from the owner who made the first call.

Reproduce it from the owner's address and not from your own. HookrWthExecutorAdapterV1.sol:125-126 is function setExecutorOnce(address executor_) external onlyOwner { if (wthExecutor != address(0)) revert ExecutorAlreadySet(); … }, and onlyOwner is evaluated first, so a call from any other address reverts NotOwner(), selector 0x30cd7471, and proves nothing about the binding. An eth_call from 0xF4Ab4698554D5c95874986d5e956c62e5E6aB3eE is the check; wthExecutor() returning a non-zero address is the same fact without the simulation. The adapter can never point anywhere else, and because its address sits inside a sealed root profile, neither can the root.

This is the strongest safety property the integration has, and it is checkable in one read by anyone. The root reads it the same way: before the refusal in beforeSwap it staticcalls wthExecutor() here with a 30,000 gas stipend and caches the answer in transient storage, so the executor the MEV view is asked is the executor this binding names and nothing else. A zero answer from wthExecutor() means an unbound adapter, not an outage: the app's own reader treats an RPC failure as a third state rather than folding it into the zero case.

Views

Everything below is view and takes no arguments.

FunctionSelectorValue on this adapter
wthExecutor()0x477a9d7c0xc356cf51134e0DF02BFE880115DD8c66Ead45803
feePolicyId()0xc893dd970xd2653e091cb7002585fd8b1192b58f11b9c951061dc797123e37d5e2ccb45cef
routeAdmissionOpen()0xc93b3f0ftrue
executionBlockNumber()0x171c174bthe chain's own block height, see below
executionClock()0xff820f33the clock the line above is read from
stackRegistry()0x6fc2be500x5b7f1A117A83aaac15B0698Aa4eB9D53fE5f5BA3
poolManager()0xdc4c90d30x8366a39CC670B4001A1121B8F6A443A643e40951
integrationKind()0x0cd82cd4keccak256("HOOKR_KERNEL_INTEGRATION_CORRECTION_EXECUTOR")
integrationFamilyId()0x36b6b7f3keccak256("HOOKR_SWAP_DELTA_V1")
integrationVersion()0x6c47fc4d1
owner()0x8da5cb5b0xF4Ab4698554D5c95874986d5e956c62e5E6aB3eE
pendingOwner()0xe30c3978the zero address

The same three integration values are also exposed as the constants INTEGRATION_KIND(), INTEGRATION_FAMILY_ID() and INTEGRATION_VERSION(). registerIntegration on 0x5b7f1A117A83aaac15B0698Aa4eB9D53fE5f5BA3 round-trips the lowercase three and refuses a registration whose metadata disagrees with them, which is why they are on the contract at all. See HookrStackRegistryV2.

feePolicyId() is the value the registry checks a pool's correctionFeePolicyId against at admission. HookrWthFeePolicyV2.FEE_POLICY_ID builds it in the public contracts repository, as keccak256 of a version-tagged string naming all five shares in bps; read the preimage there rather than retyping it. All four deployed adapters answer the same policy id, and on the deployed registry it is the only one any pool can carry: HookrStackRegistryV1.sol:1150 reverts InvalidStackLimits() unless a pool's correctionFeePolicyId equals HookrWthFeePolicyV2.FEE_POLICY_ID literally, and HookrStackRegistryV2 does not override that check. The comparison against the executor's own feePolicyId() at :1153-1157 runs only after it.

So a different waterfall is not a different registration and a new root. It is a different registry. A partner designing an executor around its own split should read Shipping a correction executor before writing any code against it.

Constants

NameValue
SPLIT_TOTAL_BPS()8,000
EXECUTOR_CALL_GAS_LIMIT()2,200,000
ADAPTER_GAS_RESERVE()60,000

8,000 bps is the sum of the three shares the correction request carries: creator 40%, trader 20%, trigger-pool LP 20%, fixed by HookrWthFeePolicyV2. It stays 8,000 on a swap with no authenticated trader, because the trader's share moves to the trigger pool's LPs rather than out of the total. The remaining 2,000 bps, 10% to the partner and 10% to Hookr, is not in the request: the executor allocates it itself, and Hookr takes the rounding residual.

The two gas numbers bound the inner call, and HookrWthExecutorAdapterV1.sol:184-187 is how: the adapter reverts InsufficientGas() when gasleft() is at or below ADAPTER_GAS_RESERVE, and otherwise hands the executor min(gasleft() - ADAPTER_GAS_RESERVE, EXECUTOR_CALL_GAS_LIMIT). The reserve is what the adapter keeps so it can always return or bubble a revert; the ceiling is what a misbehaving executor cannot exceed.

The stipend that reaches the adapter is HookrModularCorrectionLibV3.EXECUTOR_GAS_STIPEND, 2,300,000 (HookrModularCorrectionLibV3.sol:29), which is 40,000 more than the 2,260,000 the adapter can consume before it returns. The library's own header comment at :20-26 calls that sum exact and is wrong about it; the constants are the authority, and the 40,000 is slack, not a gap the reserve fails to cover. A swap sent with a tight gas limit will not carry a correction on this lane, and on this root that is a reverted swap rather than a skipped correction: Routing a correcting pool has the sizing.

The Execution Clock

executionBlockNumber() does not return block.number. It forwards to the contract executionClock() names, which is a HookrArbSysBlockClockV1: its only upstream is the ArbSys precompile at address 0x64, and it returns arbBlockNumber() narrowed to a uint64. Read on 2026-09-12 it equalled the chain's eth_blockNumber exactly.

The indirection exists because this is an Orbit chain, where block.number inside the EVM reports the settlement chain's height rather than this chain's. A correction plan carries a maxBlock and a deadline, so a plan checked against the wrong clock would expire against the wrong number. The clock's own source is not in the public contracts repository: the export carries src/interfaces/IHookrExecutionClockV1.sol and not the implementation. An integrator comparing a plan's maxBlock against a height should read executionBlockNumber() off the adapter rather than block.number, and treat the ArbSys derivation above as read from the deployed bytecode and from this adapter's answer, not from published source.

The Correction Call

The adapter implements the published executor interface, so the kernel calls it exactly as it would call any correction executor:

function executeArbitrage(HookrArbTypesV3.ExecutionRequest calldata request)
    external
    returns (uint256 realizedProfitQuote, bytes32 planDigest); // 0xaad72179

IHookrArbExecutorV3 and HookrArbTypesV3 are both published in the public contracts repository, and Shipping a correction executor walks the request field by field.

The adapter does not forward that request. It narrows it. HookrWthExecutorAdapterV1.sol:189-193 encodes IWthArbitrageExecutorV1.executeArbitrage(request.targetKey, request.rebateRecipient, split) and nothing else, so the swap's direction and trigger size, the pool's frozen correctionMaxVolumeBps and correctionMinProfitQuote, and the plan with its r, s and v never reach the executor. realizedProfitQuote is the single word the executor returns, checked at :195-196 and reported as the gross in CorrectionAttemptSucceeded; planDigest is not the executor's either, but keccak256(abi.encode(targetKey, rebateRecipient, split)) computed at :197.

The adapter holds no funds, takes no fee, and decides no amount. It does not choose the venue, the size, the direction or the price limit, and it cannot move a pool's balances: the executor settles its own trade against the PoolManager and the root returns a zero delta from the nested callback. What the adapter contributes is the one thing the partner's own contract cannot: a Hookr-side address that the registry can admit and that can never be repointed.

Errors

The twelve the contract declares, at HookrWthExecutorAdapterV1.sol:59-70. The seven with selectors below were also recovered from the deployed runtime bytecode and match.

ErrorSelectorWhen
ExecutorAlreadySet()0xa1002dd1setExecutorOnce after the bind, from the owner
ExecutorNotSet()0xaf47ef43a correction arrives before the bind
ExecutorCallFailed()0x225d570fthe executor reverted with no data
NotTargetHook()0xec99d185the caller is not the hooks address of the PoolKey it passed
NotRegisteredKernel()0x0b8cdec5the caller's pool does not name this adapter under this fee policy at these code hashes
NotOwner()0x30cd7471a non-owner calls proposeOwner or setExecutorOnce
ZeroAddress()0xd92e233da zero or code-less constructor argument, proposed owner or executor
NotPendingOwner()acceptOwnership from anyone but the proposed owner
BadSplit()the three named shares do not sum to 8,000, a share has no recipient, or the creator is not the pool's frozen correctionCreator
BadExecutorReturn()the executor returned anything but one word
InsufficientGas()at most ADAPTER_GAS_RESERVE remains on entry to the forward
InvalidExecutionClock()the constructor's clock does not answer executionBlockNumber()

ExecutorAlreadySet() is the one a third party can demonstrate, and only from the owner's address; from any other caller onlyOwner answers first with NotOwner().

None of these reaches a swapper as a failure. The root wraps the whole correction call in try/catch, so an adapter revert is caught, reported as CorrectionAttemptFailed with keccak256 of the revert data, and the swap settles anyway. The correction trust boundary is where that fail-open choice is argued.

Ownership

Two-step, and the same owner as the rest of the graph.

function proposeOwner(address candidate) external; // 0xb5ed298a
function acceptOwnership() external;               // 0x79ba5097

owner() is 0xF4Ab4698554D5c95874986d5e956c62e5E6aB3eE and pendingOwner() is the zero address, read on 2026-09-12. The owner's reach on this contract is ownership itself: the binding is already spent, so there is no owner call that changes which executor a pool reaches. See Immutability and ownership.