Building a v4 hook? Pick what it does, set the numbers, and deploy it as your own hook or stacked on v4 Marketplace. Open the builder ↗
v4 Builder docs
Build a v4 hook from modules

Modules

Ten modules, each a rule with a few numbers. The builder checks every number against the bounds below before it writes a line, and the Solidity under each module is what it writes at the default numbers, taken from the generator itself.

How modules combine

Fees are in pips: 1,000,000 pips are 100%, so 3,000 pips are 0.30%. Moves and cuts are in basis points: 10,000 are 100%. Every module writes into one internal function, _rules, which runs on each swap in a fixed order:

  1. The refusals first: allowlist, cooldown, price guard. Nothing is written when a swap is refused.
  2. Then the bookkeeping: the cooldown's last swap, the volatility average, the price guard's window.
  3. Then the fee proposals, where the highest wins, and the rebates, where the largest wins.

On a hook of its own, every swap pays at least the base fee (0 to 100,000 pips, 3,000 by default), and the rebate comes off the result. The pool must be opened with the dynamic fee flag for the override to apply. This is the end of _beforeSwap for anti-snipe, cooldown and AI agent rebate together:

Own v4 hook: _beforeSwap
function _beforeSwap(address, PoolKey calldata key, SwapParams calldata params, bytes calldata)
    internal
    override
    returns (bytes4, BeforeSwapDelta, uint24)
{
    (uint24 fee, uint8 rebate, uint8 refused) = _rules(key.toId(), tx.origin, params.zeroForOne);
    if (refused == COOLING_DOWN) revert CoolingDown(lastSwapAt[key.toId()][tx.origin] + COOLDOWN);
    uint256 f = fee > BASE_FEE ? fee : BASE_FEE;
    if (rebate > 0) f = f * (100 - rebate) / 100;
    return (this.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, uint24(f) | LPFeeLibrary.OVERRIDE_FEE_FLAG);
}

A stacked hook does not compose its own fee: it answers the hook stack with its proposal, its rebate and a veto, and the stack composes the answers of every hook in the pool.

Stacked hook: beforeSwap
function beforeSwap(address origin, PoolKey calldata key, SwapParams calldata params)
    external
    returns (uint24 feePips, uint8 rebatePct, bool veto);
function afterSwap(PoolKey calldata key, SwapParams calldata params, BalanceDelta delta) external;
}

