Vanguard

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

Hook permission flag mismatch in deployment script

Author Revealed upon completion

Description

  • A Uniswap v4 hook’s address flags (encoded in the hook contract address) are supposed to match the callbacks the hook actually enables via getHookPermissions(). For TokenLaunchHook, the contract enables afterInitialize and beforeSwap (and not beforeInitialize).

  • The deployment script mines the hook address using BEFORE_INITIALIZE_FLAG instead of AFTER_INITIALIZE_FLAG. This yields an address that advertises beforeInitialize, while the hook’s getHookPermissions() returns beforeInitialize=false and afterInitialize=true. This mismatch can lead to inconsistent expectations and integration problems during pool setup/operations.

// DeployHookScript (mines flags for BEFORE_SWAP + BEFORE_INITIALIZE)
uint160 flags = uint160(Hooks.BEFORE_SWAP_FLAG
@> + Hooks.BEFORE_INITIALIZE_FLAG);
// ... mines & deploys using the wrong flags
(address hookAddress, bytes32 salt) =
HookMiner.find(CREATE2_FACTORY, flags, type(TokenLaunchHook).creationCode, constructorArgs);
// ...
require(address(hook) == hookAddress, "DeployHookScript: Hook Address Mismatch");
// TokenLaunchHook exposes permissions that DO NOT include beforeInitialize
function getHookPermissions() public pure override returns (Hooks.Permissions memory) {
return Hooks.Permissions({
beforeInitialize: false, // @> false
afterInitialize: true, // @> true
// ...
beforeSwap: true, // @> true
afterSwap: false
// ...
});
}

Risk

Likelihood: High

  • Script is used as-is for deployments: Any use of the provided script will mine and deploy a hook address with the wrong flag baked into the address.

  • Permissions are statically returned by the hook: TokenLaunchHook returns beforeInitialize=false, guaranteeing a mismatch against the script’s BEFORE_INITIALIZE_FLAG.

Impact: High

  • Integration confusion and brittle assumptions: Downstream tooling and reviewers expect address flags to reflect enabled callbacks; a mismatch can cause incorrect operational assumptions and brittle integrations.

  • Potential operational failures: Systems that derive behavior solely from address flags (or assert flag ↔ permission parity) may alert, revert, or mis-handle hook lifecycle calls during pool initialization workflows.

Proof of Concept

  • Create DeployScriptFlagMismatchTest.t.sol under test directory and copy code below.

  • Run command forge test --mt test_DeployScript_UsesBeforeInitializeFlagButHookDoesNot -vvvv.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {Test} from "forge-std/Test.sol";
import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol";
import {Hooks} from "v4-core/libraries/Hooks.sol";
import {HookMiner} from "v4-periphery/src/utils/HookMiner.sol";
import {TokenLaunchHook} from "../src/TokenLaunchHook.sol";
import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol";
error HookAddressNotValid(address);
//The test intentionally mines an address with BEFORE_INITIALIZE_FLAG
// | BEFORE_SWAP_FLAG (exactly as in the script) and then attempts to deploy
// TokenLaunchHook.
contract DeployScriptFlagMismatchTest is Test, Deployers {
uint256 phase1Duration = 100;
uint256 phase2Duration = 200;
uint256 phase1LimitBps = 100;
uint256 phase2LimitBps = 300;
uint256 phase1Cooldown = 5;
uint256 phase2Cooldown = 3;
uint256 phase1PenaltyBps = 500;
uint256 phase2PenaltyBps = 200;
function setUp() public {
deployFreshManagerAndRouters();
}
function test_DeployScript_UsesBeforeInitializeFlagButHookDoesNot() public {
// Arrange: creation code + constructor args for TokenLaunchHook
bytes memory creationCode = type(TokenLaunchHook).creationCode;
bytes memory constructorArgs = abi.encode(
manager,
phase1Duration,
phase2Duration,
phase1LimitBps,
phase2LimitBps,
phase1Cooldown,
phase2Cooldown,
phase1PenaltyBps,
phase2PenaltyBps
);
// WRONG flags (as per DeployHookScript): BEFORE_SWAP | BEFORE_INITIALIZE
uint160 wrongFlags = uint160(Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_INITIALIZE_FLAG);
// Mine a salt producing an address with the WRONG flags
(address minedAddress, bytes32 salt) =
HookMiner.find(address(this), wrongFlags, creationCode, constructorArgs);
// Act: deploy the hook at the mined address
// Expect revert due to hook address not having the correct flags
vm.expectRevert(abi.encodeWithSelector(HookAddressNotValid.selector, minedAddress));
TokenLaunchHook hook = new TokenLaunchHook{salt: salt}(
IPoolManager(manager),
phase1Duration,
phase2Duration,
phase1LimitBps,
phase2LimitBps,
phase1Cooldown,
phase2Cooldown,
phase1PenaltyBps,
phase2PenaltyBps
);
}
}

Output:

[⠊] Compiling...
[⠢] Compiling 2 files with Solc 0.8.26
[⠆] Solc 0.8.26 finished in 60.38ms
No files changed, compilation skipped
Ran 1 test for test/DeployScriptFlagMismatch.t.sol:DeployScriptFlagMismatchTest
[PASS] test_DeployScript_UsesBeforeInitializeFlagButHookDoesNot() (gas: 4573410)
Traces:
[4573410] DeployScriptFlagMismatchTest::test_DeployScript_UsesBeforeInitializeFlagButHookDoesNot()
├─ [0] VM::expectRevert(custom error 0xf28dceb3: 00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000024e65af6a000000000000000000000000014af0b9ac23e19d1c9c57d5c32e82d138b92a08000000000000000000000000000000000000000000000000000000000)
│ └─ ← [Return]
├─ [1419] → new <unknown>@0x14Af0b9aC23e19D1c9C57d5c32E82D138b92A080
│ └─ ← [Revert] 0xe65af6a000000000000000000000000014af0b9ac23e19d1c9c57d5c32e82d138b92a080
└─ ← [Return]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 63.77ms (61.35ms CPU time)
Ran 1 test suite in 65.85ms (63.77ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Recommended Mitigation

  • Update the deployment script to mine with AFTER_INITIALIZE_FLAG (not BEFORE_INITIALIZE_FLAG) and make the bitwise OR explicit:

- // wrong: mines BEFORE_SWAP + BEFORE_INITIALIZE
- uint160 flags = uint160(Hooks.BEFORE_SWAP_FLAG
- + Hooks.BEFORE_INITIALIZE_FLAG);
+ // correct: mines BEFORE_SWAP + AFTER_INITIALIZE (matches getHookPermissions)
+ uint160 flags = uint160(Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_INITIALIZE_FLAG);

Support

FAQs

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

Give us feedback!