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
| Setting | Value |
|---|---|
| Compiler | solc 0.8.26 |
| EVM version | cancun |
| Optimizer | on, 200 runs |
| IR pipeline | used automatically if the default pipeline reports "stack too deep" |
The hook must be a single file.
Available imports
| Import prefix | Library |
|---|---|
@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
BaseHookfrom@uniswap/v4-periphery/src/utils/BaseHook.sol. - Override the internal callbacks (
_beforeSwap,_afterSwapand so on), not the external ones.BaseHookalready restricts them to the PoolManager. SwapParamsandModifyLiquidityParamscome from@uniswap/v4-core/src/types/PoolOperation.sol.getHookPermissions()must returntruefor 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.