v4 made every pool a programmable surface.
In v3 a pool was just liquidity and a fee tier. In v4 a pool can call into an external hook contract at six lifecycle points — before a swap, after a swap, on every liquidity move, on donation. That means every pool ships with an attack surface as wide as the hook it points to.
The hard part isn't reading the spec. It's answering "what is this address actually allowed to do?" in two seconds, without leaving your wallet.
"A hook address is a permission system encoded into a vanity prefix. You can see what a hook is allowed to do without trusting anyone — if you know how to look."
Permissions are baked into the address.
v4 doesn't store hook permissions in a registry. It reads them from the bottom 14 bits of the hook's address. To get permission to override beforeSwap, a hook must be deployed (typically with CREATE2 + a salt grind) to an address whose 7th-from-last bit is 1.
0x96b893683afbfec071e90f78d00de4bb932fe0cc
Reading the chain, one window at a time.
HookScan's index is a list of Initialize events emitted by the v4 PoolManager at 0x000000000004444c5dc75cb358380d2e3de08a90. We page through 9,000-block windows of eth_getLogs calls against public nodes — no subgraph, no API key, no backend.
Initialize: eight fields, three indexed.
The PoolManager emits one log per new pool. Three fields are indexed (search-friendly), five sit packed into data with ABI alignment. The decoder is ~30 lines of vanilla JS — no ethers required.
event Initialize(
PoolId indexed id,
Currency indexed currency0,
Currency indexed currency1,
uint24 fee,
int24 tickSpacing,
IHooks hooks,
uint160 sqrtPriceX96,
int24 tick
);
const decodeInit = (log) => {
const data = log.data.slice(2);
return {
poolId: log.topics[1],
currency0: '0x' + log.topics[2].slice(-40),
currency1: '0x' + log.topics[3].slice(-40),
fee: parseInt(data.slice(58, 64), 16),
tickSpacing: parseInt(data.slice(122, 128), 16),
hooks: '0x' + data.slice(152, 192),
block: parseInt(log.blockNumber, 16),
};
};
The score is four numbers in a trench coat.
RISK_SCORE is deterministic. Same address, same score, every time. Four factors weighted into a 0–100 number, with every input visible on the card.
No ML, no opaque model. If you don't agree with a weighting, the formula lives in one file: buildFactors() in hookscan.js.
Your address is your account.
Connecting an EVM wallet binds a watchlist (and soon, on-chain reviews) to that address. We never request signatures for funds, never write to chain, never POST data anywhere. Disconnect leaves nothing behind.
- ✓EIP-1193 compatible · MetaMask, Rabby, Frame, Phantom EVM, Brave, OKX
- ✓localStorage only · watchlist keyed by your address, scoped per wallet
- ✓No silent signatures · you'll see
eth_requestAccountsonce, then nothing - ✓Real disconnect · revoke from your wallet, the site forgets immediately
Use the data layer directly from the console.
Every helper is exposed on window.HookScanData. Open DevTools on any HookScan page and you have a live v4 index in your console.
> HookScanData.getStats()
// { pools: 29503, hookedPools: 14374, hooks: 1969, ... }
> const pools = await HookScanData.getHookPools(
'0x96b893683afbfec071e90f78d00de4bb932fe0cc'
);
// returns every v4 pool initialized with that hook
> const { events } = JSON.parse(
localStorage.getItem('hookscan.index.v1')
);
// raw indexed events — go nuts
That's the entire docs. No subscription, no signup, no rug-pulls. Just a single page that decodes v4 hooks for whoever shows up.
Back to the dashboard.
Take it for a spin — scan a hook, browse the live rail, leave a comment.