SocketDocs

Hooks

Write a hook

Build a Uniswap v4 hook on BaseHook, mine its address, test it against PoolManager, and propose it for Socket's allowlist.

A hook is a Solidity contract that PoolManager calls at the points its address allows. Any pool can use it once it is deployed. For Socket's app to route through it, SOCKET holders vote it onto the allowlist.

Set up#

BaseHook and HookMiner live in Uniswap's v4-hooks-public repository, which brings v4-core and v4-periphery with it. With Foundry:

Shell
forge install Uniswap/v4-hooks-public
remappings.txt
@uniswap/v4-hooks-public/=lib/v4-hooks-public/
@uniswap/v4-core/=lib/v4-hooks-public/lib/v4-core/
@uniswap/v4-periphery/=lib/v4-hooks-public/lib/v4-periphery/
forge-std/=lib/v4-hooks-public/lib/forge-std/src/
solmate/=lib/v4-hooks-public/lib/v4-core/lib/solmate/
foundry.toml
[profile.default]
solc = "0.8.26"
evm_version = "cancun"

PoolManager uses transient storage, so tests that deploy it need the Cancun EVM.

A minimal hook#

This hook charges 1% on large swaps and 0.30% on the rest. It needs beforeInitialize, to accept only dynamic-fee pools, and beforeSwap, to set each swap's fee.

src/SizeFeeHook.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {BaseHook} from "@uniswap/v4-hooks-public/src/base/BaseHook.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";
import {LPFeeLibrary} from "@uniswap/v4-core/src/libraries/LPFeeLibrary.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol";

contract SizeFeeHook is BaseHook {
    using LPFeeLibrary for uint24;

    uint24 internal constant BASE_FEE = 3_000; // 0.30%
    uint24 internal constant LARGE_FEE = 10_000; // 1%
    uint256 internal constant LARGE = 1e21;

    error NotDynamicFee();

    constructor(IPoolManager manager) BaseHook(manager) {}

    function getHookPermissions() public pure override returns (Hooks.Permissions memory) {
        return Hooks.Permissions({
            beforeInitialize: true,
            afterInitialize: false,
            beforeAddLiquidity: false,
            afterAddLiquidity: false,
            beforeRemoveLiquidity: false,
            afterRemoveLiquidity: false,
            beforeSwap: true,
            afterSwap: false,
            beforeDonate: false,
            afterDonate: false,
            beforeSwapReturnDelta: false,
            afterSwapReturnDelta: false,
            afterAddLiquidityReturnDelta: false,
            afterRemoveLiquidityReturnDelta: false
        });
    }

    // Only a dynamic-fee pool lets beforeSwap set the fee.
    function _beforeInitialize(address, PoolKey calldata key, uint160) internal pure override returns (bytes4) {
        if (!key.fee.isDynamicFee()) revert NotDynamicFee();
        return BaseHook.beforeInitialize.selector;
    }

    function _beforeSwap(address, PoolKey calldata, SwapParams calldata params, bytes calldata)
        internal
        pure
        override
        returns (bytes4, BeforeSwapDelta, uint24)
    {
        int256 amount = params.amountSpecified;
        uint256 size = uint256(amount < 0 ? -amount : amount);
        uint24 fee = size >= LARGE ? LARGE_FEE : BASE_FEE;
        return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, fee | LPFeeLibrary.OVERRIDE_FEE_FLAG);
    }
}

Rules your hook follows#

  1. Declare exactly its permissions

    getHookPermissions lists all 14 flags. BaseHook's constructor checks them against the contract's own address and reverts with HookAddressNotValid if any differs.

  2. Override the internal callbacks

    BaseHook's external callbacks accept calls only from PoolManager and forward them to internal ones: _beforeSwap, _afterAddLiquidity and so on. Override the internal callback for every flag you set. One you leave out reverts with HookNotImplemented on every call.

  3. Return the selector

    Every callback returns the selector of the function PoolManager called. A wrong one reverts the operation with InvalidHookResponse.

  4. Settle what you take

    With a returns-delta flag, the delta you return is owed to or by your hook. Settle it in the same unlock, or the whole transaction reverts.

  5. Refuse with a revert

    To reject an operation, revert. The whole operation reverts with it, and nothing moves.

Mine the address#

PoolManager reads the hook's permissions from the lowest 14 bits of its address, so the hook has to be deployed where those bits match. HookMiner.find searches CREATE2 salts until the address carries your flags:

uint160 flags = uint160(Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG);
(address hookAddress, bytes32 salt) =
    HookMiner.find(deployer, flags, type(SizeFeeHook).creationCode, abi.encode(poolManager));
SizeFeeHook hook = new SizeFeeHook{salt: salt}(poolManager);
require(address(hook) == hookAddress);

deployer is the address that executes CREATE2, and the constructor arguments must be the ones you deploy with. On Robinhood Chain, poolManager is 0x8366a39cc670b4001a1121b8f6a443a643e40951.

Test against PoolManager#

v4-core's Deployers deploys a fresh PoolManager, test routers and two tokens. Mine against the test contract, which is the deployer of new in a test:

test/SizeFeeHook.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol";
import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
import {LPFeeLibrary} from "@uniswap/v4-core/src/libraries/LPFeeLibrary.sol";
import {HookMiner} from "@uniswap/v4-hooks-public/src/utils/HookMiner.sol";
import {SizeFeeHook} from "../src/SizeFeeHook.sol";

contract SizeFeeHookTest is Deployers {
    SizeFeeHook hook;

    function setUp() public {
        deployFreshManagerAndRouters();
        deployMintAndApprove2Currencies();

        uint160 flags = uint160(Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG);
        (address expected, bytes32 salt) =
            HookMiner.find(address(this), flags, type(SizeFeeHook).creationCode, abi.encode(manager));
        hook = new SizeFeeHook{salt: salt}(manager);
        assertEq(address(hook), expected);

        (key,) = initPoolAndAddLiquidity(
            currency0, currency1, IHooks(address(hook)), LPFeeLibrary.DYNAMIC_FEE_FLAG, SQRT_PRICE_1_1
        );
    }

    function test_swap() public {
        swap(key, true, -1e15, ZERO_BYTES);
    }

    function test_refusesStaticFeePool() public {
        vm.expectRevert();
        initPool(currency0, currency1, IHooks(address(hook)), 3_000, SQRT_PRICE_1_1);
    }
}

Test every flag you set, both swap directions, exact input and exact output, and every path that reverts.

Get it listed#

Socket's built-in hooks are on the allowlist from the start. Any other hook is added by a vote of SOCKET holders. The author proposes it with:

  • its verified source on the explorer, Blockscout;
  • its address and flags;
  • whoever can change it (an owner, a proxy admin), if anyone;
  • an independent review.

Proposing locks 1,000,000 SOCKET from the author while the hook is listed.

OutcomeThe stake
The hook is listedStays locked while the hook is listed.
The proposal failsCan be withdrawn 7 days after the proposal.
The hook is removedCan be withdrawn 7 days after the removal.
The deployed code doesn't match the published sourceA removal vote can burn it.

The vote runs like every other: see Vote with SOCKET. Once listed, the app routes through pools on the hook and shows it as External hook in mustard.

Public documentation snapshot · 9 October 2026 · Original page ↗
LOCAL REPLICA · DOCUMENTATION SNAPSHOT