Cover photo

Hook Address : How to Generate Addresses with Specific Bit Patterns

Discover how Uniswap v4 encodes permissions in contract addresses instead of storage, saving billions in gas fees. Learn the mining technique behind it


What if I told you that a smart contract's address isn't random?

When you deploy a smart contract, you might think the resulting address that 42 character hexadecimal string starting with "0x" is just a random identifier. But what if that address itself could encode information? What if the last few digits could tell you exactly what the contract is allowed to do, without ever reading from storage?

This is exactly how Uniswap v4 hooks work.

Instead of storing hook permissions in storage, they encode permissions directly into the hook contract's address. A simple bitwise operation (costing ~3 gas) can check the last 14 bits of an address to determine which hook functions are enabled. Across millions of transactions, this saves billions of gas.

But here's the catch: you can't just choose your contract's address. When you deploy a contract, Ethereum's CREATE opcode gives you whatever address the hash function spits out. So how do you get an address that ends with exactly the right bit pattern?

The answer: You mine for it.


hooks permissions

As you may already know, hooks have permissions for executing logic before or after core Uniswap functions. If you're new to hooks, check out this comprehensive introduction to Uniswap v4 hooks to understand the basics.

There are 14 different permissions in a hook, and each permission determines whether your hook can execute before or after a core pool function:

- Before/After Initialize - When a pool is first created

- Before/After Add Liquidity - When liquidity providers deposit tokens

- Before/After Remove Liquidity - When liquidity is withdrawn

- Before/After Swap - When a token swap occurs

- Before/After Donate - When tokens are donated to the pool

- Return Delta Flags - Special flags for advanced liquidity management

The last 4 hexadecimal characters (16 bits) of a hook's address encode its permissions. Whenever you change permissions (add or remove hooks), it affects the address you need to deploy to.

For example:

- Address ending in 0x00C0 → Binary: 0000 0000 1100 0000 → Has bits 6 & 7 set → AFTER_SWAP and BEFORE_SWAP enabled

- Address ending in 0x2040 → Binary: 0010 0000 0100 0000 → Has bits 6 & 13 set → AFTER_SWAP and BEFORE_INITIALIZE enabled

The challenge: How do you deploy a contract to an address with a specific bit pattern?


Address Creation: CREATE vs CREATE2

There are two opcodes in the EVM that you can use to create a contract: CREATE and CREATE2.

CREATE: The Traditional Way

CREATE is deterministic and depends on two things:

  1. The sender's address

  2. The sender's nonce (transaction count)

Formula:

address = keccak256(RLP(sender_address, sender_nonce))[12:]

You can use it directly by sending a creation transaction to the null address, or by calling it inside a contract:

// Direct deployment
address contractAddress = new Contract();

// Or in assembly
bytes memory bytecode = type(Contract).creationCode;
address addr;
assembly {
    addr := create(0, add(bytecode, 32), mload(bytecode))
}

Limitations:

  • Address changes with each deployment (nonce increments)

  • Different addresses on different chains

  • No way to predict or control the specific address

  • Can't "mine" for specific bit patterns

CREATE2: Deterministic Deployment (EIP-1014)

The limitations of CREATE led developers to want another way for generating addresses. EIP-1014 introduced CREATE2, which added a crucial new parameter: the salt.

Formula:

address = keccak256(0xFF ++ deployer ++ salt ++ keccak256(bytecode))[12:]

Where:

  • 0xFF - A constant prefix to distinguish from CREATE

  • deployer - The address deploying the contract

  • salt - A 32-byte value you choose (this is the key!)

  • keccak256(bytecode) - Hash of the contract's creation code

You can deploy using CREATE2 in Solidity:

bytes32 salt = bytes32(uint256(1));
Contract newContract = new Contract{salt: salt}(constructorArgs);

Or in assembly:

bytes memory bytecode = typr(Contract).creationCode;
address addr;
assembly {
    addr := create2(0, add(bytecode, 32), mload(bytecode), salt)
}

Key advantages:

  • Same deployer + same bytecode + same salt = same address on all EVM chains

  • Address is predictable before deployment

  • You can compute the address without deploying

  • By changing the salt, you can generate different addresses

Note: Deploying to the same address across multiple chains requires the deployer (factory) address to be identical on all chains. This is why production deployments often use the CREATE2 Deployer Proxy , which exists at the same address on all major EVM chains.

