Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HookMiner #434

Merged
merged 13 commits into from
Feb 12, 2025
54 changes: 54 additions & 0 deletions src/libraries/HookMiner.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;

/// @title HookMiner
/// @notice a minimal library for mining hook addresses
library HookMiner {
// mask to slice out the bottom 14 bit of the address
uint160 constant FLAG_MASK = 0x3FFF; // 0000 ... 0000 0011 1111 1111 1111

// Maximum number of iterations to find a salt, avoid infinite loops or MemoryOOG
uint256 constant MAX_LOOP = 160_444;

/// @notice Find a salt that produces a hook address with the desired `flags`
/// @param deployer The address that will deploy the hook. In `forge test`, this will be the test contract `address(this)` or the pranking address
/// In `forge script`, this should be `0x4e59b44847b379578588920cA78FbF26c0B4956C` (CREATE2 Deployer Proxy)
/// @param flags The desired flags for the hook address. Example `uint160(Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG | ...)`
/// @param creationCode The creation code of a hook contract. Example: `type(Counter).creationCode`
/// @param constructorArgs The encoded constructor arguments of a hook contract. Example: `abi.encode(address(manager))`
/// @return (hookAddress, salt) The hook deploys to `hookAddress` when using `salt` with the syntax: `new Hook{salt: salt}(<constructor arguments>)`
function find(address deployer, uint160 flags, bytes memory creationCode, bytes memory constructorArgs)
internal
view
returns (address, bytes32)
{
flags = flags & FLAG_MASK; // mask for only the bottom 14 bits
bytes memory creationCodeWithArgs = abi.encodePacked(creationCode, constructorArgs);

address hookAddress;
for (uint256 salt; salt < MAX_LOOP; salt++) {
hookAddress = computeAddress(deployer, salt, creationCodeWithArgs);

// if the hook's bottom 14 bits match the desired flags AND the address does not have bytecode, we found a match
if (uint160(hookAddress) & FLAG_MASK == flags && hookAddress.code.length == 0) {
return (hookAddress, bytes32(salt));
}
}
revert("HookMiner: could not find salt");
}

/// @notice Precompute a contract address deployed via CREATE2
/// @param deployer The address that will deploy the hook. In `forge test`, this will be the test contract `address(this)` or the pranking address
/// In `forge script`, this should be `0x4e59b44847b379578588920cA78FbF26c0B4956C` (CREATE2 Deployer Proxy)
/// @param salt The salt used to deploy the hook
/// @param creationCodeWithArgs The creation code of a hook contract, with encoded constructor arguments appended. Example: `abi.encodePacked(type(Counter).creationCode, abi.encode(constructorArg1, constructorArg2))`
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)))))
);
}
}
63 changes: 63 additions & 0 deletions test/libraries/HookMiner.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {Test} from "forge-std/Test.sol";
import {HookMiner} from "../../src/libraries/HookMiner.sol";

contract Blank {
uint256 public num;

constructor(uint256 _num) {
num = _num;
}
}

contract HookMinerTest is Test {
function test_hookMiner(uint16 flags, uint256 number) public {
(address addr, bytes32 salt) =
HookMiner.find(address(this), uint160(flags), type(Blank).creationCode, abi.encode(number));

Blank c = new Blank{salt: salt}(number);

assertEq(address(c), addr);
assertEq(c.num(), number);

// address of the contract has the desired flags
assertEq(uint160(address(c)) & HookMiner.FLAG_MASK, flags & HookMiner.FLAG_MASK);
}

function test_hookMiner_addressCollision(uint16 flags, uint256 number) public {
(address addr, bytes32 salt) =
HookMiner.find(address(this), uint160(flags), type(Blank).creationCode, abi.encode(number));
Blank c = new Blank{salt: salt}(number);
assertEq(address(c), addr);
assertEq(c.num(), number);

// address of the contract has the desired flags
assertEq(uint160(address(c)) & HookMiner.FLAG_MASK, flags & HookMiner.FLAG_MASK);

// count the number of bits in flags
uint256 bitCount;
for (uint256 i = 0; i < 14; i++) {
if ((flags >> i) & 1 == 1) {
bitCount++;
}
}

// only check for collision, if there are less than 8 bits
// (HookMiner struggles to find two valid salts within 160k iterations)
if (bitCount <= 8) {
// despite using the same `.find()` parameters, the library skips any addresses with bytecode
(address newAddress, bytes32 otherSalt) =
HookMiner.find(address(this), uint160(flags), type(Blank).creationCode, abi.encode(number));
assertNotEq(newAddress, addr);
assertNotEq(otherSalt, salt);
Blank d = new Blank{salt: otherSalt}(number);
assertEq(address(d), newAddress);
assertEq(d.num(), number);

// address of the contract has the desired flags
assertEq(uint160(address(d)) & HookMiner.FLAG_MASK, flags & HookMiner.FLAG_MASK);
}
}
}
Loading