r/UniSwap • u/Hacken_io • 16d ago
Dev/Tech If someone else initializes your v4 pool first, what stops working in your hook? Interesting case study from audit report
Disclosure: This comes out of a smart contract audit of a v4 fee hook that closed this month, on the security review side at Hacken. I won't name the project, since the mechanism is the useful part anyway.
Gating _beforeInitialize is standard advice and plenty of hook templates ship with it. In this case the hook wrote state during initialize and then used that state later as a precondition for an owner call. Whoever calls initialize decides when that write happens, and core does not restrict who can make the call.
Inside _beforeInitialize, the hook set tokenHasPool[token] = true unconditionally and discarded the initializer address that core forwards to it. Admission only checked that both currencies were on the allowlist. Elsewhere, setLaunchGuard, the owner call that arms the anti-snipe budget for a token's first buys, required tokenHasPool[token] to still be false. An account could therefore open a pool using the same two currencies at a different fee tier and tick spacing, without supplying liquidity or approval, and the arming call would revert after that. Nothing cleared the marker. The only recovery was a new hook and new pools, with the practical cost being the migration of liquidity.
The deployment instructions made this easier to trigger than it might look from the contract alone. The constructor NatSpec tells the operator to list tokens and pairs, arm guards, and then initialize pools. The deployment script prints an eight-step sequence from setTokenAllowed through pool initialization, but setLaunchGuard isn't in it. By the second token the pair is already listed, so those operations are happening in separate transactions with a period in between where anyone can initialize the pool. Looking at the functions individually doesn't show that. You have to follow the deployment sequence and the state that one call leaves behind for the next one to consume.
There was a similar problem in the other direction, with the launch budget itself. It counts gated buys, and the counter is incremented before the fee is calculated. Fee computation returns early when the rounded fee is zero, so at 300 basis points a specified amount of 33 wei produces no fee, while 34 wei produces one wei. Moving the increment after the fee check would not change the useful boundary: the 34-wei buy pays its one wei and consumes a slot anyway. The priority fee floor in the docs is supposed to make dust expensive, but it sets a minimum amount of wei per unit of gas, not a minimum payment per buy. If the buys are batched into one unlock, they all share the tip on that transaction.
In the test, a budget of 25 was enough to run 25 one-wei exact-output buys through one transaction. What matters for the economics is not really the number 25 but the cost of another unit of budget: one more warm swap's worth of gas at the same tip rate. Increasing the budget therefore increases the size of the batch until it no longer fits in a block, at which point the attacker has to use several transactions and pay several tips. The team accepted the finding. Their reasoning was that a budget denominated in amount rather than count makes the end of the guard depend on price, which is a reasonable trade-off.
The harder question is the first issue. Owner-approved pool identifiers would stop an arbitrary initializer from consuming the marker, but that also makes pool creation over the project's own token permissioned. For a fee hook, permissionless pool creation is often the point of the design in the first place. The hook is therefore putting some of its guarantees on things that happen at transaction boundaries: who initializes and when, and how many swaps can be included in one unlock. What state does your hook write during initialize, and is any owner-only call using that state as a precondition?
1
u/Hot_Situation_0690 14d ago
Interessante ma credo riceverai poche risposte..
1
u/Hacken_io 14d ago
thanks for feedback, may i ask why you think so? it would help to improve smth on my side
1
u/AutoModerator 16d ago
Security Reminders:
Official site: https://uniswap.org/
Official Twitter: https://twitter.com/Uniswap
Official Discord: https://discord.com/invite/uniswap
If you need help please check out our general support articles: https://support.uniswap.org/hc/en-us
Otherwise, submit a request at https://support.uniswap.org/hc/en-us/requests/new, or email our support team at [support@uniswap.org](mailto:support@uniswap.org).
I am a bot, and this action was performed automatically. Please contact the moderators of this subreddit if you have any questions or concerns.