Skip to main content

Hook contract rules

This page is for people who read or edit the Solidity. It lists what a hook must look like to compile and deploy in the app.

Compiler​

SettingValue
Compilersolc 0.8.26
EVM versioncancun
Optimizeron, 200 runs
IR pipelineused automatically if the default pipeline reports "stack too deep"

The hook must be a single file.

Available imports​

Import prefixLibrary
@uniswap/v4-core/src/...Uniswap v4-core 1.0.2
@uniswap/v4-periphery/src/...Uniswap v4-periphery 1.0.3
@openzeppelin/contracts/...OpenZeppelin Contracts 5
solmate/src/...Solmate
permit2/src/...Permit2

Nothing else can be imported. Test helpers and forge-std are not available.

Structure​

  • Inherit BaseHook from @uniswap/v4-periphery/src/utils/BaseHook.sol.
  • Override the internal callbacks (_beforeSwap, _afterSwap and so on), not the external ones. BaseHook already restricts them to the PoolManager.
  • SwapParams and ModifyLiquidityParams come from @uniswap/v4-core/src/types/PoolOperation.sol.
  • getHookPermissions() must return true for exactly the callbacks the hook implements. A mismatch makes the deployment revert.

Constructor​

Use one of these two forms:

constructor(IPoolManager _poolManager) BaseHook(_poolManager) {}
constructor(IPoolManager _poolManager, address _owner) BaseHook(_poolManager) {
owner = _owner;
}

The app passes Robinhood Chain's PoolManager and, for the second form, your wallet address.

Do not use msg.sender as the owner

Hooks are deployed through a CREATE2 deployer contract, so msg.sender in the constructor is that deployer, not you. Take the owner as a constructor argument.

Callback signatures​

function _beforeInitialize(address, PoolKey calldata, uint160) internal returns (bytes4);
function _afterInitialize(address, PoolKey calldata, uint160, int24) internal returns (bytes4);
function _beforeAddLiquidity(address, PoolKey calldata, ModifyLiquidityParams calldata, bytes calldata) internal returns (bytes4);
function _beforeRemoveLiquidity(address, PoolKey calldata, ModifyLiquidityParams calldata, bytes calldata) internal returns (bytes4);
function _afterAddLiquidity(address, PoolKey calldata, ModifyLiquidityParams calldata, BalanceDelta, BalanceDelta, bytes calldata) internal returns (bytes4, BalanceDelta);
function _afterRemoveLiquidity(address, PoolKey calldata, ModifyLiquidityParams calldata, BalanceDelta, BalanceDelta, bytes calldata) internal returns (bytes4, BalanceDelta);
function _beforeSwap(address, PoolKey calldata, SwapParams calldata, bytes calldata) internal returns (bytes4, BeforeSwapDelta, uint24);
function _afterSwap(address, PoolKey calldata, SwapParams calldata, BalanceDelta, bytes calldata) internal returns (bytes4, int128);
function _beforeDonate(address, PoolKey calldata, uint256, uint256, bytes calldata) internal returns (bytes4);
function _afterDonate(address, PoolKey calldata, uint256, uint256, bytes calldata) internal returns (bytes4);

Good practice​

  • Keep storage per pool, keyed by PoolId (key.toId()), since one hook can serve many pools.
  • Hooks run on every swap. Keep callbacks cheap.
  • Do not let a callback revert on ordinary swaps unless blocking them is the point.
  • If the hook uses a return-delta permission, settle every amount it creates.
  • Cap anything configurable, such as a fee, in the contract itself.