All skills
uniswap avatar

/v4-security-foundations

@a718080
by Uniswap Labsuniswap/uniswap-ai233 stars
39

Security-first Uniswap v4 hook development. Use when user mentions "v4 hooks", "hook security", "PoolManager", "beforeSwap", "afterSwap", or asks about V4 hook best practices, vulnerabilities, or audit requirements.

Use this Skill: https://skilld.dev/gh/uniswap/uniswap-ai/v4-security-foundations

This session only. Nothing lands on disk.

referencesbase-hook-template.md

≈3.1k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Base Hook Template

A security-first Solidity template for Uniswap v4 hooks with all permissions disabled by default.

Complete Template

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {BaseHook} from "v4-periphery/src/base/hooks/BaseHook.sol";
import {Hooks} from "v4-core/src/libraries/Hooks.sol";
import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
import {PoolKey} from "v4-core/src/types/PoolKey.sol";
import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol";
import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol";
import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "v4-core/src/types/BeforeSwapDelta.sol";
import {Currency} from "v4-core/src/types/Currency.sol";

/// @title SecureHook
/// @notice Security-first v4 hook template
/// @dev All permissions disabled by default - enable only what you need
contract SecureHook is BaseHook {
    using PoolIdLibrary for PoolKey;

    // ═══════════════════════════════════════════════════════════════════════
    // ERRORS
    // ═══════════════════════════════════════════════════════════════════════

    error NotPoolManager();
    error RouterNotAllowed();
    error ZeroAddress();
    error NotAdmin();
    error Unauthorized();
    error AdminTransferToSelf();
    error NoPendingAdmin();

    // ═══════════════════════════════════════════════════════════════════════
    // EVENTS
    // ═══════════════════════════════════════════════════════════════════════

    event RouterAdded(address indexed router);
    event RouterRemoved(address indexed router);
    event AdminTransferProposed(address indexed currentAdmin, address indexed proposedAdmin);
    event AdminTransferred(address indexed previousAdmin, address indexed newAdmin);

    // ═══════════════════════════════════════════════════════════════════════
    // STATE
    // ═══════════════════════════════════════════════════════════════════════

    /// @notice Allowlisted routers that can interact with this hook
    mapping(address => bool) public allowedRouters;

    /// @notice Hook administrator
    address public admin;

    /// @notice Pending administrator for two-step transfer
    /// @dev Set by proposeAdmin(), cleared by acceptAdmin(). Only the pendingAdmin
    ///      address can call acceptAdmin() to complete the transfer. This ensures
    ///      admin privileges are never transferred to an address that cannot
    ///      interact with the contract (e.g., a typo or non-existent wallet).
    address public pendingAdmin;

    // ═══════════════════════════════════════════════════════════════════════
    // MODIFIERS
    // ═══════════════════════════════════════════════════════════════════════

    /// @notice Ensures caller is the PoolManager
    modifier onlyPoolManager() {
        if (msg.sender != address(poolManager)) revert NotPoolManager();
        _;
    }

    /// @notice Ensures sender (router) is allowlisted
    modifier onlyAllowedRouter(address sender) {
        if (!allowedRouters[sender]) revert RouterNotAllowed();
        _;
    }

    /// @notice Ensures caller is admin
    modifier onlyAdmin() {
        if (msg.sender != admin) revert NotAdmin();
        _;
    }

    // ═══════════════════════════════════════════════════════════════════════
    // CONSTRUCTOR
    // ═══════════════════════════════════════════════════════════════════════

    constructor(IPoolManager _poolManager) BaseHook(_poolManager) {
        admin = msg.sender;
    }

    // ═══════════════════════════════════════════════════════════════════════
    // HOOK PERMISSIONS - ALL DISABLED BY DEFAULT
    // ═══════════════════════════════════════════════════════════════════════

    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: false,
            afterSwap: false,
            beforeDonate: false,
            afterDonate: false,
            // DANGER ZONE - These enable delta manipulation
            beforeSwapReturnDelta: false,      // CRITICAL: NoOp attack vector
            afterSwapReturnDelta: false,       // HIGH: Can extract value
            afterAddLiquidityReturnDelta: false,
            afterRemoveLiquidityReturnDelta: false
        });
    }

    // ═══════════════════════════════════════════════════════════════════════
    // HOOK CALLBACKS - Implement only what you enable
    // ═══════════════════════════════════════════════════════════════════════

    // Uncomment and implement only the callbacks you need

    /*
    function beforeInitialize(
        address sender,
        PoolKey calldata key,
        uint160 sqrtPriceX96
    ) external override onlyPoolManager returns (bytes4) {
        // Validate pool parameters here
        return BaseHook.beforeInitialize.selector;
    }

    function afterInitialize(
        address sender,
        PoolKey calldata key,
        uint160 sqrtPriceX96,
        int24 tick
    ) external override onlyPoolManager returns (bytes4) {
        // Initialize hook state here
        return BaseHook.afterInitialize.selector;
    }

    function beforeSwap(
        address sender,
        PoolKey calldata key,
        IPoolManager.SwapParams calldata params,
        bytes calldata hookData
    ) external override onlyPoolManager onlyAllowedRouter(sender) returns (bytes4, BeforeSwapDelta, uint24) {
        // Pre-swap logic here
        return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0);
    }

    function afterSwap(
        address sender,
        PoolKey calldata key,
        IPoolManager.SwapParams calldata params,
        BalanceDelta delta,
        bytes calldata hookData
    ) external override onlyPoolManager returns (bytes4, int128) {
        // Post-swap logic here
        return (BaseHook.afterSwap.selector, 0);
    }
    */

    // ═══════════════════════════════════════════════════════════════════════
    // ADMIN FUNCTIONS
    // ═══════════════════════════════════════════════════════════════════════

    /// @notice Add a router to the allowlist
    /// @param router The router address to allow
    function addAllowedRouter(address router) external onlyAdmin {
        if (router == address(0)) revert ZeroAddress();
        allowedRouters[router] = true;
        emit RouterAdded(router);
    }

    /// @notice Remove a router from the allowlist
    /// @param router The router address to remove
    function removeAllowedRouter(address router) external onlyAdmin {
        allowedRouters[router] = false;
        emit RouterRemoved(router);
    }

    /// @notice Propose a new admin (two-step transfer for safety)
    /// @dev Two-step transfer prevents accidental loss of admin privileges.
    ///      Step 1: Current admin proposes new admin via proposeAdmin()
    ///      Step 2: Proposed admin accepts the role via acceptAdmin()
    ///      Calling proposeAdmin() again overwrites any existing pending transfer.
    ///      Only the most recent proposed admin can call acceptAdmin().
    /// @param newAdmin The proposed new admin address (must not be zero or current admin)
    function proposeAdmin(address newAdmin) external onlyAdmin {
        if (newAdmin == address(0)) revert ZeroAddress();
        if (newAdmin == admin) revert AdminTransferToSelf();
        pendingAdmin = newAdmin;
        emit AdminTransferProposed(admin, newAdmin);
    }

    /// @notice Accept the admin role (must be called by the pending admin)
    /// @dev Completes the two-step admin transfer. The caller must be the address
    ///      previously set via proposeAdmin(). After acceptance, pendingAdmin is
    ///      cleared to address(0) to prevent replay. Reverts if no transfer is
    ///      pending (pendingAdmin == address(0)) to avoid silent no-ops.
    function acceptAdmin() external {
        if (pendingAdmin == address(0)) revert NoPendingAdmin();
        if (msg.sender != pendingAdmin) revert Unauthorized();
        address previousAdmin = admin;
        admin = pendingAdmin;
        pendingAdmin = address(0);
        emit AdminTransferred(previousAdmin, admin);
    }
}

