Skip to content

feat: added pool key registration#164

Draft
mgretzke wants to merge 1 commit into
devfrom
fix-front-running-pool-key
Draft

feat: added pool key registration#164
mgretzke wants to merge 1 commit into
devfrom
fix-front-running-pool-key

Conversation

@mgretzke

@mgretzke mgretzke commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Reserve migration pool keys to prevent shared-key migration griefing

Addresses audit finding #1 ("Shared pool-key reuse can force launches into recovery instead of liquidity migration").

Problem

LBPStrategy never reserved a launch's final v4 pool key at registration. The pool was only initialized later during migrate(), so a competing launch sharing the same (currency, token, fee, tickSpacing, hook) could initialize the shared key first and force the victim's migration into the recovery path instead of creating liquidity.

Changes

  • Pool-key reservation at registration. initializeDistribution() now derives the final poolId and records registeredInitializers[poolId] = initializer, reverting PoolIdOccupied if the key is already taken. Replaces the previous per-initializer reserves mapping.
  • validateHook rejects already-initialized pools. The getSlot0 check now runs for every hook (including address(0)), so a doomed launch fails fast at registration.
  • Replay protection hardened. migrate() clears the reservation before attempting tryMigrate, so neither a successful migration nor a recovery can be replayed (closes a double-migrate path that could re-trigger recovery and pay out reservedTokenAmountForLP again from the shared singleton balance).
  • Removed the hookless → strategy-hook auto-fallback in _initializePool; the migration now uses exactly the reserved key.

Result

  • Two launches sharing a nonzero/reused hook can no longer race for the same pool key — the second collides at registration. ✅
  • Same-key migration can no longer force a sibling launch into recovery for hooked pools. ✅

Open question — should we block hook == address(0)?

The reservation is a strategy-side claim; it cannot bind the PoolManager. v4 has no pool-key reservation primitive — a key is uninitialized or initialized (one-way, permanent), and the only gate on who may initialize is the hook's beforeInitialize. A hookless (hook == address(0)) pool has no gate, so anyone can front-run poolManager.initialize on the raw key during the auction and force the launch into recovery — for the cost of a single call. This is exactly the reported PoC (vector 1) and it is not closed by reservation; removing the old fallback actually made it cheaper (no competing auction required).

Since a gated hook is the only v4 mechanism that can protect the key, proposal: require a hook (reject hook == address(0)), with options:

  1. Default to the strategy address as the hook — routing-friendly (one known, constant hook). Requires validateHook to allow hook == address(this) (skip the IInitializerHook ERC165/authorized() probe, since SelfInitializerMixin already self-gates init). Fully safe for fresh-token launches; mild residual registration-squat for already-circulating tokens (shared key, needs token supply, reroute via different fee/tickSpacing). Static-fee only.
  2. Per-launch unique hook (LBPStrategyHook, currently stubbed) — unique address per launch ⇒ unique poolId ⇒ no shared-key collisions at all. Eliminates the residual, at the cost of per-pool hook discovery for routing.

Decision needed before merge: keep hook == address(0) supported (and document it as best-effort / recover-only) or block it and pick option 1 or 2.

Follow-ups / nits

  • Stale docstring on _initializePool still describes the removed fallback.
  • Misleading comment at the PoolIdOccupied revert suggests address(0) / the strategy address as alternatives — address(0) is the unprotected option and the strategy address currently fails validateHook.

@mgretzke mgretzke requested review from dianakocsis and zhongeric June 5, 2026 16:34
Comment on lines +134 to +139
if (!ERC165Checker.supportsInterface(hook, type(IInitializerHook).interfaceId)) {
revert InvalidHook(hook);
}
if (IInitializerHook(hook).authorized() != address(this)) {
revert InvalidHook(hook);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you split these up for readability?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete this file? Not sure what this is for

Comment on lines +236 to +237
// Ensure the pool id is still registered, to prevent replay attacks
if (registeredInitializers[poolId] != address(initializer)) revert InitializerNotRegistered(initializer);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is to prevent a hook (for example) re-entering this function?

Comment on lines +452 to +474
function _createPoolKey(address currency, address token, uint24 lpFee, int24 poolTickSpacing, address hook)
private
pure
returns (PoolKey memory key)
{
return _createPoolKey(Currency.wrap(currency), Currency.wrap(token), lpFee, poolTickSpacing, hook);
}

function _createPoolKey(Currency currency, Currency token, uint24 lpFee, int24 poolTickSpacing, address hook)
private
pure
returns (PoolKey memory key)
{
key = PoolKey({
currency0: currency < token ? currency : token,
currency1: currency < token ? token : currency,
fee: lpFee,
tickSpacing: poolTickSpacing,
hooks: IHooks(hook)
});
return key;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No reason to have both of these imo.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants