Summary
lib/util/hooksAddressesAllowlist.ts admits hooks by address. Around 180 addresses are listed across chains, and each entry is a decision that the code at that address was reviewed and found acceptable to route user trades through.
The gap is that an address is not code. A hook deployed behind a proxy can be reviewed as benign and later point at a different implementation, and the allowlist entry keeps passing because the address never changed. Whatever review preceded admission then applies to a program that is no longer the one executing.
This is not hypothetical for hooks specifically: a hook is the one contract in the v4 swap path that can be given permission to alter the accounting, so it is also the contract where a silent implementation swap is worth the most to an attacker.
Suggested change
Record the code hash alongside the address at review time, and verify it before routing.
Because routing-api is off-chain, this is cheap in a way the on-chain version is not: one eth_getCode (or a keccak256 of it) per admitted hook, cached, refreshed on whatever cadence you already use for chain state. A mismatch means the reviewed code is not the deployed code, and the safe response is to drop that hook from the route set and alert, rather than to route through it.
Concretely, the allowlist entries would carry a pair rather than a bare address:
{ address: '0x...', codeHash: '0x...' } // codeHash recorded at review
and the routing path would filter on keccak256(await getCode(address)) === codeHash.
Two details that matter in practice:
- Fail closed. If the code hash cannot be read, the hook should be excluded rather than admitted — an RPC failure should not become an admission.
- Rotation is a review event. A legitimate upgrade produces a mismatch, which is correct: it should re-enter review rather than pass silently. That is the property being bought.
A related, much cheaper filter, if it is not already applied: BEFORE_SWAP_RETURNS_DELTA_FLAG and AFTER_SWAP_RETURNS_DELTA_FLAG are encoded in the hook's own address, so delta-returning hooks can be identified by bitmask with no call at all. Whether they should be routed is a policy question rather than a safety one, but separating them at least makes the decision explicit.
Context
We hit this while integrating v4 into an aggregator and ended up implementing the on-chain version — code hash pinned at admission, re-checked at routing time, with a timelocked two-step path for delta hooks: hook-safety-gate (MIT, zero dependencies).
Not proposing you adopt that contract — an off-chain router does not need it, and the on-chain design carries an owner-controlled admission step that would make no sense here. The part that seemed worth raising is the property, not the implementation.
Happy to open a PR adding the code-hash field and the filter if that would be useful, or to leave it as a note if you have already considered and rejected it — in which case the reasoning would be genuinely interesting to us.
Summary
lib/util/hooksAddressesAllowlist.tsadmits hooks by address. Around 180 addresses are listed across chains, and each entry is a decision that the code at that address was reviewed and found acceptable to route user trades through.The gap is that an address is not code. A hook deployed behind a proxy can be reviewed as benign and later point at a different implementation, and the allowlist entry keeps passing because the address never changed. Whatever review preceded admission then applies to a program that is no longer the one executing.
This is not hypothetical for hooks specifically: a hook is the one contract in the v4 swap path that can be given permission to alter the accounting, so it is also the contract where a silent implementation swap is worth the most to an attacker.
Suggested change
Record the code hash alongside the address at review time, and verify it before routing.
Because
routing-apiis off-chain, this is cheap in a way the on-chain version is not: oneeth_getCode(or akeccak256of it) per admitted hook, cached, refreshed on whatever cadence you already use for chain state. A mismatch means the reviewed code is not the deployed code, and the safe response is to drop that hook from the route set and alert, rather than to route through it.Concretely, the allowlist entries would carry a pair rather than a bare address:
and the routing path would filter on
keccak256(await getCode(address)) === codeHash.Two details that matter in practice:
A related, much cheaper filter, if it is not already applied:
BEFORE_SWAP_RETURNS_DELTA_FLAGandAFTER_SWAP_RETURNS_DELTA_FLAGare encoded in the hook's own address, so delta-returning hooks can be identified by bitmask with no call at all. Whether they should be routed is a policy question rather than a safety one, but separating them at least makes the decision explicit.Context
We hit this while integrating v4 into an aggregator and ended up implementing the on-chain version — code hash pinned at admission, re-checked at routing time, with a timelocked two-step path for delta hooks: hook-safety-gate (MIT, zero dependencies).
Not proposing you adopt that contract — an off-chain router does not need it, and the on-chain design carries an owner-controlled admission step that would make no sense here. The part that seemed worth raising is the property, not the implementation.
Happy to open a PR adding the code-hash field and the filter if that would be useful, or to leave it as a note if you have already considered and rejected it — in which case the reasoning would be genuinely interesting to us.