Pre-launch. The protocol is not publicly live yet. This documentation describes the contracts as implemented; deployed addresses are published at launch.

MEV Protection

Fee distribution is permissionless, which means an attacker can choose when it runs. Two independent mechanisms make that unprofitable: a slippage floor on the swap, and a cap on how much can be swapped per call.

The attack being defended

Distribution performs a swap on the same pool anyone can trade. An attacker can therefore sandwich it atomically: buy, call distributeFees, sell. The protocol's swap moves price in the attacker's favour, and they capture the difference.

Slippage bounds alone do not solve this. They cap how bad a single swap can be, but an attacker who controls the timing can simply operate within the bound and repeat.

The per-call swap cap

The size of the protocol's swap is capped as a fraction of pool depth, so the price impact the attacker can capture is smaller than the fees they pay to capture it.

cap_bps  = live_fee_tier_bps * CAP_MULT_BPS / 100
max_safe = pool_native * cap_bps / (10000 - cap_bps)

The second line inverts constant-product slippage: for slippage = swap / (pool + swap) ≤ cap, the largest permissible swap is pool * cap / (1 - cap).

ConstantValueMeaning
CAP_MULT_BPS100 (1.00× the live fee tier)Cap as a multiple of the live fee tier
MAX_REINVEST_SLIPPAGE_BPS480 bps (4.8%)Minimum-output floor on the internal swap
SWAP_SLIPPAGE_BPS200 bps (2%)Floor on the SecondaryHook's $LP24 acquisition swap

Why break-even sits below zero

An atomic sandwich pays the pool fee on both legs — the buy and the sell. Setting the cap at 1.00× the live fee tier means the maximum price impact the attacker can extract is approximately one fee tier, while their round trip costs two. Measured attacker profit and loss is negative at every pool depth, including the supply-dumped floor where pools are thinnest.

The cap derives from the same live read

The obvious counter-attack is to flash-deepen the pool so it falls into a lower fee tier before triggering distribution. That fails: the cap is computed from the fee tier read in the same call, so a lower tier produces a proportionally smaller cap. Deepening the pool shrinks the attacker's own window in lockstep with the fee they hoped to avoid.

Capping never bricks distribution

When pending fees exceed max_safe, the excess is not rejected — it rolls forward. _reinvestAsLiquidity processes what it safely can and returns the remainder for the caller to re-account. Distribution therefore proceeds over several calls rather than reverting.

Only half the reinvest budget is actually swapped (the other half goes straight to liquidity and incurs no slippage), so the effective bound on the reinvest path is max_safe * 2. On Gnosis, where the reinvest share is 40%, the processed chunk is sized at max_safe * 9 / 4 so the swap stays inside the safe band.

Only require that some swap is possible

Both hooks assert max_safe > 0 — that the pool can absorb a swap — and never that the capped chunk clears an economic floor. Requiring a floor on top of the cap is what previously bricked processing on thin pools: the cap shrank the chunk below the floor, the assert failed, and pending fees could only grow.

Minimum-output floors

Independently of sizing, every internal swap carries a minimum output. On the reinvest path the expectation is derived from spot price:

expected = (half_native * sqrtP / 2^96) * sqrtP / 2^96
min_out  = expected * (10000 - MAX_REINVEST_SLIPPAGE_BPS) / 10000

The SecondaryHook's $LP24 acquisition uses a reserve-derived expectation instead, which is harder to manipulate — see Dual Pools.

Related protections

VectorDefence
Salt stolen from the mempoolThe CREATE2 salt is domain-separated by msg.sender, so a mined vanity salt is useless to anyone else
Pool pre-initialization at a corrupt priceTransient _initializing gate on both V4 hooks; atomic pool init on Gnosis
Flash-loan CTO takeoverWithdrawals locked to a block strictly after voting closed, so buy → vote → end → withdraw → repay cannot complete atomically
Post-close vote re-litigationThe outcome is frozen at end_vote(); finalization reads the stored result, not a live recount
Fee-on-fee compoundingTransient internal-swap flag exempts the hook's own swaps