Vanguard

First Flight #56
Beginner FriendlyDeFiFoundry
0 EXP
Submission Details
Impact: high
Likelihood: low

Hook Flags Mismatch in Deploy Script + Launch Initialization Breaks

Author Revealed upon completion

Root + Impact

Description

  • Normal behavior: In Uniswap v4, hook permissions are encoded directly into the hook address via specific bit flags. These permissions determine which lifecycle callbacks (e.g., afterInitialize, beforeSwap) the PoolManager is allowed to invoke. A hook must encode exactly the same permissions that it declares in getHookPermissions(). To prevent misconfiguration, Uniswap v4 provides abstract base hook implementations (e.g., BaseHook) that validate this alignment at deployment time.

  • Specific issue: TokenLaunchHook declares afterInitialize as enabled in getHookPermissions() and implements critical launch initialization logic inside afterInitialize. However, the deployment script mines the hook address without encoding AFTER_INITIALIZE_FLAG and instead encodes BEFORE_INITIALIZE_FLAG, which the hook explicitly does not declare or implement.

This creates a structural mismatch between:

  • the permissions encoded in the hook address, and

  • the permissions required by the hook’s implemented functions.

As a result, the hook violates the Uniswap v4 permission invariant and is deployed in an invalid configuration.

// TokenLaunchHook.sol
function getHookPermissions() public pure override returns (Hooks.Permissions memory) {
return Hooks.Permissions({
beforeInitialize: false, // @> Hook does NOT declare beforeInitialize
afterInitialize: true, // @> Hook REQUIRES afterInitialize
beforeSwap: true,
afterSwap: false,
beforeAddLiquidity: false,
afterAddLiquidity: false,
beforeRemoveLiquidity: false,
afterRemoveLiquidity: false,
beforeDonate: false,
afterDonate: false
});
}
function afterInitialize(
address,
PoolKey calldata key,
uint160,
int24
) external override onlyPoolManager returns (bytes4) {
launchStartTime = block.timestamp; // @> Critical launch state
uint128 liquidity = StateLibrary.getLiquidity(poolManager, key.toId());
initialLiquidity = uint256(liquidity); // @> Used for phase limits
return TokenLaunchHook.afterInitialize.selector;
}
// deployLaunchHook.s.sol
uint160 flags = uint160(
Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_INITIALIZE_FLAG // @> Permission mismatch
// @> Missing Hooks.AFTER_INITIALIZE_FLAG
);

Additionally, the hook does not inherit from BaseHook and does not perform any runtime permission validation (e.g., validateHookPermissions), allowing this invalid configuration to be deployed unchecked.

Risk

Likelihood:

  • Occurs deterministically on every deployment using the current deployment script, as the hook address is always mined with incorrect permissions.

  • Manifests immediately at pool initialization time, since hook execution eligibility is derived from the address-encoded flags.

Impact:

  • In strict Uniswap v4 implementations, pool initialization reverts by design, preventing the pool from being created at all.

  • In permissive implementations, the afterInitialize callback is silently skipped, leaving:

  • launchStartTime == 0

  • initialLiquidity == 0

This results in a permanently uninitialized launch state, causing the phased launch logic and swap restrictions to malfunction or be bypassed entirely.

Proof of Concept

This issue is a structural violation of the Uniswap v4 hook permission model and can be demonstrated without executing any swaps or writing test code.

  1. TokenLaunchHook implements the afterInitialize function and declares afterInitialize: true in getHookPermissions().

  2. The deployment script mines the hook address without encoding AFTER_INITIALIZE_FLAG and instead encodes BEFORE_INITIALIZE_FLAG.

  3. The hook does not inherit from BaseHook and does not validate permission alignment during deployment.

  4. Pool initialization reverts in strict implementations, or afterInitialize is never executed, leaving launchStartTime and initialLiquidity equal to zero.

Recommended Mitigation

Ensure that the permissions encoded in the hook address exactly match the permissions declared by the hook.

  • Remove BEFORE_INITIALIZE_FLAG

  • Add AFTER_INITIALIZE_FLAG

// deployLaunchHook.s.sol
- uint160 flags = uint160(
- Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_INITIALIZE_FLAG
- );
+ uint160 flags = uint160(
+ Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_INITIALIZE_FLAG
+ );

Additional hardening (strongly recommended)

  • Inherit from BaseHook (or OpenZeppelin’s equivalent) to enforce permission validation at deployment time.

  • Alternatively, explicitly call validateHookPermissions() in the constructor to ensure the deployed hook address encodes the intended permissions.

  • Add deployment or test-time assertions verifying: launchStartTime != 0 after pool initialization.

require(hook.launchStartTime() != 0, "afterInitialize did not run");
//This prevents silent misconfiguration if flags/perms drift again in future changes.

Reference:

https://www.cyfrin.io/blog/uniswap-v4-hooks-security-deep-dive#hooks-and-permissions

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!