Hooks
Write a hook
Build a Uniswap v4 hook on BaseHook, mine its address, test it against PoolManager, and propose it for Socket's allowlist.
A hook is a Solidity contract that PoolManager calls at the points its address allows. Any pool can use it once it is deployed. For Socket's app to route through it, SOCKET holders vote it onto the allowlist.
Set up#
BaseHook and HookMiner live in Uniswap's v4-hooks-public repository, which brings v4-core and v4-periphery with it. With Foundry:
forge install Uniswap/v4-hooks-public@uniswap/v4-hooks-public/=lib/v4-hooks-public/
@uniswap/v4-core/=lib/v4-hooks-public/lib/v4-core/
@uniswap/v4-periphery/=lib/v4-hooks-public/lib/v4-periphery/
forge-std/=lib/v4-hooks-public/lib/forge-std/src/
solmate/=lib/v4-hooks-public/lib/v4-core/lib/solmate/[profile.default]
solc = "0.8.26"
evm_version = "cancun"PoolManager uses transient storage, so tests that deploy it need the Cancun EVM.
A minimal hook#
This hook charges 1% on large swaps and 0.30% on the rest. It needs beforeInitialize, to accept only dynamic-fee pools, and beforeSwap, to set each swap's fee.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {BaseHook} from "@uniswap/v4-hooks-public/src/base/BaseHook.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";
import {LPFeeLibrary} from "@uniswap/v4-core/src/libraries/LPFeeLibrary.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol";
contract SizeFeeHook is BaseHook {
using LPFeeLibrary for uint24;
uint24 internal constant BASE_FEE = 3_000; // 0.30%
uint24 internal constant LARGE_FEE = 10_000; // 1%
uint256 internal constant LARGE = 1e21;
error NotDynamicFee();
constructor(IPoolManager manager) BaseHook(manager) {}
function getHookPermissions() public pure override returns (Hooks.Permissions memory) {
return Hooks.Permissions({
beforeInitialize: true,
afterInitialize: false,
beforeAddLiquidity: false,
afterAddLiquidity: false,
beforeRemoveLiquidity: false,
afterRemoveLiquidity: false,
beforeSwap: true,
afterSwap: false,
beforeDonate: false,
afterDonate: false,
beforeSwapReturnDelta: false,
afterSwapReturnDelta: false,
afterAddLiquidityReturnDelta: false,
afterRemoveLiquidityReturnDelta: false
});
}
// Only a dynamic-fee pool lets beforeSwap set the fee.
function _beforeInitialize(address, PoolKey calldata key, uint160) internal pure override returns (bytes4) {
if (!key.fee.isDynamicFee()) revert NotDynamicFee();
return BaseHook.beforeInitialize.selector;
}
function _beforeSwap(address, PoolKey calldata, SwapParams calldata params, bytes calldata)
internal
pure
override
returns (bytes4, BeforeSwapDelta, uint24)
{
int256 amount = params.amountSpecified;
uint256 size = uint256(amount < 0 ? -amount : amount);
uint24 fee = size >= LARGE ? LARGE_FEE : BASE_FEE;
return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, fee | LPFeeLibrary.OVERRIDE_FEE_FLAG);
}
}Rules your hook follows#
Declare exactly its permissions
getHookPermissionslists all 14 flags. BaseHook's constructor checks them against the contract's own address and reverts withHookAddressNotValidif any differs.Override the internal callbacks
BaseHook's external callbacks accept calls only from PoolManager and forward them to internal ones:
_beforeSwap,_afterAddLiquidityand so on. Override the internal callback for every flag you set. One you leave out reverts withHookNotImplementedon every call.Return the selector
Every callback returns the selector of the function PoolManager called. A wrong one reverts the operation with
InvalidHookResponse.Settle what you take
With a returns-delta flag, the delta you return is owed to or by your hook. Settle it in the same
unlock, or the whole transaction reverts.Refuse with a revert
To reject an operation, revert. The whole operation reverts with it, and nothing moves.
Mine the address#
PoolManager reads the hook's permissions from the lowest 14 bits of its address, so the hook has to be deployed where those bits match. HookMiner.find searches CREATE2 salts until the address carries your flags:
uint160 flags = uint160(Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG);
(address hookAddress, bytes32 salt) =
HookMiner.find(deployer, flags, type(SizeFeeHook).creationCode, abi.encode(poolManager));
SizeFeeHook hook = new SizeFeeHook{salt: salt}(poolManager);
require(address(hook) == hookAddress);deployer is the address that executes CREATE2, and the constructor arguments must be the ones you deploy with. On Robinhood Chain, poolManager is 0x8366a39cc670b4001a1121b8f6a443a643e40951.
Test against PoolManager#
v4-core's Deployers deploys a fresh PoolManager, test routers and two tokens. Mine against the test contract, which is the deployer of new in a test:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol";
import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
import {LPFeeLibrary} from "@uniswap/v4-core/src/libraries/LPFeeLibrary.sol";
import {HookMiner} from "@uniswap/v4-hooks-public/src/utils/HookMiner.sol";
import {SizeFeeHook} from "../src/SizeFeeHook.sol";
contract SizeFeeHookTest is Deployers {
SizeFeeHook hook;
function setUp() public {
deployFreshManagerAndRouters();
deployMintAndApprove2Currencies();
uint160 flags = uint160(Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG);
(address expected, bytes32 salt) =
HookMiner.find(address(this), flags, type(SizeFeeHook).creationCode, abi.encode(manager));
hook = new SizeFeeHook{salt: salt}(manager);
assertEq(address(hook), expected);
(key,) = initPoolAndAddLiquidity(
currency0, currency1, IHooks(address(hook)), LPFeeLibrary.DYNAMIC_FEE_FLAG, SQRT_PRICE_1_1
);
}
function test_swap() public {
swap(key, true, -1e15, ZERO_BYTES);
}
function test_refusesStaticFeePool() public {
vm.expectRevert();
initPool(currency0, currency1, IHooks(address(hook)), 3_000, SQRT_PRICE_1_1);
}
}Test every flag you set, both swap directions, exact input and exact output, and every path that reverts.
Get it listed#
Socket's built-in hooks are on the allowlist from the start. Any other hook is added by a vote of SOCKET holders. The author proposes it with:
- its verified source on the explorer, Blockscout;
- its address and flags;
- whoever can change it (an owner, a proxy admin), if anyone;
- an independent review.
Proposing locks 1,000,000 SOCKET from the author while the hook is listed.
| Outcome | The stake |
|---|---|
| The hook is listed | Stays locked while the hook is listed. |
| The proposal fails | Can be withdrawn 7 days after the proposal. |
| The hook is removed | Can be withdrawn 7 days after the removal. |
| The deployed code doesn't match the published source | A removal vote can burn it. |
The vote runs like every other: see Vote with SOCKET. Once listed, the app routes through pools on the hook and shows it as External hook in mustard.