CREATE3: A Quick Mention

CREATE3 is a library pattern that combines CREATE2 and CREATE:

  1. Use CREATE2 to deploy a proxy contract

  2. Use CREATE from that proxy to deploy the actual contract

This allows for even more flexibility, but we don't need it for HookMiner. It's worth knowing about for other use cases, but for mining hook addresses, CREATE2 is perfect.

The Mining Opportunity

From the above comparison, there's only one thing we can easily modify to generate the address we need: the salt. (You could also modify the bytecode slightly, but that changes your contract logic—not ideal!)

So we must play with the salt to find our desired address. But how do we know which salt to use? We'll cover this in the mining algorithm section, but first, you need to understand binary operations.


Binary, Shifting, and Bitwise Operations

To understand how HookMiner works, you need to grasp a few fundamental concepts about how computers represent numbers and how we can manipulate individual bits.

Binary Basics

An Ethereum address is:

  • 20 bytes in length

  • 160 bits (20 × 8)

  • 40 hexadecimal characters (each hex digit = 4 bits)

Each bit is either 1 or 0:

Decimal  Binary (4 bits shown)
1   0001
2   0010
4   0100
8   1000
15  1111

Bit Shifting

Bit shifting moves bits left or right, which is equivalent to multiplying or dividing by powers of 2.

Right Shift (>>) - Dividing by powers of 2:

8 >> 1 = 8 / 2¹ = 4

In binary:
1000 >> 1 = 0100
(shifted right, equivalent to adding a zero at the start)

Left Shift (<<) - Multiplying by powers of 2:

2 << 1 = 2 × 2¹ = 4

In binary:
0010 << 1 = 0100
(shifted left, equivalent to adding a zero at the end)

Bitwise Operations

The EVM has four bitwise operators, but we only need two for HookMiner:

AND (&) - Returns 1 only if both bits are 1:

Truth table:
1 & 1 = 1
1 & 0 = 0
0 & 1 = 0
0 & 0 = 0

Example:
  0011 & 1010  =  0010

OR (|) - Returns 0 only if both bits are 0:

Truth table:
0 | 0 = 0
1 | 0 = 1
0 | 1 = 1
1 | 1 = 1

Example:
  0011 | 1010  =  1011

Use case: Combining multiple flags. If you want both BEFORE_SWAP and AFTER_SWAP, you OR their values together.

Additional Resources: For a deeper dive into bitwise operations, check out this guide on bitwise operators or EVM.codes documentation.

I hope this is clear! If not, make sure to fully understand these concepts before continuing, as they're fundamental to how the mining algorithm works.


Hook Flags: How Permissions Are Encoded

Now that you undertand binary and bitwise operations, let's see how Uniswap v4 defines hook permissions in code. This comes directly from the core library Hooks.sol :

The ALL_HOOK_MASK Constant

uint160 internal constant ALL_HOOK_MASK = uint160((1 << 14) - 1);

Let's break down how this creates the mask:

Step 1: 1 << 14 (shift 1 left by 14 positions)
Binary: 0100_0000_0000_0000
Decimal: 16,384

Step 2: Subtract 1 (flips all bits below)
Binary: 0011_1111_1111_1111
Decimal: 16,383
Hex: 0x3FFF

This mask has exactly 14 bits set (bits 0-13), which correspond to the 14 hook permissions. Bits 14 and 15 are left as 0 because we don't use them.

The 14 Hook Permission Flags

Each permission is created by shifting 1 to a specific position:

// Bit 13 - BEFORE_INITIALIZE_FLAG
uint160 internal constant BEFORE_INITIALIZE_FLAG = 1 << 13;
// Binary: 0010_0000_0000_0000 

// Bit 12 - AFTER_INITIALIZE_FLAG
uint160 internal constant AFTER_INITIALIZE_FLAG = 1 << 12;
// Binary: 0001_0000_0000_0000 

// Bit 11 - BEFORE_ADD_LIQUIDITY_FLAG
uint160 internal constant BEFORE_ADD_LIQUIDITY_FLAG = 1 << 11;
// Binary: 0000_1000_0000_0000 

// -------REST -----------

