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.
uint160 flags = uint160(Hooks.BEFORE_SWAP_FLAG
@> + Hooks.BEFORE_INITIALIZE_FLAG);
(address hookAddress, bytes32 salt) =
HookMiner.find(CREATE2_FACTORY, flags, type(TokenLaunchHook).creationCode, constructorArgs);
require(address(hook) == hookAddress, "DeployHookScript: Hook Address Mismatch");
function getHookPermissions() public pure override returns (Hooks.Permissions memory) {
return Hooks.Permissions({
beforeInitialize: false,
afterInitialize: true,
beforeSwap: 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
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);
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 {
bytes memory creationCode = type(TokenLaunchHook).creationCode;
bytes memory constructorArgs = abi.encode(
manager,
phase1Duration,
phase2Duration,
phase1LimitBps,
phase2LimitBps,
phase1Cooldown,
phase2Cooldown,
phase1PenaltyBps,
phase2PenaltyBps
);
uint160 wrongFlags = uint160(Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_INITIALIZE_FLAG);
(address minedAddress, bytes32 salt) =
HookMiner.find(address(this), wrongFlags, creationCode, constructorArgs);
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
- // 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);