# 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 **Published by:** [ChaosSR](https://paragraph.com/@chaossr/) **Published on:** 2025-12-26 **Categories:** solidity, uniswap, hooks, blockchain, bitwise, binary, address **URL:** https://paragraph.com/@chaossr/hooks-addresses-mining ## Content 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 CREATE2There are two opcodes in the EVM that you can use to create a contract: CREATE and CREATE2. CREATE: The Traditional WayCREATE is deterministic and depends on two things:The sender's addressThe 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 chainsNo way to predict or control the specific addressCan'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 CREATEdeployer - The address deploying the contractsalt - A 32-byte value you choose (this is the key!)keccak256(bytecode) - Hash of the contract's creation codeYou 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 chainsAddress is predictable before deploymentYou can compute the address without deployingBy changing the salt, you can generate different addressesNote: 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 MentionCREATE3 is a library pattern that combines CREATE2 and CREATE:Use CREATE2 to deploy a proxy contractUse CREATE from that proxy to deploy the actual contractThis 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 OpportunityFrom 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 BasicsAn Ethereum address is:20 bytes in length160 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 → 1111Bit ShiftingBit 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 OperationsThe 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 = 1011Use 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 EncodedNow 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 Constantuint160 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 FlagsEach 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 ORTo 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 AlgorithmNow 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 CREATE2First, 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 Constantsuint160 constant FLAG_MASK = Hooks.ALL_HOOK_MASK; // 0x3FFF = 0011_1111_1111_1111 uint256 constant MAX_LOOP = 160_444; // Maximum iterations before giving upFLAG_MASK: Isolates the last 14 bits (the permission bits) MAX_LOOP : Safety limit to prevent infinite loops (we'll explain why 160,444 specifically laterThe find() FunctionHere'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 CheckThe 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 == 0This 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 == flagsThis 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 SpaceWith 14 bits to match:Total possibilities: 2¹⁴ = 16,384Probability a random address matches: p = 1/16,384 ≈ 0.0061%Expected Number of AttemptsThis follows a geometric distribution. The expected number of tries before finding a match:E[N] = 1/p = 16,384 attemptsOn average, you'll find a match after trying about 8,192 salts (half of 16,384).Failure ProbabilityThe 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%ResultSuccess probability: ≈ 99.994%MAX_LOOP ≈ 10 × 2¹⁴: Gives near-guaranteed successLarge enough for safety, small enough to be computationally cheapSo 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 PermissionsNow that we understand how addresses are mined, let's see how Uniswap v4 actually uses these addresses to validate permissions at runtime.The Validation FunctionWhen 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 ExampleLet'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 AddressesLet's test our understanding by analyzing real hooks deployed on Uniswap v4!Finding Deployed HooksYou can find deployed hooks on Dune Analytics. Example: Analyzing a Real HookLet's analyze this hook deployed on Base: 0x2f6f792a40a6d216296584d87928a0177546e040 Step 1: Extract the last 4 hex digitsAddress: 0x2f6f792a40a6d216296584d87928a0177546e040 ^^^^ Last 4 hex: e040Step 2: Convert to binary (16 bits)Hex: e040 Binary breakdown: e 0 4 0 1110 0000 0100 0000 Full 16 bits: 1110 0000 0100 0000Step 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 0000Step 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 DoesWithout even reading the contract code, we now know:It executes logic when a new pool is initializedIt executes logic after each swap completesIt does not interact with liquidity operations, donations, or return deltasThis 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:Take the last 4 hex digitsConvert to binaryDrop the top 2 bits (keep only 14 bits)Map each bit to its hook permissionIdentify which hooks are activeConclusionUniswap 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 chainsAddresses that encode metadataGas-efficient permission systemsFactory patterns with predictable addressesYou can apply the same CREATE2 + mining approach!Further ReadingEIP-1014: Skinny CREATE2 - The CREATE2 specificationUniswap v4 Whitepaper - Full protocol designBitwise Operations - Deep dive into bit manipulationDeterministic Deployment Proxy - The CREATE2 deployer used in productionHappy 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 ## Publication Information - [ChaosSR](https://paragraph.com/@chaossr/): Publication homepage - [All Posts](https://paragraph.com/@chaossr/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@chaossr): Subscribe to updates