Hooks / Liquidity provider economics
LiquidityFloor
A liquidity commitment enforced per position: a fraction of what you add cannot leave until the pool's unlock date, and every provider is held to that fraction of their own stake rather than to a shared pool total.
LiquidityFloor implements 3 of the fourteen Uniswap v4 callbacks:
afterInitialize, afterAddLiquidity, beforeRemoveLiquidity.
How it works
A new pool has a bootstrapping problem that is really a credibility problem. Swappers cannot tell the difference between liquidity that intends to stay and liquidity that will leave the moment the pool is quoted by an aggregator, and the second kind is indistinguishable from the first right up until it is gone. The published answers lock whole positions for a term.
That works and it is also badly mispriced: a provider who would happily commit a third of their capital for six months has to choose between committing all of it and committing none. Term locks therefore select for the providers least sensitive to the lock, which is not the same set as the providers most useful to the pool. This hook takes the commitment as a *fraction*.
Configure `floorBps` and `unlockTimestamp`, and thereafter each position may freely withdraw down to `floorBps` of everything it has ever added, with the remainder released at the unlock. Adding more liquidity raises your own floor proportionally, so topping up is never a trap: you keep the same ratio of free to committed capital that you signed up for. The design is deliberately race-free, which is the property that distinguishes it from a pool-wide minimum.
A floor expressed as "total pool liquidity must stay above X" is a bank run waiting to happen: it is satisfiable by whoever withdraws first and binding only on whoever is last, so rational providers race for the exit precisely when the pool most needs them. Holding each position to its own commitment removes the race entirely. Nothing another provider does can change what you are allowed to withdraw.
The hook takes no fee, holds no funds and has no privileged role. It cannot stop a provider from ceasing to quote, only from removing committed liquidity, and it does not restrict swaps at all. Position identity is the v4 position key: the address that called `modifyLiquidity` on the `PoolManager` (in practice a position manager or a router), the tick range, and the caller's salt.
Two providers sharing one position manager therefore share a commitment only if they also share a salt, which position managers do not do. Prior art: `LiquidityLock`, `Timelock Addition` and `LockingLiquidity` all lock positions wholesale for a term. Fractional, per-position, top-up-safe commitments are the contribution here.
Prior art
LiquidityLock, Timelock Addition and LockingLiquidity all lock positions wholesale for a term. Fractional, per-position, top-up-safe commitments are the contribution here, along with the observation that a pool-wide minimum is a bank run rather than a floor.
Where it does not help
The commitment binds the v4 position key, which is the address that called modifyLiquidity. A provider who routes through a position manager that pools many users under one key would share a commitment with them; every mainstream position manager gives each position its own key, but a custom router need not.
Configuring a pool
Uniswap v4 removed hookData from initialize, so a hook that needs per-pool parameters has to
receive them out of band. LiquidityFloor takes them through configure, which anyone may call for a pool
key whose pool does not exist yet, and which nobody may call afterwards. The parameters are part of what the pool
is, so they are fixed for its lifetime.
// 1. Fix the terms, before the pool exists.
hook.configure(
key,
LiquidityFloorHook.Config({
floorBps: /* uint16 */ 0,
unlockTimestamp: /* uint64 */ 0
})
);
// 2. Initialize the pool. The hook rejects a pool it was never configured for.
poolManager.initialize(key, startingSqrtPriceX96);
| Parameter | Type | Units |
|---|---|---|
floorBps | uint16 | basis points (10000 = 100%) |
unlockTimestamp | uint64 | unix seconds |
From TypeScript
The SDK ships the catalogue, the address book and the pool-key helpers, so a client never hardcodes an address or recomputes a pool id by hand.
npm i @hookforge/sdk
import {getHook, hookAddress, poolKeyFor, poolId} from "@hookforge/sdk";
const hook = getHook("liquidity-floor");
const address = hookAddress("liquidity-floor", 1);
const key = poolKeyFor({hook: address, currencyA: USDC, currencyB: WETH, tickSpacing: 60});
console.log(poolId(key));
What it reverts with
| Error | Meaning |
|---|---|
CommitmentBreached(uint256,uint256) | The withdrawal would take the position below its commitment. |
InvalidFloor() | `floorBps` was zero (no commitment) or above 100%. |
PoolAlreadyInitialized() | The pool already exists, so its configuration is final. |
PoolNotConfigured() | The pool was initialized without a configuration for this hook. |
UnlockInThePast() | The unlock must be in the future at configuration time, or the commitment means nothing. |
Addresses
| Chain | Chain ID | Address |
|---|---|---|
| unichain | 130 | 0xFeCAf8bb3DD7fBE0Df1b6eFa0E865ef598535600 |
| robinhood | 4663 | 0x3041fD311d0f39428655f45ce4501DEC7BfAD600 |
| base | 8453 | 0x375ae2136a29aaA49E5f17c2Fc2A6A4832385600 |
| arbitrum | 42161 | 0x1b142F790e2FfEb875a36FAED37Bbf8cE1Cf9600 |
These addresses are deterministic, not live. They are the CREATE2 addresses the deploy script mines so that the low fourteen bits encode this hook's permissions. Until the deploy runs on a chain, there is no code at them. Check before you send anything anywhere.
Source and verification
The contract is contracts/src/hooks/LiquidityFloorHook.sol,
and everything on this page is generated from it: the prose is its NatSpec, the parameters are its
configure ABI, the callbacks above are the flags it declares, and the tags are the strings its own
hookTags() returns. A hook cannot be documented here as something it is not.
Ask the deployed contract what it is and it answers directly, with no registry in the loop:
cast call $HOOK "hookName()(string)" # LiquidityFloor
cast call $HOOK "specURI()(string)" # https://hookforge.pages.dev/schema/hooks/liquidity-floor.json
cast call $HOOK "hookTags()(string[])" # lp-economics, commitment, launch, rug-resistance