// Bit 7 - BEFORE_SWAP_FLAG
uint160 internal constant BEFORE_SWAP_FLAG = 1 << 7;
// Binary: 0000_0000_1000_0000 

// Bit 6 - AFTER_SWAP_FLAG
uint160 internal constant AFTER_SWAP_FLAG = 1 << 6;
// Binary: 0000_0000_0100_0000 

// --------- REST ----------

Combining Flags with OR

To enable multiple permissions, you combine flags using the OR operator:

uint160 flags = BEFORE_SWAP_FLAG | AFTER_SWAP_FLAG;

// In binary:
//   0000_0000_1000_0000  (BEFORE_SWAP_FLAG)
// | 0000_0000_0100_0000  (AFTER_SWAP_FLAG)
// = 0000_0000_1100_0000  (both flags set)
// = 0x00C0 in hex

This combined value (`0x00C0`) is what we need to see in the last 14 bits of our hook contract's address!


The HookMiner Algorithm

Now we have all the pieces. Let's see how HookMiner actually finds the right salt to generate an address with the correct permissions.

Computing Addresses with CREATE2

First, we need a function to compute what address a given salt will produce:

    function computeAddress(address deployer, uint256 salt, bytes memory creationCodeWithArgs)
        internal
        pure
        returns (address hookAddress)
    {
        return address(
            uint160(uint256(keccak256(abi.encodePacked(bytes1(0xFF), deployer, salt, keccak256(creationCodeWithArgs)))))
        );
    }

This implements the CREATE2 formula we discussed earlier. The key insight: the deployer and creationCodeWithArgs will always be the same, but we can vary the salt to get different addresses.

You could also slightly modify the contract bytecode if you want, but that's more complex and changes your contract's logic.

The Mining Constants

uint160 constant FLAG_MASK = Hooks.ALL_HOOK_MASK; // 0x3FFF = 0011_1111_1111_1111
uint256 constant MAX_LOOP = 160_444; // Maximum iterations before giving up

