CommitRevealDark implements 2 of the fourteen Uniswap v4 callbacks: afterInitialize, beforeSwap.
drag to orbit
Uniswap v4 hook · Order flow and MEV
CommitRevealDark
Every swap must have been committed to in an earlier block, so nobody can react to an order they can see.
- Family
- Order flow and MEV
- Callbacks
- 2 of 14
- Fee
- static
- Admin keys
- none
- Licence
- Apache-2.0
How it works
Sandwiching works because of a timing asymmetry, not because of secrecy. Your order is visible before it executes, and an attacker can construct and place their own transactions after seeing it. Every defence that hides the order attacks the wrong half of that: encrypted mempools, private relays and threshold schemes all try to stop the attacker seeing, which is hard, needs new infrastructure, and fails the moment one relay defects.
This attacks the other half. If the pool accepts only swaps that were committed to at least one block earlier, then an attacker who sees your order cannot act on it in the pool, because their own reaction would need a commitment made before they knew there was anything to react to. Seeing the order stops being useful.
Nothing has to be hidden, and nothing has to be trusted. A commitment is a hash of the swap's parameters and a salt, so it reveals nothing until it is used. Revealing means simply making the swap: the hook recomputes the hash from what actually arrived and requires it to match a commitment that is old enough, unused and unexpired.
A commitment that does not match is not a swap this pool will accept. Commitments carry a bond, and this is the part that makes the scheme cost something to abuse. Without it, a trader would commit to every order they might want at every size, then reveal the one they liked, which is a free option on the pool paid for by nobody.
The bond is returned in full when the commitment is used and forfeited to the pool when it expires unused, so keeping options open is exactly as expensive as the options are worth to you. The cost of all this is two transactions and a delay of at least one block, and that is the honest trade. A pool wearing this hook is not for the impatient; it is for flow large enough that being sandwiched costs more than waiting a block.
Prior art
Commit-reveal is old, and appears on-chain in ENS registration, in sealed-bid auctions and in the sandwich-resistant AMM designs that reorder within a block. Encrypted mempools (Shutter, SUAVE, threshold schemes) attack the same problem by hiding the order. Requiring every swap in a pool to have been committed in an earlier block, so that seeing an order is useless rather than impossible, is the contribution here.
Where it does not help
Two transactions and a block of delay on every trade, which rules the pool out for anything latency-sensitive and makes it unusable through routers that will not forward the salt. It does not stop an attacker who commits speculatively every block and reveals when they see something worth reacting to: the bond makes that expensive rather than impossible, and a pool that sets the bond too low is not protected, it is inconvenient. Finally, a reveal carries its salt in the clear, so an observer can copy it and execute your trade ahead of you.
That is a denial rather than a theft, since the copier pays for the swap and the bond still returns to whoever committed, but a trader who is being griefed this way has no recourse beyond committing again.
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,
CommitRevealDarkHook.Config({
delayBlocks: /* uint32 */ 0,
ttlBlocks: /* uint32 */ 0,
bond: /* uint128 */ 0
})
);
poolManager.initialize(key, startingSqrtPriceX96);
Parameters
| Parameter | Type | Units |
|---|---|---|
delayBlocks | uint32 | |
ttlBlocks | uint32 | |
bond | uint128 |
From TypeScript
npm i @hookforge/sdk
import {getHook, hookAddress, poolKeyFor} from "@hookforge/sdk";
const hook = getHook("commit-reveal-dark");
const key = poolKeyFor({
hook: hookAddress("commit-reveal-dark", 8453), // Base
currencyA: USDC, currencyB: WETH,
tickSpacing: 60,
});
What it reverts with
| Error | Meaning |
|---|---|
AlreadyCommitted() | This commitment already exists. Vary the salt. |
Expired(uint256) | The commitment has expired and its bond is forfeited. |
InvalidDelay() | A delay of zero would let somebody commit and reveal in the same block, which is no protection at all. |
InvalidTtl() | The time to live must exceed the delay, or a commitment expires before it may be used. |
NotCommitted(bytes32) | The swap does not match any standing commitment. |
NotExpiredYet(uint256) | The commitment has not expired, so its bond cannot be swept. |
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. |
TooSoon(uint256) | The commitment is not old enough to be revealed yet. |
WrongBond(uint256) | The bond sent does not match what the pool requires. |
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)" # CommitRevealDark
cast call $HOOK "specURI()(string)" # https://commit-reveal-dark.pages.dev/hook.json
cast call $HOOK "hookTags()(string[])" # mev, anti-sandwich, commit-reveal, order-flow, no-admin
Build, test and deploy
git clone --recurse-submodules https://github.com/nirholas/commit-reveal-dark
cd commit-reveal-dark
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/commit-reveal-dark
cd commit-reveal-dark
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.