BlockReversal

BlockReversal implements 2 of the fourteen Uniswap v4 callbacks: afterInitialize, beforeSwap.

drag to orbit

Uniswap v4 hook · Order flow and MEV

BlockReversal

Charges the swap that unwinds a block's own price move, which is the only leg of a sandwich that can be identified without knowing who anybody is.

Family
Order flow and MEV
Callbacks
2 of 14
Fee
dynamic
Admin keys
none
Licence
Apache-2.0

How it works

Anti-sandwich designs almost all try to identify the attacker: track an address, require a bond, tax a trader who reverses direction. Every one of them is defeated by a second EOA, which costs a bot nothing. Identity is the wrong thing to look at, because a sandwich is not a property of who is trading.

It is a property of the price path. A sandwich has a shape: within one block, the price moves away from where the block opened and then comes back. The profitable leg is always the one that comes back, because that is where the position is closed.

This hook never asks who is trading. It records where the price stood when the block opened, watches how far it has travelled since, and prices any swap that moves it back toward the opening more expensively the more of that move it is unwinding. The victim is not charged: their swap is the one that moves the price away.

Nor is the attacker's opening leg. Only the close pays, which is the leg that has something to pay with. The same charge lands on an honest trader who happens to trade against the block's direction, and that is not a flaw to apologise for.

Such a trader is buying at a price that an earlier trade in the same block improved for them, at the expense of the liquidity providers who supplied it. Returning part of that improvement to those providers is the correct answer whether the trader meant to sandwich anybody or not. The fee is an LP fee, so that is exactly where it goes.

Nothing here needs an oracle, an auction, a keeper, a bond, a private mempool, or a way to tell one address from another. It reads the pool's own tick against where the block opened, and that is all.

Prior art

Sandwich-resistant AMM designs reorder or batch within a block. Encrypted mempools (Shutter, SUAVE) hide the order. Auction-based capture (am-AMM, MEV-Share, Diamond) sells or reclaims the backrun.

Volatility-indexed dynamic fees raise the fee for everybody when the pool is moving. Pricing an individual swap by how much of the current block's own price move it is unwinding, so the closing leg pays and the victim does not, with no identity and no auction, is the contribution here.

Where it does not help

An honest trader who trades against the block's direction pays the surcharge too. That is deliberate and it is the real cost: the mechanism cannot distinguish a sandwich close from an ordinary trade in the same direction, because at the level it observes them they are the same event. It also does nothing about a sandwich split across two blocks, which is a different and much riskier attack for the attacker to run.

And a pool with a large `thresholdTicks` is protected only against sandwiches big enough to cross it, while a small one charges ordinary two-way flow more often; that trade is the pool's to make.

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,
    BlockReversalHook.Config({
        baseFee: /* uint24 */ 0,
        maxSurcharge: /* uint24 */ 0,
        thresholdTicks: /* int24 */ 0,
        halfPointTicks: /* int24 */ 0
    })
);

poolManager.initialize(key, startingSqrtPriceX96);

The pool's fee field must be LPFeeLibrary.DYNAMIC_FEE_FLAG. The hook rejects a pool initialized without it, which is the most common integration failure.

Parameters

ParameterTypeUnits
baseFeeuint24hundredths of a bip (3000 = 0.30%)
maxSurchargeuint24hundredths of a bip (3000 = 0.30%)
thresholdTicksint24ticks
halfPointTicksint24ticks

From TypeScript

npm i @hookforge/sdk

import {getHook, hookAddress, poolKeyFor} from "@hookforge/sdk";

const hook = getHook("block-reversal");
const key  = poolKeyFor({
  hook: hookAddress("block-reversal", 8453),   // Base
  currencyA: USDC, currencyB: WETH,
  tickSpacing: 60, dynamicFee: true,
});

What it reverts with

ErrorMeaning
FeeTooLarge()A surcharge cannot push the total past the protocol's own ceiling on an LP fee.
InvalidThreshold()A threshold or half point of zero would make the surcharge fire on any move at all.
NotDynamicFee()The hook was attempted to be initialized with a non-dynamic fee.
PoolAlreadyInitialized()The pool already exists, so its configuration is final.
PoolNotConfigured()The pool was initialized without a configuration for this hook.
SafeCastOverflowedIntToUint(int256)An int value doesn't fit in a uint of bits size.
SafeCastOverflowedUintDowncast(uint8,uint256)Value doesn't fit in a uint of bits size.

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.

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)"    # BlockReversal
cast call $HOOK "specURI()(string)"     # https://block-reversal.pages.dev/hook.json
cast call $HOOK "hookTags()(string[])"  # mev, anti-sandwich, dynamic-fee, oracle-free, no-admin

Build, test and deploy

git clone --recurse-submodules https://github.com/nirholas/block-reversal
cd block-reversal
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/block-reversal
cd block-reversal

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.