FLAG_MASK: Isolates the last 14 bits (the permission bits)
MAX_LOOP : Safety limit to prevent infinite loops (we'll explain why 160,444 specifically later

The find() Function

Here's the complete mining algorithm:

function find(
    address deployer,
    uint160 flags,
    bytes memory creationCode,
    bytes memory constructorArgs
) internal view returns (address, bytes32) {
    // Step 1: Ensure flags only uses the bottom 14 bits
    flags = flags & FLAG_MASK;
    
    // Step 2: Combine creation code with constructor arguments
    bytes memory creationCodeWithArgs = abi.encodePacked(creationCode, constructorArgs);
    
    // Step 3: Try different salts until we find a match
    address hookAddress;
    for (uint256 salt; salt < MAX_LOOP; salt++) {
        // Compute the address this salt would produce
        hookAddress = computeAddress(deployer, salt, creationCodeWithArgs);
        
        // Check if it matches our desired flags AND the address is empty
        if (uint160(hookAddress) & FLAG_MASK == flags && hookAddress.code.length == 0) {
            return (hookAddress, bytes32(salt));
        }
    }
    
    // If we get here, we failed to find a match (extremely rare!)
    revert("HookMiner: could not find salt");
}

Understanding the Validation Check

The core of the algorithm is this line:

if (uint160(hookAddress) & FLAG_MASK == flags && hookAddress.code.length == 0)

Let's break it down into two conditions:

Condition 1: hookAddress.code.length == 0

This check ensures that no contract is already deployed at this address. In Ethereum, you cannot deploy a contract to an address that already has code. This check prevents us from finding an address that matches our flags but is already occupied.

Condition 2: uint160(hookAddress) & FLAG_MASK == flags

This is the permission matching check. It extracts the last 14 bits of the address and compares them to our desired flags.

Example :

// Our desired flags
flags = BEFORE_SWAP_FLAG | AFTER_SWAP_FLAG;
// = 0x0000_0000_1100_0000

// Suppose the address ends with these last 4 hex digits
address hookAddress = 0x...00C0;

Breaking down the check:

Step 1: Convert address to uint160 and AND with FLAG_MASK
  0x...00C0 (last 16 bits: 0000_0000_1100_0000)
& 0x3FFF   (FLAG_MASK:    0011_1111_1111_1111)
= 0x00C0   (result:        0000_0000_1100_0000)

Step 2: Compare with desired flags
0x00C0 == 0x00C0 ✅ MATCH!

Result: This address has the exact permissions we need!
Why MAX_LOOP = 160,444?

You might wonder: why this specific number? Is it arbitrary? No—it's based on probability theory.

The Search Space

With 14 bits to match:

  • Total possibilities: 2¹⁴ = 16,384

  • Probability a random address matches: p = 1/16,384 ≈ 0.0061%

Expected Number of Attempts

This follows a geometric distribution. The expected number of tries before finding a match:

E[N] = 1/p = 16,384 attempts

On average, you'll find a match after trying about 8,192 salts (half of 16,384).

Failure Probability

The probability of NOT finding a match after N tries:

P(fail after N tries) = (1 - p)^N ≤ e^(-Np)

Plugging in N = 160,444:

e^(-160,444 / 16,384) ≈ e^(-9.79) ≈ 5.6 × 10^(-5) ≈ 0.0056%

Result

  • Success probability:99.994%

  • MAX_LOOP ≈ 10 × 2¹⁴: Gives near-guaranteed success

  • Large enough for safety, small enough to be computationally cheap

So 160,444 is not an arbitrary number—it's carefully chosen to provide a very high success rate (99.994%) while keeping the computation reasonable. It's about 10× the expected number of attempts, which gives us a huge safety margin.

In practice: Most mining operations succeed in 5,000-20,000 iterations, well below the MAX_LOOP limit.


How Uniswap V4 Validates Hook Permissions

Now that we understand how addresses are mined, let's see how Uniswap v4 actually uses these addresses to validate permissions at runtime.

The Validation Function

When a hook is called, Uniswap v4 uses code similar to this:

function validateHookPermissions(
    address hook,
    uint160 requiredFlag
) internal pure returns (bool) {
    // Extract the hook's permissions from its address
    uint160 hookPermissions = uint160(hook) & ALL_HOOK_MASK;
    
    // Check if the required flag is set
    return (hookPermissions & requiredFlag) == requiredFlag;
}

The key insight: This entire validation happens with simple bitwise operations, no storage reads, no external calls. Just pure computation on the address itself. This is why it's so gas-efficient!


Complete Usage Example

Let's put it all together and see how you'd actually use HookMiner in a real deployment.

Step 1: Your Hook Contract

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;

import {BaseHook} from "@uniswap/v4-core/src/BaseHook.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";

contract MyHook is BaseHook {
    constructor(IPoolManager _poolManager) BaseHook(_poolManager) {}
    
    function getHookPermissions() public pure override returns (Hooks.Permissions memory) {
        return Hooks.Permissions({
            beforeInitialize: false,
            afterInitialize: false,
            beforeAddLiquidity: false,
            afterAddLiquidity: false,
            beforeRemoveLiquidity: false,
            afterRemoveLiquidity: false,
            beforeSwap: true,        // ✅ Enabled
            afterSwap: true,         // ✅ Enabled
            beforeDonate: false,
            afterDonate: false,
            beforeSwapReturnDelta: false,
            afterSwapReturnDelta: false,
            afterAddLiquidityReturnDelta: false,
            afterRemoveLiquidityReturnDelta: false
        });
    }
    
    function beforeSwap(
        address,
        PoolKey calldata,
        IPoolManager.SwapParams calldata,
        bytes calldata
    ) external override returns (bytes4, BeforeSwapDelta, uint24) {
        // Your custom logic before swaps
        return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0);
    }
    
    function afterSwap(
        address,
        PoolKey calldata,
        IPoolManager.SwapParams calldata,
        BalanceDelta,
        bytes calldata
    ) external override returns (bytes4, int128) {
        // Your custom logic after swaps
        return (BaseHook.afterSwap.selector, 0);
    }
}

Step 2: Deployment Script

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;

import "forge-std/Script.sol";
import {HookMiner} from "./HookMiner.sol";
import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";
import {MyHook} from "../src/MyHook.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";

