Vanguard

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

Deployment script uses wrong hook permission flag causing deployment failure

Author Revealed upon completion

Deployment script uses wrong hook permission flag causing deployment failure

Description

The deployment script mines for a hook address with incorrect permission flags. It uses BEFORE_INITIALIZE_FLAG but the hook contract declares afterInitialize: true:

Deployment script (deployLaunchHook.s.sol):

// @audit-info: The second flag is not correct - it should be AFTER_INITIALIZE_FLAG
uint160 flags = uint160(Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_INITIALIZE_FLAG);

Hook contract (TokenLaunchHook.sol):

function getHookPermissions() public pure override returns (Hooks.Permissions memory) {
return Hooks.Permissions({
@> beforeInitialize: false,
@> afterInitialize: true,
// ...
beforeSwap: true,
// ...
});
}

In Uniswap V4, hook permissions are encoded in the hook's address. The HookMiner.find() function searches for a salt that produces an address with specific permission bits set. The TokenLaunchHook contract inherits the Uniswap V4 BaseHook contract, which then inherits the Uniswap V4 Hooks library. In the BaseHook contract, there is a validateHookAddress() call in the constructor. This in turn calls Hooks.validateHookPermissions(_this,getHookPermissions()), which is used to ensure the deployed hooks address causes the intended hooks to be called, and will revert not.

This means that with the incorrect flag configuration in DeployLaunchHook.s.sol, deployments will always revert.

From BaseHook.sol:

constructor(IPoolManager _manager) ImmutableState(_manager) {
validateHookAddress(this);
}
/// @notice Validates the deployed hook address agrees with the expected permissions of the hook
/// @dev this function is virtual so that we can override it during testing,
/// which allows us to deploy an implementation to any address
/// and then etch the bytecode into the correct address
function validateHookAddress(BaseHook _this) internal pure virtual {
Hooks.validateHookPermissions(_this, getHookPermissions());
}

From Hooks.sol:

/// @notice Utility function intended to be used in hook constructors to ensure
/// the deployed hooks address causes the intended hooks to be called
/// @param permissions The hooks that are intended to be called
/// @dev permissions param is memory as the function will be called from constructors
function validateHookPermissions(IHooks self, Permissions memory permissions) internal pure {
if (
permissions.beforeInitialize != self.hasPermission(BEFORE_INITIALIZE_FLAG)
|| permissions.afterInitialize != self.hasPermission(AFTER_INITIALIZE_FLAG)
|| permissions.beforeAddLiquidity != self.hasPermission(BEFORE_ADD_LIQUIDITY_FLAG)
|| permissions.afterAddLiquidity != self.hasPermission(AFTER_ADD_LIQUIDITY_FLAG)
|| permissions.beforeRemoveLiquidity != self.hasPermission(BEFORE_REMOVE_LIQUIDITY_FLAG)
|| permissions.afterRemoveLiquidity != self.hasPermission(AFTER_REMOVE_LIQUIDITY_FLAG)
|| permissions.beforeSwap != self.hasPermission(BEFORE_SWAP_FLAG)
|| permissions.afterSwap != self.hasPermission(AFTER_SWAP_FLAG)
|| permissions.beforeDonate != self.hasPermission(BEFORE_DONATE_FLAG)
|| permissions.afterDonate != self.hasPermission(AFTER_DONATE_FLAG)
|| permissions.beforeSwapReturnDelta != self.hasPermission(BEFORE_SWAP_RETURNS_DELTA_FLAG)
|| permissions.afterSwapReturnDelta != self.hasPermission(AFTER_SWAP_RETURNS_DELTA_FLAG)
|| permissions.afterAddLiquidityReturnDelta != self.hasPermission(AFTER_ADD_LIQUIDITY_RETURNS_DELTA_FLAG)
|| permissions.afterRemoveLiquidityReturnDelta
!= self.hasPermission(AFTER_REMOVE_LIQUIDITY_RETURNS_DELTA_FLAG)
) {
HookAddressNotValid.selector.revertWith(address(self));
}
}

Risk

Likelihood:

  • This will occur on every deployment using the provided script

Impact:

  • The script is completely ineffective in deploying the TokenLaunchHook and will always revert

Proof of Concept

Add the following test to TokenLaunchHookUnit.t.sol

function test_IncorrectHookFlags() public {
address CREATE2_FACTORY = 0x4e59b44847b379578588920cA78FbF26c0B4956C;
// Create a hook with incorrect flags
bytes memory creationCode = type(TokenLaunchHook).creationCode;
bytes memory constructorArgs = abi.encode(
manager,
phase1Duration,
phase2Duration,
phase1LimitBps,
phase2LimitBps,
phase1Cooldown,
phase2Cooldown,
phase1PenaltyBps,
phase2PenaltyBps
);
// These flags are incorrect - the hook should have AFTER_INITIALIZE_FLAG and BEFORE_SWAP_FLAG
uint160 flags = uint160(Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG);
(, bytes32 salt) = HookMiner.find(CREATE2_FACTORY, flags, creationCode, constructorArgs);
// Try to deploy the hook, reverts with HookAddressNotValid
vm.expectRevert();
TokenLaunchHook antiBotHookTwo = new TokenLaunchHook{salt: salt}(
manager,
phase1Duration,
phase2Duration,
phase1LimitBps,
phase2LimitBps,
phase1Cooldown,
phase2Cooldown,
phase1PenaltyBps,
phase2PenaltyBps
);
}

Recommended Mitigation

Fix the deployment script to use the correct flag:

function run() public {
// hook contracts must have specific flags encoded in the address
- uint160 flags = uint160(Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_INITIALIZE_FLAG);
+ uint160 flags = uint160(Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_INITIALIZE_FLAG);
// Mine a salt that will produce a hook address with the correct flags
// ...
}

Support

FAQs

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

Give us feedback!