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 ↗

Stock Hours

What it does

Written by an earlier version of the builder: its verified source below is what runs.

    Volatility feeOff-hours feePrice guard
    src/StockHours.sol, as verified on Sourcify
    // SPDX-License-Identifier: MIT
    pragma solidity 0.8.26;
    
    // StockHours: a v4 hook for the v4 Marketplace hook stack, written by v4 Builder (v4builder/1) from the recipe below.
    // The hook stack calls it on every swap of the pools that stack it; it answers with a fee, a rebate or a veto.
    //
    // - The fee follows the average price move between swaps: 0.05% when calm, plus 0% for each 0.01% of
    //   average move, up to 2%.
    // - From 20:00 to 14:30 UTC on weekdays and all weekend, when the stock market is closed, the fee is
    //   1%.
    // - Once the price has moved more than 5% within 15 minutes, swaps that push it further are refused
    //   until the window rolls over. Swaps that bring it back go through.
    
    import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
    import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";
    import {SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
    import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
    import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
    import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
    
    /// The interface every hook listed on v4 Marketplace implements, as served at v4market.com/sdk/ILog.sol.
    interface ILog {
        function onStack(PoolKey calldata key, bytes calldata cfg) external;
        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 StockHours 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_TICK = 20; // pips added per tick (0.01%) of average move
        uint256 public constant CLOSED_FEE = 10000; // 1% while the stock market is closed
        int24 public constant MAX_MOVE_TICKS = 487; // a 5% move
        uint256 public constant GUARD_WINDOW = 900; // seconds
        IPoolManager public constant POOL_MANAGER = IPoolManager(0x8366a39CC670B4001A1121B8F6A443A643e40951);
        uint8 internal constant PRICE_GUARD = 1;
    
        struct PoolState {
            uint64 openedAt;
            int24 lastTick;
            uint32 ewma; // 100 x the average tick move between swaps
            uint64 windowStart;
            int24 windowTick;
        }
    
        mapping(PoolId => PoolState) public pools;
    
        error NotHookStack();
    
        constructor(address hookStack_) {
            hookStack = hookStack_;
        }
    
        modifier onlyHookStack() {
            if (msg.sender != hookStack) revert NotHookStack();
            _;
        }
    
        /// Called once by the hook stack when a pool stacks this hook.
        function onStack(PoolKey calldata key, bytes calldata) external onlyHookStack {
            PoolId id = key.toId();
            (, int24 tick,,) = POOL_MANAGER.getSlot0(id);
            PoolState storage s = pools[id];
            if (s.openedAt != 0) return;
            s.openedAt = uint64(block.timestamp);
            s.lastTick = tick;
            s.windowStart = uint64(block.timestamp);
            s.windowTick = tick;
        }
    
        /// Called by the hook stack before every swap: a fee proposal in pips (0 for no opinion),
        /// a rebate in percent, and a veto. It never reverts on a rule.
        function beforeSwap(address origin, PoolKey calldata key, SwapParams calldata params)
            external
            onlyHookStack
            returns (uint24 feePips, uint8 rebatePct, bool veto)
        {
            (uint24 fee, uint8 rebate, uint8 refused) = _rules(key.toId(), origin, params.zeroForOne);
            return (fee, rebate, refused != 0);
        }
    
        /// Not used: this hook is listed with afterSwap off.
        function afterSwap(PoolKey calldata, SwapParams calldata, BalanceDelta) external view onlyHookStack {}
    
        /// 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)
        {
            uint24 fee;
            PoolState storage s = pools[id];
            (, int24 tick,,) = POOL_MANAGER.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);
            }
            {
                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;
                uint256 v = CALM_FEE + FEE_PER_TICK * e / 100;
                if (v > MAX_FEE) v = MAX_FEE;
                if (v > fee) fee = uint24(v);
            }
            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;
        }
    }
    

    The recipe, as written in the factory's event

    {"v":1,"name":"Stock Hours","target":"stack","baseFee":500,"modules":[{"id":"volatilityFee","p":{"calmFee":500,"maxFee":20000,"sensitivity":20}},{"id":"offHoursFee","p":{"closedFee":10000}},{"id":"priceGuard","p":{"maxMove":500,"window":900}}]}
    Start from this recipe