contract DeployHook is Script {
    function run() external {
        // Use the deterministic CREATE2 deployer (same address on all chains)
        address CREATE2_DEPLOYER = 0x4e59b44847b379578588920cA78FbF26c0B4956C;
        
        // Your pool manager address (replace with actual address)
        IPoolManager manager = IPoolManager(vm.envAddress("POOL_MANAGER_ADDRESS"));
        
        // Define desired hook flags (must match your hook's getHookPermissions)
        uint160 flags = uint160( Hooks.BEFORE_SWAP_FLAG |  Hooks.AFTER_SWAP_FLAG ); // Combined: 0x00C0
        
        console.log("Mining for address with flags:", flags);
        
        // Mine the salt to find an address with the correct flags
        (address predictedAddress, bytes32 salt) = HookMiner.find(
            CREATE2_DEPLOYER,
            flags,
            type(MyHook).creationCode,
            abi.encode(manager)
        );
        
        console.log("Found valid address:", predictedAddress);
        console.log("Last 4 hex digits:", uint16(uint160(predictedAddress)));
        
        // Start broadcasting transactions
        vm.startBroadcast();
        
        // Deploy the hook using CREATE2 with the mined salt
        MyHook hook = new MyHook{salt: salt}(manager);
        
        vm.stopBroadcast();
        
        // Verify the address matches our prediction
        require(address(hook) == predictedAddress, "Address mismatch!");
        
        console.log("Hook successfully deployed to:", address(hook));
    }
}


Real-World Analysis: Decoding Hook Addresses

Let's test our understanding by analyzing real hooks deployed on Uniswap v4!

Finding Deployed Hooks

You can find deployed hooks on Dune Analytics.

Example: Analyzing a Real Hook

Let's analyze this hook deployed on Base: 0x2f6f792a40a6d216296584d87928a0177546e040

Step 1: Extract the last 4 hex digits

Address: 0x2f6f792a40a6d216296584d87928a0177546e040
                                                                                          ^^^^
Last 4 hex: e040

Step 2: Convert to binary (16 bits)

Hex: e040
Binary breakdown:
e    0    4    0
1110 0000 0100 0000
Full 16 bits:
1110 0000 0100 0000

Step 3: Apply FLAG_MASK (drop 2 highest bits)

We only care about the last 14 bits (bits 0-13):

Original:  1110 0000 0100 0000
           ^^                    ← These top 2 bits (14-15) are masked out
           
After mask: 10 0000 0100 0000
            ^^ ^^^^ ^^^^ ^^^^
            |  |_____________|
            |         |
            |         +-- 14 flag bits we care about
            +------------ This becomes bit 13

Final 14 bits: 10 0000 0100 0000

Step 4: Map to hook permissions

Now let's see which bits are set (reading right to left, bit 0 to bit 13):

Bit Position: 13 12 11 10  9  8  7  6  5  4  3  2  1  0
Binary:        1  0  0  0  0  0  0  1  0  0  0  0  0  0
Active:                     

Step 5: Identify enabled hooks

13

BEFORE_INITIALIZE

Yes

7

AFTER_SWAP

Yes

Result: This hook has two permissions enabled:

  • BEFORE_INITIALIZE (bit 13)

  • AFTER_SWAP (bit 6)

What This Hook Does

Without even reading the contract code, we now know:

  1. It executes logic when a new pool is initialized

  2. It executes logic after each swap completes

  3. It does not interact with liquidity operations, donations, or return deltas

This is the power of address-based permissions! Anyone can look at the address and immediately understand what the hook is capable of, without reading storage or calling any functions. It's transparent, immutable, and gas-efficient.

Try It Yourself!

Pick any hook address from the Dune dashboard and decode it:

  1. Take the last 4 hex digits

  2. Convert to binary

  3. Drop the top 2 bits (keep only 14 bits)

  4. Map each bit to its hook permission

  5. Identify which hooks are active


Conclusion

Uniswap v4 turned a constraint (contract addresses are immutable) into a feature (encode permissions in the address itself). By storing permissions in the address rather than in storage, they achieved a 700x gas improvement per permission check.

This technique isn't limited to Uniswap v4. Anytime you need:

  • Deterministic addresses across chains

  • Addresses that encode metadata

  • Gas-efficient permission systems

  • Factory patterns with predictable addresses

You can apply the same CREATE2 + mining approach!

Further Reading


Happy mining!

If you found this guide helpful, consider sharing it with other developers exploring Uniswap v4. And if you build something cool with HookMiner, I'd love to see it!

feel free to reach me here : https://x.com/0xlinguin