TopOfBlockAuction implements 2 of the fourteen Uniswap v4 callbacks: afterInitialize, beforeSwap.
drag to orbit
Uniswap v4 hook · Order flow and MEV
TopOfBlockAuction
Sells the right to trade first in a block, and gives the proceeds to the liquidity providers who are being traded against.
- Family
- Order flow and MEV
- Callbacks
- 2 of 14
- Fee
- static
- Admin keys
- none
- Licence
- Apache-2.0
How it works
The first swap in a block against a pool whose price has moved overnight is worth money, and today that money goes to whoever wins the gas auction or pays the builder. The liquidity providers, whose stale quote is the entire source of the value, receive the ordinary swap fee and nothing else. This is the largest single transfer away from passive liquidity in the system, and it happens in a market the pool cannot see and does not participate in.
The idea of auctioning that right and paying the pool instead is not new: it is the am-AMM proposal, and MEV-Share and MEV-Boost redistribute adjacent value off-chain. What has been missing is a version the pool runs itself, per block, with no auctioneer, no off-chain infrastructure and nobody to trust with the proceeds. This is that.
A searcher bids native currency for a named future block. The highest bid at the time the block arrives wins the exclusive right to the first swap in it, and the bid is paid to the pool's liquidity providers. Everybody else can still trade in that block, just not first.
Once the winner has taken their slot, or if nobody bid, the pool is ordinary. Bidding on a future block rather than the current one is what makes this work without an auctioneer. A bid for the block being built cannot be evaluated inside that block without knowing bids that have not arrived, so the auction would have to be settled by somebody.
Bidding one block ahead means the winner is already known when the block opens, and the contract simply checks who it is. A losing bid is never taken. Bids are held and the loser withdraws in full, so bidding costs nothing but gas, and a searcher who is outbid is not out of pocket.
Prior art
The mechanism is the am-AMM auction (Adams, Moallemi, Reynolds, Robinson), which auctions pool management rights over a longer horizon, and the broader family of order-flow auctions run off-chain by builders and relays. Running the auction per block, inside the pool, with the winner known before the block opens so no auctioneer is needed, is the contribution here.
Where it does not help
The pool cannot see whether the winner actually used their slot well, only that they took it, so a searcher who wins and does nothing simply wastes their bid and blocks nobody. More importantly, a builder can still reorder the block: this sells the first slot in the pool's own accounting, not the first position in the block, so a searcher who controls ordering can place their own transaction before the winner's and the winner gets a slot that is no longer first. It raises the cost of that rather than preventing it.
Using it
Uniswap v4 removed hookData from initialize, so per-pool parameters arrive out of band.
Fix them for a pool key whose pool does not exist yet, then initialize. Nobody can change them afterwards,
including you.
hook.configure(
key,
TopOfBlockAuctionHook.Config({
leadBlocks: /* uint32 */ 0,
minBid: /* uint128 */ 0
})
);
poolManager.initialize(key, startingSqrtPriceX96);
Parameters
| Parameter | Type | Units |
|---|---|---|
leadBlocks | uint32 | |
minBid | uint128 |
From TypeScript
npm i @hookforge/sdk
import {getHook, hookAddress, poolKeyFor} from "@hookforge/sdk";
const hook = getHook("top-of-block-auction");
const key = poolKeyFor({
hook: hookAddress("top-of-block-auction", 8453), // Base
currencyA: USDC, currencyB: WETH,
tickSpacing: 60,
});
What it reverts with
| Error | Meaning |
|---|---|
BidTooLow(uint256) | The bid does not beat the standing one. |
BlockTooSoon(uint256) | The bid targets a block that is not far enough ahead, or has already passed. |
InvalidLead() | leadBlocks of zero would ask the pool to settle an auction inside the block it is bidding on. |
NothingToWithdraw() | There is nothing to withdraw. |
PoolAlreadyInitialized() | The pool already exists, so its configuration is final. |
PoolNotConfigured() | The pool was initialized without a configuration for this hook. |
SlotBelongsTo(address) | Somebody else holds the first slot in this block. |
The callbacks it claims
Uniswap v4 reads a hook's permissions from the low fourteen bits of its own address, which is why deploying one
means mining a CREATE2 salt. This hook claims 2, so every deployment of it has an address ending
in 0x1080.
- beforeInitialize
- afterInitialize
- beforeAddLiquidity
- afterAddLiquidity
- beforeRemoveLiquidity
- afterRemoveLiquidity
- beforeSwap
- afterSwap
- beforeDonate
- afterDonate
- beforeSwapReturnsDelta
- afterSwapReturnsDelta
- afterAddLiquidityReturnsDelta
- afterRemoveLiquidityReturnsDelta
It says what it is, on-chain
Nothing about a hook's address tells an indexer, a wallet, a router or an agent what the pool does, which is why
hook discovery today is a curated list. This hook answers for itself, in one eth_call, with no
registry in the loop.
cast call $HOOK "hookName()(string)" # TopOfBlockAuction
cast call $HOOK "specURI()(string)" # https://top-of-block-auction.pages.dev/hook.json
cast call $HOOK "hookTags()(string[])" # mev, auction, order-flow, lp-economics, no-admin
Build, test and deploy
git clone --recurse-submodules https://github.com/nirholas/top-of-block-auction
cd top-of-block-auction
forge build && forge test
# Dry run: mines the salt, prints the address, sends nothing.
forge script script/Deploy.s.sol --rpc-url $RPC_URL
# For real.
forge script script/Deploy.s.sol --rpc-url $RPC_URL --broadcast --verify
Status
Unaudited. Built to an audited shape, on OpenZeppelin's audited hook bases, and tested against
a real PoolManager. No third party has reviewed it. Read "where it does not help" above before
putting money behind it. Not affiliated with Uniswap Labs.
Try it
This is the hook running, not a picture of it. Connect a wallet on a chain it is deployed to, or bring the whole stack up locally in one command and use it with no funds and no wallet risk at all.
Loading the demo… if this does not change, JavaScript is blocked and the demo cannot run.
Run the whole thing locally
git clone --recurse-submodules https://github.com/nirholas/top-of-block-auction
cd top-of-block-auction
anvil &
forge script script/DeployLocal.s.sol --rpc-url http://127.0.0.1:8545 --broadcast \
--private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
node web/build.mjs && npx serve web/dist
The deploy script writes web/local.json itself and the build merges it, so the page points at the
chain you just created without you editing anything. Point a wallet at
http://127.0.0.1:8545 and every button on this page works.
Anvil's first account is pre-funded and its key is public by design. Never use it anywhere real.