For hook builders
What the market will see when it scans your hook, and how to build one that passes each check without publishing anything you do not want to.
Anyone can scan your hook, with or without your source. The report they see is built only from your address, your bytecode and what your hook does to a real swap. This page goes through the checks from the builder's side.
A safe verdict means safe by construction: no return delta and no beforeRemoveLiquidity, a take of 1% or less, LPs who come back whole (or a pool closed to them), a fixed fee of 1% or less, no owner and no owner-style functions, no proxy, no DELEGATECALL or SELFDESTRUCT, and every callback guarded. Any caution on checks 01 to 07 makes the verdict caution; a caution on 08 alone does not.
Before launch
A scan needs a deployed hook on Robinhood Chain and, for checks 02 and 03, a pool with liquidity at its price. Deploy, create a pool, add liquidity, then open /app/hook/<your address>. You will see what a trader will see. Check 08 will be a caution for the first week: that is expected for every new hook, and it does not change the verdict.
01 Permissions that touch your money
Mine only the bits you use. A return-delta permission you do not need costs you a caution on 01 and buys nothing.
If you need a return delta (a launch tax, a buyback, a rebate), 01 will be a caution, and so will your verdict: safe means nothing that may take, and a hook that can take is honestly described as a caution. That is a description, not a penalty. What matters next is 02 and 03, which measure what the hook does. Keep the delta bounded by a constant in the code, and say in your documentation what it is for and where it goes.
02 What it takes from a swap
- Keep the take at 1% or less for a pass. Up to 10% is a caution; above 10% is a fail.
- Never refuse one direction. A hook that lets the buy through and reverts the sell fails, whatever its reasons (a cooldown, an anti-bot window, a max-wallet rule). If your design restricts selling, the report will say so, and it should.
- Do not gate swaps by sender. A hook that only accepts swaps from an approved router refuses the probe in both directions, which fails. A router whitelist also means every aggregator that is not on it cannot route through you.
- Document decaying taxes. If your tax starts high and falls, a scan shows its value at the block it ran, and the report improves as the tax falls. Publish the curve so people can read the report in context.
03 LPs can get out
- Do not block withdrawals. A refused withdrawal is a fail. Avoid
beforeRemoveLiquidityunless you need it, and if you have it, do not let it revert for an ordinary LP. - Do not charge LPs on the way out through
afterRemoveLiquidityReturnDeltaunless that is the product, and if it is, keep it small: above 5% is a fail. - If the pool is closed by design, refuse deposits in
beforeAddLiquidity. A closed pool makes 03 n/a, not a fail. A pool that accepts deposits and then refuses the withdrawal is the case that fails.
04 The fee is the fee
Check 04 judges the fee's level as well as its kind:
- a fixed fee of 1% or less passes;
- a fixed fee above 1% is a caution;
- a dynamic fee is a caution at any level up to 10%, because the hook sets it and it can change;
- a pool fee above 10%, fixed or dynamic, fails.
Use a fixed fee in the pool key if your design allows it. If you need a dynamic fee, bound it in the code well below 10% and document the rule that sets it. Fee and take are judged separately: a dynamic fee cannot be used to hide a cost that would otherwise show up as take, since 04 reads the fee where it stands.
05 No one holds the switch
- No owner is a pass. If the hook needs no configuration after deployment, set everything in the constructor and do not inherit an
Ownableyou will not use. - Renounce ownership once configuration is done, so
owner()returns the zero address. - If you must keep control, put it behind a timelock so changes are announced before they happen. The check, and so the verdict, will still be a caution, since
owner()still returns an address, but the people who read the report can see that the owner is a timelock.
Renaming a setter will get it past the name match. It will not make it any less a setter, and the report lists every selector anyway. Do not do it.
06 The code cannot change
- No proxy. An upgradeable hook fails 06, and nothing else on the report can outweigh it. If you need to change behaviour, deploy a new hook and new pools.
- No DELEGATECALL. External library calls compile to DELEGATECALL; use internal library functions instead.
- No SELFDESTRUCT.
07 Only the PoolManager can call it
Every callback must refuse any caller other than the PoolManager. The standard base contract for hooks does this for you with an onlyPoolManager modifier. If you write your own, put the check on every callback, including the ones that do nothing:
modifier onlyPoolManager() {
if (msg.sender != address(poolManager)) revert NotPoolManager();
_;
}
The scan calls each callback your address switches on from a stranger. One that answers fails the check.
08 Track record
Nothing to build: time and use. At least three pools and seven days. Until then 08 lowers the score a little and leaves the verdict alone.
Publishing your source
Verifying your source on the explorer is not part of the scan or the score, and the scan does not need it. It is still the best thing you can do for the people who use your hook, and nothing in censoredHooks replaces it.
For builders who want to keep the source private and still give the market more than a bytecode reading, a private review is planned: the source stays with you, and you get the same checks plus a signed review of what the code does. It is not available yet.