Base

Base contract implementations are provided in the library as building blocks to leverage Uniswap v4’s features natively, such as custom accounting, custom curves, and asynchronous swaps.

Hook

BaseHook is provided as the underlying scaffolding contract. It declares every supported hook callback along with modifiers and revert statements that enforce security and prevent misuse. By design, all hook entrypoints/actions are turned off. This allows the inheriting contract to choose which methods to enable by overriding the permissions struct in getHookPermissions and implementing the respective internal functions.

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

import {BaseHook} from "src/base/BaseHook.sol";
import {Hooks} from "v4-core/src/libraries/Hooks.sol";
import {PoolKey} from "v4-core/src/types/PoolKey.sol";
import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
import {BeforeSwapDelta, toBeforeSwapDelta} from "v4-core/src/types/BeforeSwapDelta.sol";

contract CounterHook is BaseHook {
    uint256 public counter;

    constructor(IPoolManager _poolManager) BaseHook(_poolManager) {}

    /**
     * @inheritdoc BaseHook
     */
    function _beforeSwap(address, PoolKey calldata, IPoolManager.SwapParams calldata, bytes calldata)
        internal
        virtual
        override
        returns (bytes4, BeforeSwapDelta, uint24)
    {
        counter++;
        return (this.beforeSwap.selector, toBeforeSwapDelta(0, 0), 0);
    }

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

Among the security checks enforced by BaseHook, validateHookAddress ensures that the contract address matches the declared permissions.

Custom Accounting

BaseCustomAccounting inherits from BaseHook to enforce hook-owned liquidity and allow for custom token accounting for a specific pool. Liquidity modifications (addition/removal) are handled directly by the hook contract and then apply them to the pool via the PoolManager.

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

import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
import {BaseCustomAccounting} from "src/base/BaseCustomAccounting.sol";
import {ERC20} from "openzeppelin/token/ERC20/ERC20.sol";
import {FullMath} from "v4-core/src/libraries/FullMath.sol";
import {TickMath} from "v4-core/src/libraries/TickMath.sol";
import {LiquidityAmounts} from "v4-periphery/src/libraries/LiquidityAmounts.sol";
import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol";
import {SafeCast} from "v4-core/src/libraries/SafeCast.sol";
import {StateLibrary} from "v4-core/src/libraries/StateLibrary.sol";

contract SimpleAccounting is BaseCustomAccounting, ERC20 {
    using SafeCast for uint256;
    using StateLibrary for IPoolManager;

    constructor(IPoolManager _poolManager) BaseCustomAccounting(_poolManager) ERC20("Mock", "MOCK") {}

    /// @inheritdoc BaseCustomAccounting
    function _getAddLiquidity(uint160 sqrtPriceX96, AddLiquidityParams memory params)
        internal
        pure
        override
        returns (bytes memory modify, uint256 liquidity)
    {
        liquidity = LiquidityAmounts.getLiquidityForAmounts(
            sqrtPriceX96,
            TickMath.getSqrtPriceAtTick(params.tickLower),
            TickMath.getSqrtPriceAtTick(params.tickUpper),
            params.amount0Desired,
            params.amount1Desired
        );

        return (
            abi.encode(
                IPoolManager.ModifyLiquidityParams({
                    tickLower: params.tickLower,
                    tickUpper: params.tickUpper,
                    liquidityDelta: liquidity.toInt256(),
                    salt: 0
                })
            ),
            liquidity
        );
    }

    /// @inheritdoc BaseCustomAccounting
    function _getRemoveLiquidity(RemoveLiquidityParams memory params)
        internal
        view
        override
        returns (bytes memory, uint256 liquidity)
    {
        liquidity = FullMath.mulDiv(params.liquidity, poolManager.getLiquidity(poolKey.toId()), totalSupply());

        return (
            abi.encode(
                IPoolManager.ModifyLiquidityParams({
                    tickLower: params.tickLower,
                    tickUpper: params.tickUpper,
                    liquidityDelta: -liquidity.toInt256(),
                    salt: 0
                })
            ),
            liquidity
        );
    }

    /// @inheritdoc BaseCustomAccounting
    function _mint(AddLiquidityParams memory params, BalanceDelta, uint256 liquidity) internal override {
        _mint(params.to, liquidity);
    }

    /// @inheritdoc BaseCustomAccounting
    function _burn(RemoveLiquidityParams memory, BalanceDelta, uint256 liquidity) internal override {
        _burn(msg.sender, liquidity);
    }
}

The inheriting contracts must implement the respective functions to calculate the liquidity modification parameters and the amount of liquidity shares to mint or burn. Additionally, the implementer must keep in mind that the hook is the sole liquidity owner and is therefore responsible for managing fees on any liquidity shares.

Custom Curve

Building on the custom accounting foundation, BaseCustomCurve takes customization a step further by allowing developers to completely replace Uniswap v4’s default concentrated liquidity math with their own swap logic.

By overriding the _beforeSwap function, the inheriting contract can implement its own swap logic and curves. Because the hook still owns the liquidity, it can route tokens around in ways that diverge from the standard invariant, perhaps adopting stable-swap curves, bonding curves, or other designs that better suit specialized use cases. The contract also redefines how liquidity additions and removals occur internally, but it does so in a manner that remains compatible with the rest of the Uniswap v4 engine’s architecture and routers.

Async Swap

BaseAsyncSwap offers a way to skip the execution of exact-input swaps by the PoolManager in order to support asynchronous swaps and other cases that require non-atomic execution.

When processing exact-input swaps, the hook returns a delta that nets out the input amount to zero, then mints ERC-6909 tokens to the contract’s address. This approach effectively bypasses the standard swap logic and allows the hook to manage user positions or tokens until a final settlement stage. The user’s input tokens are held by the hook contract, which can later be redeemed or settled according to logic defined by the implementer.