MEV-resistant commit-reveal AMM — front-run nothing, swap anything.
A minimal constant-product AMM where swap intent is hidden from block builders until the moment of execution. Searchers see an opaque hash on-chain. By the time they know the trade params, it's already settled.
bytes32 secret = keccak256(abi.encode(myAddress, block.timestamp)); // keep private
bytes32 hash = pool.buildHash(tokenIn, amountIn, minAmountOut, deadline, secret);
pool.commit(hash);The AMM stores only hash → {trader, commitBlock}. Nothing about the swap params leaks on-chain.
pool.reveal(tokenIn, amountIn, minAmountOut, deadline, secret);The contract re-hashes the plaintext, verifies it matches the stored commitment, checks the reveal window, and executes the constant-product swap atomically. Tokens are pulled and sent in the same transaction.
| Constant | Value | Purpose |
|---|---|---|
REVEAL_DELAY |
1 block | Must wait at least one block — prevents same-block sandwich |
REVEAL_DEADLINE |
256 blocks | Commitment expires if not revealed — prevents stale commitments |
- No lookahead: block builders see
commit(hash)buttokenIn,amountIn, andminAmountOutare hidden inside the hash until reveal. - No same-block sandwich:
REVEAL_DELAYforces at least one block between commit and reveal. A searcher cannot insert a front-run in the same block as the commit. - No front-running on reveal: the swap executes in the same tx as the reveal — there is no gap to exploit between approval and execution.
- One-time use: each commitment hash can only be revealed once (
I-5).
| ID | Invariant |
|---|---|
| I-1 | k_after >= k_before — fees only increase the pool |
| I-2 | totalShares > 0 → reserve0 > 0 && reserve1 > 0 |
| I-3 | shares[addr] / totalShares == addr's proportional claim |
| I-4 | MINIMUM_LIQUIDITY permanently locked to address(1) on first deposit |
| I-5 | Each commitment hash is consumed exactly once |
| Function | Phase | Description |
|---|---|---|
addLiquidity(amount0, amount1) |
LP | Deposit tokens, receive LP shares |
removeLiquidity(shares) |
LP | Burn shares, receive proportional tokens |
commit(hash) |
Swap Phase 1 | Register intent — hash only, no tokens locked |
reveal(tokenIn, amountIn, minOut, deadline, secret) |
Swap Phase 2 | Verify + execute atomically |
getAmountOut(tokenIn, amountIn) |
View | Quote for UI |
buildHash(...) |
View | Helper to compute commitment hash off-chain |
git clone https://github.com/0xan0nxyz/dark-pool
cd dark-pool
forge install
forge test17 tests — 0 failed
Unit: addLiquidity, removeLiquidity, commit, reveal (happy path + all 7 revert cases)
Fuzz: k-invariant across 256 random swap sizes, proportional LP removal
- Latency: Users must wait
REVEAL_DELAYblocks. On Ethereum (~12s/block) this is ~12 seconds minimum. - Commitment expiry: Unrevealed commitments expire after 256 blocks (~51 min). No tokens are locked so there is nothing to refund.
- Fixed input: Each commitment is for an exact
amountIn. Slippage protection is viaminAmountOutset at commit time. - No flash loans: The pool does not lend reserves intra-transaction.
MIT