Hook Miner
A v4 hook’s permissions are not stored anywhere: they are the lowest 14 bits of
its address. The PoolManager calls a hook function only if the matching bit is
set. To ship a hook with beforeSwap and afterSwap, you need to deploy it to
an address ending in exactly ...00c0 (masked to 14 bits).
The Hook Miner finds a CREATE2 salt that produces such an address. On average it takes 2^14 (about 16,000) hashes.
POST /v1/hooks/mineRequest
Section titled “Request”| Field | Required | Notes |
|---|---|---|
flags |
yes | Permission names (["beforeSwap","afterSwap"]) or a 14-bit mask (192, "0x00c0") |
initCodeHash |
one of | keccak256(creationCode ++ abi.encode(constructorArgs)) |
initCode |
one of | The full init code; HookKit hashes it for you |
deployer |
no | CREATE2 deployer address. Default: the deterministic deployer 0x4e59b44847b379578588920cA78FbF26c0B4956C |
startSalt |
no | Integer to start searching from. Default 0 |
maxIterations |
no | Up to 400000. Default 200000 |
The mined address must match exactly: bits you did not ask for are zero, so the hook gets no permissions you did not request.
curl -X POST https://api.hookkit.dev/v1/hooks/mine \ -H "content-type: application/json" \ -d '{ "flags": ["beforeSwap", "afterSwap"], "initCodeHash": "0xabababababababababababababababababababababababababababababababab" }'{ "found": true, "salt": "0x0000000000000000000000000000000000000000000000000000000000001613", "address": "0x8741504Dd906BC4af41A4eC5096aBC42e0b640c0", "iterations": 5652, "flags": 192, "flagsHex": "0x00c0", "permissions": { "beforeSwap": true, "afterSwap": true, "...": false }, "deployer": "0x4e59b44847b379578588920cA78FbF26c0B4956C", "initCodeHash": "0xabababababababababababababababababababababababababababababababab", "ms": 26, "verify": true, "note": "Deploy via the CREATE2 deployer: send salt ++ initCode as calldata to 0x4e59b44847b379578588920cA78FbF26c0B4956C, or `new Hook{salt: salt}(...)` from a Foundry script."}verify is HookKit recomputing the CREATE2 address from deployer, salt, and
initCodeHash. If no salt is found within maxIterations, the response is:
{ "found": false, "iterations": 200000, "nextSalt": "200000", "flags": 192, "note": "No match within maxIterations; retry with startSalt = nextSalt." }Invalid flags
Section titled “Invalid flags”A *ReturnDelta flag is only valid together with its base flag. The miner
rejects combinations the PoolManager would reject:
curl -X POST https://api.hookkit.dev/v1/hooks/mine \ -H "content-type: application/json" \ -d '{ "flags": ["afterSwapReturnDelta"], "initCodeHash": "0xabab...abab" }'{ "error": { "code": "invalid_flags", "message": "afterSwapReturnDelta requires afterSwap" } }The init code hash
Section titled “The init code hash”The salt is only valid for the exact init code you mined it for: the contract’s creation code plus its ABI-encoded constructor arguments. Change a constructor argument (for example the PoolManager address) and you need a new salt.
With Foundry:
# creation code + abi-encoded constructor args (PoolManager on Robinhood Chain)INIT_CODE=$(cast concat-hex \ $(forge inspect src/MyHook.sol:MyHook bytecode) \ $(cast abi-encode "constructor(address)" 0x8366a39cc670b4001a1121b8f6a443a643e40951))
cast keccak $INIT_CODEOr with viem:
import { encodeDeployData, keccak256 } from "viem"
const initCodeHash = keccak256( encodeDeployData({ abi, bytecode, args: ["0x8366a39cc670b4001a1121b8f6a443a643e40951"] }),)Deploy with Foundry
Section titled “Deploy with Foundry”Mine the salt, then deploy with new MyHook{salt: salt}(...) from a
forge script. Foundry routes new ... {salt: ...} in scripts through the
deterministic deployer 0x4e59b448..., which is the HookKit default, so the
address matches.
pragma solidity ^0.8.26;
import {Script} from "forge-std/Script.sol";import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";import {MyHook} from "../src/MyHook.sol";
contract DeployHook is Script { IPoolManager constant POOL_MANAGER = IPoolManager(0x8366a39cc670b4001a1121b8f6a443a643e40951);
function run() external { // From POST /v1/hooks/mine bytes32 salt = vm.envBytes32("HOOK_SALT"); address expected = vm.envAddress("HOOK_ADDRESS");
vm.startBroadcast(); MyHook hook = new MyHook{salt: salt}(POOL_MANAGER); vm.stopBroadcast();
require(address(hook) == expected, "DeployHook: address mismatch"); }}HOOK_SALT=0x...1613 HOOK_ADDRESS=0x... \forge script script/DeployHook.s.sol \ --rpc-url https://rpc.mainnet.chain.robinhood.com \ --broadcast --account deployerWithout Foundry, send a plain transaction to
0x4e59b44847b379578588920cA78FbF26c0B4956C with calldata salt ++ initCode.
After deploying, confirm the permissions with
GET /v1/hooks/:address.
import { HookKit, flagsFor } from "@hookkit/sdk"
const hookkit = new HookKit()
const mined = await hookkit.hooks.mine({ flags: ["beforeSwap", "afterSwap"], // or flagsFor(["beforeSwap", "afterSwap"]) === 0xc0 initCodeHash,})
if (mined.found) console.log(mined.salt, mined.address)