Skip to content

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/mine
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.

Terminal window
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." }

A *ReturnDelta flag is only valid together with its base flag. The miner rejects combinations the PoolManager would reject:

Terminal window
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 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:

Terminal window
# 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_CODE

Or with viem:

import { encodeDeployData, keccak256 } from "viem"
const initCodeHash = keccak256(
encodeDeployData({ abi, bytecode, args: ["0x8366a39cc670b4001a1121b8f6a443a643e40951"] }),
)

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.

script/DeployHook.s.sol
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");
}
}
Terminal window
HOOK_SALT=0x...1613 HOOK_ADDRESS=0x... \
forge script script/DeployHook.s.sol \
--rpc-url https://rpc.mainnet.chain.robinhood.com \
--broadcast --account deployer

Without 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)