the shape of it
One hook, attached to one FLY/ETH pool on Ethereum mainnet. It has a single job: turn a slice of swap volume into permanently destroyed FLY without anyone having to act.
The whole design is a response to one constraint. On mainnet, running a buyback on every swap would cost more than the buyback is worth. So the hook splits the work: the cheap half runs on every swap, the expensive half runs once every few dozen swaps, and the swapper unlucky enough to trigger it gets paid for the trouble.
permissions
Two of the fourteen permission flags are on. Everything else is off, including every liquidity callback — the hook has no opinion about LPs and no way to touch their positions.
function getHookPermissions() public pure
returns (Hooks.Permissions memory)
{
return Hooks.Permissions({
beforeInitialize: false,
afterInitialize: false,
beforeAddLiquidity: false,
afterAddLiquidity: false,
beforeRemoveLiquidity: false,
afterRemoveLiquidity: false,
beforeSwap: false,
afterSwap: true,
beforeDonate: false,
afterDonate: false,
beforeSwapReturnDelta: false,
afterSwapReturnDelta: true,
afterAddLiquidityReturnDelta: false,
afterRemoveLiquidityReturnDelta: false
});
}v4 encodes those flags in the low bits of the hook’s own address, so the address has to be mined with CREATE2 until it carries exactly AFTER_SWAP_FLAG | AFTER_SWAP_RETURNS_DELTA_FLAG and nothing else.
That is the useful part: the permission set is verifiable from the address alone, before anyone reads a line of source. An address that cannot call beforeRemoveLiquidity cannot grief withdrawals, whatever the code says.
accrual
The fee is taken on the unspecified currency — the side of the swap the trader did not pin down — so it is charged against the amount actually delivered rather than the amount requested. Partial fills are not overcharged.
function _afterSwap(
address, PoolKey calldata key,
SwapParams calldata params,
BalanceDelta delta, bytes calldata
) internal override returns (bytes4, int128) {
// the currency the trader did not specify
bool exactIn = params.amountSpecified < 0;
bool take0 = params.zeroForOne != exactIn;
Currency c = take0 ? key.currency0 : key.currency1;
int128 out = take0 ? delta.amount0() : delta.amount1();
uint128 fee = uint128(out) * FEE_BPS / 10_000;
// claim tokens, not an ERC-20 transfer
poolManager.mint(address(this), c.toId(), fee);
treasury += fee;
if (treasury >= THRESHOLD) _fire(key, c);
return (BaseHook.afterSwap.selector, int128(fee));
}The important line is mint, not take. Taking would move real ERC-20s out of the singleton on every single swap. Minting credits the hook with ERC-6909 claim tokens inside the PoolManager instead — a balance write, no token contract touched, no transfer event. That is the only reason a per-swap fee is affordable here at all.
Returning the fee as a positive int128 is what charges it to the swapper. The hook does not conjure value; it moves a documented slice of the output.
the crossing
_fire runs only when the accumulated treasury reaches the threshold. At a 0.35 ETH threshold and 12 bps, that is roughly one swap in fifty on a pool doing normal volume — the other forty-nine pay a single storage write and leave.
The threshold is denominated in the treasury currency and fixed at deployment. It does not float, it is not governed, and nothing about the crossing depends on who is swapping or how large their swap is. The only thing that decides is the running total.
the buyback
afterSwap is already executing inside the PoolManager’s unlock context, so the hook does not need to unlock anything or route through a callback. It burns its claim tokens back into a real balance and swaps them for FLY.
function _fire(PoolKey calldata key, Currency c) internal {
uint128 spend = treasury;
treasury = 0;
poolManager.burn(address(this), c.toId(), spend);
BalanceDelta d = poolManager.swap(key, SwapParams({
zeroForOne: ethIsCurrency0,
amountSpecified: -int256(uint256(spend)),
sqrtPriceLimitX96: NO_LIMIT
}), "");
// ... burn + rebate, below
}Open question — 1 of 2This calls swap on the same pool from inside that pool’s own afterSwap. v4’s unlock model is transient-storage based rather than a per-pool reentrancy lock, so the call is expected to succeed — but it re-enters afterSwap, which would accrue a fee on the buyback itself and recurse. The hook must short-circuit on its own sender. Whether that guard is sufficient against the deployed v4-core version is the single thing most worth auditing here, and the fallback design is to defer the buyback to the next swap instead of nesting it.
the burn
FLY bought by the hook is sent to 0x…dEaD and never comes back. The hook holds no FLY between transactions: what it buys in a block, it destroys in the same block, minus the rebate.
There is no vesting, no treasury balance to point at, and no reserve to argue about. The only observable state the hook carries between transactions is the running fee total, and that is denominated in the pool’s other currency.
the cranker rebate
The swapper whose transaction crossed the line pays real gas for the buyback and the burn. The rebate is a fixed fraction of the FLY just bought, transferred to msg.sender before the remainder is destroyed.
Sizing it is a balance between two failures:
- Too small, and at high base fees the crossing swap is a loss. Traders route around the pool when the treasury looks nearly full, and the burn stalls.
- Too large, and the rebate is worth farming. Someone splits swaps to land on the crossing deliberately, and a share of every burn leaks to a bot.
Open question — 2 of 2A fixed fraction is the wrong shape: gas is priced in ETH and volatile, the rebate is priced in FLY and volatile, and the two are uncorrelated. A rebate sized from block.basefee and the buyback’s realised price would track the actual cost, at the price of reading two more values on the hot path. This is not settled.
What is settled: no registration, no whitelist, no allowlist of crankers. There is nothing to sign up for and no privileged lane. The crossing is unpredictable from outside because it depends on the exact output of every swap before yours.
parameters
Fixed at deployment. None of these are governed, and the hook exposes no setter for any of them.
| Name | Value | Note |
|---|
| FEE_BPS | 12 | Basis points of the unspecified output routed to the treasury. |
| THRESHOLD | 0.35 ETH | Treasury level that triggers the buyback. Provisional. |
| REBATE_BPS | TBD | Share of the bought FLY paid to the cranker. See open question 2. |
| BURN_SINK | 0x…dEaD | Destination for the remainder. No key exists for it. |
| POOL | FLY / ETH | One pool. The hook rejects initialisation on any other key. |
gas
These are design targets, not measurements — nothing has been benchmarked because nothing has been written yet.
| Path | Target | What it covers |
|---|
| ordinary swap | ~9k | Hook dispatch, one 6909 mint, one warm storage write. |
| crossing swap | ~140k extra | 6909 burn, nested swap, two FLY transfers. |
If the ordinary path turns out to cost meaningfully more than that, the design does not work and the honest response is to say so rather than raise the threshold until the numbers hide.
failure modes
The buyback reverts
A nested swap that reverts takes the user’s swap with it, which is unacceptable: an ordinary trader would lose a transaction to a mechanism they did not opt into. The buyback must be wrapped so that a failure leaves the treasury untouched and lets the original swap settle normally. A failed crossing retries on the next swap.
The pool is sandwiched around the crossing
The buyback is a market order of known size at a knowable moment, which is exactly what a searcher wants. It can be front-run. The mitigations are all imperfect: a price limit caps the damage but can cancel the buyback, and splitting the buyback across blocks breaks the same-transaction property that the whole thing is built on. Treated as a known cost rather than a solved problem.
Volume goes to zero
Nothing accrues, nothing crosses, nothing burns. The hook does not degrade — it simply stops. There is no timer, no minimum, and no fallback that fires on an empty pool.
Someone sends FLY or ETH to the hook
It stays there. The hook has no sweep function, which is deliberate: a sweep is an address with a claim on the contract’s balance, and the point is that no such address exists.
deployment
The hook address is mined until its low bits match the permission set, then deployed with CREATE2 from a known factory so the address is reproducible from the salt and the init code. Verification is the usual: match the deployed bytecode against a public source, and check the flag bits in the address by hand.
Deployment is the last privileged action. There is no proxy, no admin, no initialiser that can be called twice, and no upgrade path. Changing anything means deploying a different hook to a different address and convincing liquidity to move — which is the only migration mechanism that does not require trusting anyone.
what this is not
- Not deployed. There is no hook address, no pool, and no token contract to inspect. Everything above is design.
- Not audited. Nothing has been reviewed, because nothing has been written.
- Not a yield mechanism. The burn reduces supply. It does not pay anyone, does not accrue to holders, and makes no claim about price.
- Not novel cryptography. It is a fee, a counter, and a market buy. The only interesting part is that the expensive step rides along with a swap that was going to happen anyway.
Corrections and holes in the above are worth more than agreement: @flyuniv4.