PairHook
EthereumPairHook is a Uniswap v4 hook on Ethereum. Brand new: its first pool went live today. Active: 249 trades in the last week, and 1 new pool this week.
- Contract name
- PairHook
How this hook works
Liquidity managerA liquidity manager hook. It manages its own liquidity, trades on its own and burns tokens.
Manages its own liquidity
It adds and removes liquidity itself, so it can own part or all of the pool.
The code that does this (unlockCallback):function unlockCallback(bytes calldata data) external onlyPoolManager returns (bytes memory) { uint8 op = abi.decode(data, (uint8)); if (op == OP_SEED) { (uint160 sqrtP, int24 tick,,) = poolManager.getSlot0(_pid()); // The price sits at or just above this grid tick, so the range below it holds PAIR only. tickUpper = _floorTick(tick); _addLiquidity(sqrtP, 0, IERC20Min(TOKEN).balanceOf(address(this))); emit Seeded(quote, sqrtP, tickUpper, liquidity); } else if (op == OP_COLLECT) { (BalanceDelta fees,) = poolManager.modifyLiquidity(_poolKey(), _position(0), ""); _takeAndPayFees(uint128(fees.amount0()), uint128(fees.amount1())); } else { _repairCallback(data); } return ""; }Trades on its own
It makes trades itself, for example to convert the fees it collected.
The code that does this (repairCallback):function _repairCallback(bytes calldata data) internal override { (, address win, address champ) = abi.decode(data, (uint8, address, address)); address oldQuote = quote; uint160 upperX = TickMath.getSqrtPriceAtTick(tickUpper); // 1. Pull the whole position out of the old pool (its LP fees are paid out on the way) and swap // the quote it held old -> new, each hop floored to the v3 TWAP. (uint256 reserveIn, uint256 tokens) = _removeAll(); uint256 amountOut = _migrateSwap(oldQuote, win, reserveIn); if (amountOut < MIN_MIGRATION) revert DustMigration(); // 2. Re-denominate the range. The pool price is PAIR per quote, so the range's upper edge (the // launch price) moves by reserveIn / amountOut, the rate the swap actually realized, and is // then floored onto the tick grid. int24 newUpper = _floorTick(TickMath.getTickAtSqrtPrice(_rescale(upperX, reserveIn, amountOut))); // 3. Price the new pool so that the position over that range holds exactly what there is to put // back: all the PAIR and all the quote the swap returned. Nothing is left over to pay away; // the grid rounding shows up as a price at most 0.6% off the realized rate, inside the 1% fee. uint160 target = _fitPrice(TickMath.getSqrtPriceAtTick(newUpper), amountOut, tokens); // 4. Point at the new pool and bring it to that price. A quote the token has worn before already // has a live slot0 (v4 cannot re-initialize), so that pool is adopted and glided instead: its // position was pulled when the pair last left it, so it is empty and the glide moves nothing. quote = win; tickUpper = newUpper; PoolKey memory key = _poolKey(); poolId = PoolId.unwrap(key.toId()); // v4 skips beforeInitialize on our own call, so set it here (uint160 existing,,,) = poolManager.getSlot0(key.toId()); if (existing == 0) { // …Burns tokens
It destroys some tokens (or sends them to an address nobody controls), so there are fewer of them.
The code that does this (bid):function bid(address asset, uint256 amount) external { // Once the epoch has run out the tally is final: a bid sent with the `repair` that reads it // would leave everyone else no block to answer in. if (block.timestamp >= epochEnd) revert EpochClosed(); if (!allowed[asset]) revert NotAllowed(); if (amount == 0) revert ZeroBid(); IPairToken(TOKEN).burnFrom(msg.sender, amount); uint256 e = epoch; uint256 total = bids[e][asset] + amount; bids[e][asset] = total; uint256 bt = bidderTotal[e][asset][msg.sender] + amount; bidderTotal[e][asset][msg.sender] = bt; if (bt > bidderTotal[e][asset][champion[e][asset]]) champion[e][asset] = msg.sender; emit Bid(e, asset, msg.sender, amount, total); }Liquidity can’t be added directly
Nobody can add liquidity to its pools through Uniswap directly. Liquidity only comes in through the hook’s own code.
Only approved callers create pools
New pools with this hook can only be created by approved callers, for example its own factory or launcher.
The code that does this (beforeInitialize):function _beforeInitialize(address sender, PoolKey calldata key, uint160) internal override returns (bytes4) { if (initialized) revert AlreadyInitialized(); if (sender != FACTORY) revert NotSeeder(); bytes32 id = PoolId.unwrap(key.toId()); if (id != PoolId.unwrap(_poolKey().toId())) revert WrongPool(); poolId = id; initialized = true; return BaseHook.beforeInitialize.selector; }Anyone can trigger payouts
Anyone can collect the fees it has built up, so nobody has to wait on the hook developer.
The code that does this (collect):function collect() external lock { if (liquidity == 0) revert NotInitialized(); poolManager.unlock(abi.encode(OP_COLLECT)); }
Who controls it: The hook developer can call: add asset, remove asset. The code cannot be changed.
Based on the hook’s published code.What do these labels mean?
What it can do
It checks every deposit before it goes in.
It runs when a pool is created, often to lock in that pool’s settings.
Hook history
- Bid 2 times
Latest transactions
- 0xeadc028a…3734b5 block 26,406,873 · 300000.00M WBTC
- 0x9d92661c…417722 block 26,406,832 · 10.00 CULT
- Added starting liquidity 1 time
Counted from what the hook recorded on-chain across all its pools, for every action in its published code, plus token transfers in and out of it.