contract Example is ILog {
using PoolIdLibrary for PoolKey;
using StateLibrary for IPoolManager;

/// The v4 Marketplace hook stack (the Dam): the only caller.
address public immutable hookStack;

uint256 public constant CALM_FEE = 500; // 0.05%
uint256 public constant MAX_FEE = 20000; // 2%
uint256 public constant FEE_PER_PERCENT = 2000; // pips added for each 1% of average move
uint256 public constant CLOSED_FEE = 10000; // 1% while the stock market is closed
IPoolManager public constant POOL_MANAGER = IPoolManager(0x8366a39CC670B4001A1121B8F6A443A643e40951);

struct PoolState {
    uint64 openedAt;
    int24 lastTick;
    uint32 ewma; // 100 x the average tick move between swaps
}

01Anti-snipe fee

A high fee at the open that falls to nothing.

The fee starts at 25% when the pool opens and falls in a straight line to zero over 10 minutes.
ParameterUnitBoundsDefault
Fee at the openpips1,000 to 500,000 (0.1% to 50%)250,000 (25%)
Falls to zero overseconds60 to 604,800 (1 minute to 7 days)600 (10 minutes)

Targets: own hook and stacked hook. Callbacks on an own hook: afterInitialize, beforeSwap.

The Solidity it writes

Anti-snipe fee, default numbers
uint256 public constant LAUNCH_FEE = 250000; // 25% at the open
uint256 public constant LAUNCH_WINDOW = 600; // seconds for it to fall to zero

function _afterInitialize(address, PoolKey calldata key, uint160, int24)
    internal
    override
    returns (bytes4)
{
    if (!key.fee.isDynamicFee()) revert NeedsDynamicFee();
    PoolId id = key.toId();
    PoolState storage s = pools[id];
    s.openedAt = uint64(block.timestamp);
    return this.afterInitialize.selector;
}

/// The rules, in order: the refusals first (nothing is written when a swap is refused),
/// then the bookkeeping, then the fee proposals (the highest wins) and the rebates (the largest wins).
function _rules(PoolId id, address, bool)
    internal
    view returns (uint24, uint8, uint8)
{
    uint24 fee;
    PoolState storage s = pools[id];
    {
        uint256 elapsed = block.timestamp - s.openedAt;
        if (elapsed < LAUNCH_WINDOW) {
            uint256 f = LAUNCH_FEE * (LAUNCH_WINDOW - elapsed) / LAUNCH_WINDOW;
            if (f > fee) fee = uint24(f);
        }
    }
    return (fee, 0, 0);
}

02Volatility fee

Cheap when the price is calm, dearer when it jumps.

The fee follows the average price move between swaps: 0.05% when calm, plus 0.2% for each 1% the price moves on average, up to 2%.
ParameterUnitBoundsDefault
Fee when calmpips100 to 30,000 (0.01% to 3%)500 (0.05%)
Fee cappips1,000 to 100,000 (0.1% to 10%)20,000 (2%)
Added per 1% of average movepips100 to 50,000 (0.01% to 5%)2,000 (0.2%)

Targets: own hook and stacked hook. Callbacks on an own hook: afterInitialize, beforeSwap.

The Solidity it writes

Volatility fee, default numbers
uint256 public constant CALM_FEE = 500; // 0.05%
uint256 public constant MAX_FEE = 20000; // 2%
uint256 public constant FEE_PER_PERCENT = 2000; // pips added for each 1% of average move

function _afterInitialize(address, PoolKey calldata key, uint160, int24 tick)
    internal
    override
    returns (bytes4)
{
    if (!key.fee.isDynamicFee()) revert NeedsDynamicFee();
    PoolId id = key.toId();
    PoolState storage s = pools[id];
    s.openedAt = uint64(block.timestamp);
    s.lastTick = tick;
    return this.afterInitialize.selector;
}

/// The rules, in order: the refusals first (nothing is written when a swap is refused),
/// then the bookkeeping, then the fee proposals (the highest wins) and the rebates (the largest wins).
function _rules(PoolId id, address, bool)
    internal
    returns (uint24, uint8, uint8)
{
    uint24 fee;
    PoolState storage s = pools[id];
    (, int24 tick,,) = poolManager.getSlot0(id);
    {
        int256 d = int256(tick) - int256(s.lastTick);
        uint256 move = uint256(d < 0 ? -d : d);
        uint256 e = (7 * uint256(s.ewma) + 100 * move) / 8;
        if (e > type(uint32).max) e = type(uint32).max;
        s.ewma = uint32(e);
        s.lastTick = tick;
        // e is 100 x the average move in ticks, and 100 ticks are a 1% move.
        uint256 v = CALM_FEE + FEE_PER_PERCENT * e / 10_000;
        if (v > MAX_FEE) v = MAX_FEE;
        if (v > fee) fee = uint24(v);
    }
    return (fee, 0, 0);
}

03Off-hours fee

A higher fee while the stock market is closed.

From 20:00 to 14:30 UTC on weekdays and all weekend, when the stock market is closed, the fee is 1%.
ParameterUnitBoundsDefault
Fee while the market is closedpips500 to 100,000 (0.05% to 10%)10,000 (1%)

Targets: own hook and stacked hook. Callbacks on an own hook: beforeSwap.

The Solidity it writes

Off-hours fee, default numbers
uint256 public constant CLOSED_FEE = 10000; // 1% while the stock market is closed

function _afterInitialize(address, PoolKey calldata key, uint160, int24)
    internal
    pure
    override
    returns (bytes4)
{
    if (!key.fee.isDynamicFee()) revert NeedsDynamicFee();
    return this.afterInitialize.selector;
}

/// The rules, in order: the refusals first (nothing is written when a swap is refused),
/// then the bookkeeping, then the fee proposals (the highest wins) and the rebates (the largest wins).
function _rules(PoolId, address, bool)
    internal
    view returns (uint24, uint8, uint8)
{
    uint24 fee;
    if (_marketClosed(block.timestamp) && CLOSED_FEE > fee) fee = uint24(CLOSED_FEE);
    return (fee, 0, 0);
}

/// NYSE regular hours, 14:30 to 20:00 UTC, Monday to Friday. Holidays count as open.
function _marketClosed(uint256 t) internal pure returns (bool) {
    uint256 weekday = (t / 1 days + 4) % 7; // 0 is Sunday
    if (weekday == 0 || weekday == 6) return true;
    uint256 second = t % 1 days;
    return second < 14 hours + 30 minutes || second >= 20 hours;
}

04Cooldown

One swap per wallet per window.

A wallet that just swapped in this pool waits 30 seconds before its next swap.
ParameterUnitBoundsDefault
Wait between swapsseconds5 to 86,400 (5 seconds to 1 day)30 (30 seconds)

Targets: own hook and stacked hook. Callbacks on an own hook: beforeSwap.

The Solidity it writes

Cooldown, default numbers
uint256 public constant COOLDOWN = 30; // seconds between two swaps of one wallet

/// The rules, in order: the refusals first (nothing is written when a swap is refused),
/// then the bookkeeping, then the fee proposals (the highest wins) and the rebates (the largest wins).
function _rules(PoolId id, address origin, bool)
    internal
    returns (uint24, uint8, uint8)
{
    uint256 last = lastSwapAt[id][origin];
    if (last != 0 && block.timestamp < last + COOLDOWN) return (0, 0, COOLING_DOWN);
    lastSwapAt[id][origin] = block.timestamp;
    return (0, 0, 0);
}

05Price guard

Stops swaps that push a runaway price further.

Once the price has moved more than 10% within 5 minutes, swaps that push it further are refused until the window rolls over. Swaps that bring it back go through.
ParameterUnitBoundsDefault
Largest move allowedbasis points100 to 5,000 (1% to 50%)1,000 (10%)
Withinseconds60 to 86,400 (1 minute to 1 day)300 (5 minutes)

Targets: own hook and stacked hook. Callbacks on an own hook: afterInitialize, beforeSwap.

The Solidity it writes

Price guard, default numbers
int24 public constant MAX_MOVE_TICKS = 953; // a 10% move
uint256 public constant GUARD_WINDOW = 300; // seconds

function _afterInitialize(address, PoolKey calldata key, uint160, int24 tick)
    internal
    override
    returns (bytes4)
{
    PoolId id = key.toId();
    PoolState storage s = pools[id];
    s.openedAt = uint64(block.timestamp);
    s.windowStart = uint64(block.timestamp);
    s.windowTick = tick;
    return this.afterInitialize.selector;
}

/// The rules, in order: the refusals first (nothing is written when a swap is refused),
/// then the bookkeeping, then the fee proposals (the highest wins) and the rebates (the largest wins).
function _rules(PoolId id, address, bool zeroForOne)
    internal
    returns (uint24, uint8, uint8)
{
    PoolState storage s = pools[id];
    (, int24 tick,,) = poolManager.getSlot0(id);
    if (block.timestamp >= s.windowStart + GUARD_WINDOW) {
        s.windowStart = uint64(block.timestamp);
        s.windowTick = tick;
    } else {
        int256 moved = int256(tick) - int256(s.windowTick);
        // A swap of token0 for token1 lowers the tick; the other way raises it.
        if ((moved > MAX_MOVE_TICKS && !zeroForOne) || (moved < -MAX_MOVE_TICKS && zeroForOne)) return (0, 0, PRICE_GUARD);
    }
    return (0, 0, 0);
}

06Launch allowlist

Only listed wallets can swap at the open.

For the first 15 minutes, only wallets you list can swap. You can add more from your wallet after the deploy.
ParameterUnitBoundsDefault
Allowlist only forseconds60 to 604,800 (1 minute to 7 days)900 (15 minutes)
Walletsaddressesup to 50, by comma or new linenone

Targets: own hook and stacked hook. Callbacks on an own hook: afterInitialize, beforeSwap. The deployer's wallet becomes the owner of the allowlist and can add or remove wallets after the deploy.

The Solidity it writes

Launch allowlist, default numbers
uint256 public constant ALLOWLIST_WINDOW = 900; // seconds after the open

function _afterInitialize(address, PoolKey calldata key, uint160, int24)
    internal
    override
    returns (bytes4)
{
    PoolId id = key.toId();
    PoolState storage s = pools[id];
    s.openedAt = uint64(block.timestamp);
    return this.afterInitialize.selector;
}

/// The rules, in order: the refusals first (nothing is written when a swap is refused),
/// then the bookkeeping, then the fee proposals (the highest wins) and the rebates (the largest wins).
function _rules(PoolId id, address origin, bool)
    internal
    view returns (uint24, uint8, uint8)
{
    PoolState storage s = pools[id];
    if (block.timestamp < s.openedAt + ALLOWLIST_WINDOW && !allowed[origin]) return (0, 0, NOT_ALLOWED);
    return (0, 0, 0);
}

/// The deployer adds or removes wallets from the launch allowlist.
function setAllowed(address[] calldata wallets, bool ok) external {
    if (msg.sender != owner) revert NotOwner();
    for (uint256 i; i < wallets.length; i++) {
        allowed[wallets[i]] = ok;
        emit Allowed(wallets[i], ok);
    }
}

07Holder rebate

Holders of a token pay a lower fee.

A wallet holding at least the minimum of the token at an address pays 25% less fee.
ParameterUnitBoundsDefault
Tokenaddressany address but zeronone
Minimum heldtoken unitsa whole number, up to 60 digits0
Fee cutpercent1% to 100%25%

Targets: own hook and stacked hook. Callbacks on an own hook: beforeSwap. The balance is read with a capped static call: a token that reverts counts as zero, so it can never make a swap fail.

The Solidity it writes

Holder rebate, default numbers, USDG as the token
address public constant HOLD_TOKEN = 0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168;
uint256 public constant HOLD_MIN = 1000000; // in the token's own units
uint8 public constant HOLDER_REBATE = 25; // percent off the fee

function _afterInitialize(address, PoolKey calldata key, uint160, int24)
    internal
    pure
    override
    returns (bytes4)
{
    if (!key.fee.isDynamicFee()) revert NeedsDynamicFee();
    return this.afterInitialize.selector;
}

/// The rules, in order: the refusals first (nothing is written when a swap is refused),
/// then the bookkeeping, then the fee proposals (the highest wins) and the rebates (the largest wins).
function _rules(PoolId, address origin, bool)
    internal
    view returns (uint24, uint8, uint8)
{
    uint8 rebate;
    if (_balance(HOLD_TOKEN, origin) >= HOLD_MIN && HOLDER_REBATE > rebate) rebate = HOLDER_REBATE;
    return (0, rebate, 0);
}

/// A balance read that can never make the swap fail: a token that reverts or answers badly counts as zero.
function _balance(address token, address wallet) internal view returns (uint256 b) {
    (bool ok, bytes memory ret) = token.staticcall{gas: 30_000}(abi.encodeWithSelector(bytes4(0x70a08231), wallet));
    if (ok && ret.length >= 32) b = abi.decode(ret, (uint256));
}

08AI agent rebate

Registered AI agents pay a lower fee.

A wallet that holds an AI agent identity in the chain's ERC-8004 registry pays 25% less fee.
ParameterUnitBoundsDefault
Fee cutpercent1% to 100%25%

Targets: own hook and stacked hook. Callbacks on an own hook: beforeSwap. The identity is read from the chain's ERC-8004 identity registry, with the same capped static call.

The Solidity it writes

AI agent rebate, default numbers
address public constant AGENT_REGISTRY = 0x8004A169FB4a3325136EB29fA0ceB6D2e539a432; // ERC-8004 identity registry
uint8 public constant AGENT_REBATE = 25; // percent off the fee

function _afterInitialize(address, PoolKey calldata key, uint160, int24)
    internal
    pure
    override
    returns (bytes4)
{
    if (!key.fee.isDynamicFee()) revert NeedsDynamicFee();
    return this.afterInitialize.selector;
}

/// The rules, in order: the refusals first (nothing is written when a swap is refused),
/// then the bookkeeping, then the fee proposals (the highest wins) and the rebates (the largest wins).
function _rules(PoolId, address origin, bool)
    internal
    view returns (uint24, uint8, uint8)
{
    uint8 rebate;
    if (_balance(AGENT_REGISTRY, origin) > 0 && AGENT_REBATE > rebate) rebate = AGENT_REBATE;
    return (0, rebate, 0);
}

/// A balance read that can never make the swap fail: a token that reverts or answers badly counts as zero.
function _balance(address token, address wallet) internal view returns (uint256 b) {
    (bool ok, bytes memory ret) = token.staticcall{gas: 30_000}(abi.encodeWithSelector(bytes4(0x70a08231), wallet));
    if (ok && ret.length >= 32) b = abi.decode(ret, (uint256));
}

09Swap tax

A cut of every swap, sent to a wallet.

0.3% of every swap's output (or input, on exact-output swaps) is sent to an address.
ParameterUnitBoundsDefault
Cut of each swapbasis points1 to 500 (0.01% to 5%)30 (0.3%)
Sent toaddressany address but zeronone

Targets: own hook only. Callbacks on an own hook: afterSwap, afterSwapReturnDelta.

The Solidity it writes

Swap tax, default numbers, sent to 0x…dEaD
uint256 public constant TAX_BPS = 30; // 0.3% of each swap
address public constant TAX_TO = 0x000000000000000000000000000000000000dEaD; // burned

/// The cut is taken in the swap's unspecified currency: the output of an exact-input swap,
/// the input of an exact-output one. The swapper's delta carries it, the pool is untouched.
function _afterSwap(address, PoolKey calldata key, SwapParams calldata params, BalanceDelta delta, bytes calldata)
    internal
    override
    returns (bytes4, int128)
{
    bool specifiedIs0 = (params.amountSpecified < 0) == params.zeroForOne;
    (Currency currency, int128 amount) = specifiedIs0 ? (key.currency1, delta.amount1()) : (key.currency0, delta.amount0());
    if (amount < 0) amount = -amount;
    uint256 cut = uint256(uint128(amount)) * TAX_BPS / 10_000;
    if (cut == 0) return (this.afterSwap.selector, 0);
    poolManager.take(currency, TAX_TO, cut);
    return (this.afterSwap.selector, int128(int256(cut)));
}

10Liquidity lock

No liquidity leaves the pool before a date.

Liquidity added to the pool cannot be removed for 30 days after the pool opens.
ParameterUnitBoundsDefault
Locked fordays1 to 3,65030

Targets: own hook only. Callbacks on an own hook: afterInitialize, beforeRemoveLiquidity.

The Solidity it writes

Liquidity lock, default numbers
uint256 public constant LP_LOCK = 2592000; // 30 days after the open

function _afterInitialize(address, PoolKey calldata key, uint160, int24)
    internal
    override
    returns (bytes4)
{
    PoolId id = key.toId();
    PoolState storage s = pools[id];
    s.openedAt = uint64(block.timestamp);
    return this.afterInitialize.selector;
}

function _beforeRemoveLiquidity(address, PoolKey calldata key, ModifyLiquidityParams calldata, bytes calldata)
    internal
    view
    override
    returns (bytes4)
{
    uint256 until = pools[key.toId()].openedAt + LP_LOCK;
    if (block.timestamp < until) revert LiquidityLocked(until);
    return this.beforeRemoveLiquidity.selector;
}