Usage Guide

1. Copy the template

Copy this template to your project and rename appropriately.

2. Enable only needed permissions

In getHookPermissions(), set true only for callbacks you implement:

function getHookPermissions() public pure override returns (Hooks.Permissions memory) {
    return Hooks.Permissions({
        // ... other permissions false ...
        beforeSwap: true,  // Enable this
        afterSwap: true,   // And this
        // ... rest false ...
    });
}

3. Implement enabled callbacks

Uncomment and implement only the callbacks you enabled:

function beforeSwap(
    address sender,
    PoolKey calldata key,
    IPoolManager.SwapParams calldata params,
    bytes calldata hookData
) external override onlyPoolManager onlyAllowedRouter(sender) returns (bytes4, BeforeSwapDelta, uint24) {
    // Your logic here
    return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0);
}

4. Deploy with correct address

v4 hooks require specific address patterns. Use the hook miner:

forge script script/DeployHook.s.sol --rpc-url $RPC_URL

Security Checklist for This Template

  • PoolManager verification via onlyPoolManager modifier
  • Router allowlisting via onlyAllowedRouter modifier
  • All dangerous permissions disabled by default
  • Admin functions protected
  • Zero address checks
  • Two-step admin transfer prevents accidental privilege loss
  • Add reentrancy guard if making external calls
  • Add your specific business logic tests

Source: SKILL.md on GitHub

No alerts16d4 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    The analyzed skill contains only educational material, security templates, and audit checklists for Uniswap v4 hook development. There is no executable code or scripts, and no security risks were identified.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

  • Runlayer6mo

    2/4 files flagged

Signed by skilld at a718080. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub yesterday.

Activeupdated 8 months ago
What it can do
Reads files Network Runs commands
Modelopus
model
opus
metadata
{
  "author": "uniswap",
  "version": "1.1.0"
}
All 5 allowed tools
ReadGlobGrepWebFetchTask(subagent_type:Explore)

README badge

README badge for uniswap/uniswap-ai/v4-security-foundations