How the machine works

One hook, one pot per pool, two mechanics, one optional LP program. Everything below is the verified on-chain behaviour — no trust assumptions, no privileged actors.

01 · concepts

MAIN, SECONDARY, and the pot

Every hooked pool declares two roles for its two currencies. MAIN is the asset being defended — it is what the pot buys and what the pot's recipient receives. SECONDARY is the buyback currency — the ONLY asset the pot holds and the only asset donate() accepts.

The pot is a per-pool, permissionless war chest. Anyone can fuel it — the token team, a protocol treasury, a community member, another contract. It spends itself automatically through two mechanics: the pump on buys and the shield on sells.

The pot's recipient decides where bought MAIN goes: address(0) means burn (the cascade below), anything else receives it directly.

the burn cascade (recipient = 0x0)
try token.burn()
→ transfer to 0xdEaD· if no burn
→ held by the hook forever· if both fail — still out of supply

native-main pools cannot set burn intent — you can't burn the network token. failed recipient deliveries are parked and retryable with flushDirect(poolId).

02 · attack

The pump — why it can't be sandwiched

On a SECONDARY → MAIN buy, afterSwap makes the pot buy more MAIN inside the buyer's own transaction. The spend is sized from the carrying buy:

// the slice a buy unlocks unlocked = min(pot, buyer's input) // the 80% haircut keeps the spend strictly // inside the sandwich break-even spend    = unlocked · 80%

A sandwich needs the attacker's round trip to profit from the pump's price push. But forcing a bigger pump requires a bigger real buy, which costs the attacker fee + impact twice — and the haircut means the pump moves the price by LESS than the attacker paid to trigger it. The attack is self-financing a donation to the pool.

a buy, step by step
user swaps SECONDARY → MAIN
pool executes the user's swap· user pays the pool fee
afterSwap: pot spends ≤ 80% of unlocked slice
pot's MAIN → recipient (or burn cascade)

note: zero-fee pools never pump by design — a fee-less pool makes the round trip free, which would break the sandwich arithmetic above.

03 · defense

The shield — sells that don't move the price

On a MAIN → SECONDARY sell, beforeSwap lets the pot absorb the sell at the pool's EXACT execution price — fee and tick impact included. The seller receives precisely what the pool would have paid; the pool's price simply does not move. The absorbed MAIN goes to the recipient instead of the pool.

Because the shield never pays ABOVE the pool's own price, selling into the pot is never better than selling into the pool. Moving spot first buys an attacker nothing, and every round trip still pays the pool's fee and impact twice.

When the pot can't cover the whole sell, it absorbs the slice it can afford and the remainder swaps through the pool in the same call — partial defense, zero seller friction.

a sell, step by step
user swaps MAIN → SECONDARY
beforeSwap: pot quotes the pool's exact price
pot pays the seller, takes the MAIN
uncovered remainder swaps through the pool· only if the pot ran out
04 · the waterfall

Harvest split, compound and carry

The pool's LP program (one per pool, owned) accrues swap fees on both sides. A harvest — automatic once fees pass the minimums, or manual — splits each side FROM THE GROSS:

  • compound share (both sides) → re-minted as liquidity
  • buyback share (secondary side) → fuels the pot
  • burn share (main side) → the burn cascade
  • the residual of each side → one recipient per side

Constraints, enforced on-chain when the config is set: compound + buyback ≤ 100% and compound + burn ≤ 100%; burn must be 0 when MAIN is native; a recipient is required wherever a residual exists. Config changes only affect FUTURE harvests.

Compounding anchors on the secondary token: it places as much of the budget as the current price allows and CARRIES the unmatched remainder to the next harvest — nothing leaks, nothing is double-compounded (harvested fuel sits in the pot, not in the next compound).

one harvest, both sides
swap fees accrue in the position
fees ≥ minimums → next swap triggers harvest
SECONDARY: compound% + buyback% + residual→recipient
MAIN: compound% + burn% + residual→recipient
compound mints liquidity · unmatched side carries

the in-swap auto-harvest runs under a hard gas budget so a heavy config can never tax swappers: it reverts atomically (fees stay safe) and the public harvest(key) — full caller gas — picks it up.

05 · who controls what

Three roles, all surrenderable

pot admin
whoever initialized the pool on the PoolManager
  • · initPot — declare MAIN + recipient
  • · setRecipient (0x0 = burn)
  • · create the pool's ONE LP program
admin is fixed at pool creation
program owner
set at program creation (defaults to creator)
  • · add / remove liquidity
  • · harvest (when not public)
  • · transfer or surrender ownership
owner = 0x0 → LP locked FOREVER, harvest forced public
program operator
starts as the owner; reassignable
  • · edit the fee split
  • · edit minimums + recipients
  • · toggle public harvest
operator = 0x0 → config frozen forever

the hook itself is ownerless: no role above has power over the hook, only over their own pool's pot or program. lockers, DAOs and vesting contracts compose on top by holding these roles.

06 · guide

Launch a hooked pool

1
Build the PoolKey with the hook address
Sort your two currencies (native = address(0) is always currency0), pick a NON-ZERO fee tier, and set hooks = GlueHook. A zero-fee pool would never pump.
2
Launch everything in ONE transaction
Call launchPool(key, sqrtPriceX96, main, recipient, 0, 0, liquidity, owner, config): it initializes the pool (YOU become the pot admin), declares which currency is MAIN (defended) and where bought MAIN goes (0x0 = burn cascade), and seeds the LP program with its split rules — atomically. Prefer separate steps? PoolManager.initialize, initPot and addLiquidityAdvanced still work individually.
3
Fuel the pot
donate(key, amount) in the SECONDARY currency — from your treasury, your launch contract, or your community. The machine runs from here.
// 1. the key (native/token pool, 0.30%) PoolKey memory key = PoolKey({ currency0: address(0), currency1: TOKEN, fee: 3000, tickSpacing: 60, hooks: 0x89C5e863e1CD6D0EfcA0dF1699Fbe5F67e30a0C8 }); // 2. ONE tx: init + roles + seeded LP program hook.launchPool{value: 5 ether}( key, SQRT_PRICE_1_1, TOKEN, address(0), // defended + burn 0, 0, liquidity, // full range msg.sender, config ); // 3. fuel it (secondary = native here) hook.donate{value: 10 ether}(key, 10 ether);
07 · guide

Contract-to-contract buybacks — no oracle

Traditional buyback machinery needs a price oracle and a keeper — two trust dependencies. Hook needs neither: your contract just donate()s, and the pot executes at REAL market prices, riding real user buys, only when there is real demand.

Two views quote the machine before you act: quotePump(key, buySize) returns what the pot would spend and buy alongside a buy of that size; quoteShield(key, sellSize) returns how much of a sell the pot would absorb and pay.

Everything is at the same address on all 18 chains, so a single integration ports everywhere unchanged.

// route protocol revenue into the pot IGlueHook constant HOOK = IGlueHook(0x89C5e863e1CD6D0EfcA0dF1699Fbe5F67e30a0C8); function routeRevenue(uint256 amt) external { // ERC20 secondary: approve + donate SECONDARY.approve(address(HOOK), amt); HOOK.donate(key, amt); } // read the machine (uint256 spend, uint256 out) = HOOK.quotePump(key, 1 ether); (uint256 absorbed, uint256 paid) = HOOK.quoteShield(key, -1e18);
08 · guide

LP program recipes

The pot admin creates the pool's single program with addLiquidity (everything off, tune later) or addLiquidityAdvanced (full rules at creation). Ticks 0/0 mean full range. Some proven configurations:

plain LP

a normal position; harvest manually, keep everything.

  • · compound 0%
  • · buyback 0%
  • · burn 0%
  • · recipients = you
growth engine

fees deepen liquidity and fuel the pot automatically.

  • · compound 50%
  • · buyback 50% (sec)
  • · burn 0%
  • · auto-harvest armed
deflationary

the main side burns, the secondary side fuels defense.

  • · compound 30%
  • · buyback 40% (sec)
  • · burn 70% (main)
  • · recipient = treasury
trustless lock

owner surrendered at birth: LP locked forever, machine public.

  • · owner = 0x0
  • · harvest forced public
  • · operator kept OR 0x0
  • · config frozen optional
09 · reference

Addresses, ABI and audit

deployments — 18 networkssame address everywhere
networkhookenv
Ethereum0x89C5…a0C8mainnet
Base0x89C5…a0C8mainnet
Unichain0x89C5…a0C8mainnet
Arbitrum0x89C5…a0C8mainnet
Optimism0x89C5…a0C8mainnet
BNB Chain0x89C5…a0C8mainnet
Polygon0x89C5…a0C8mainnet
World Chain0x89C5…a0C8mainnet
Zora0x89C5…a0C8mainnet
Soneium0x89C5…a0C8mainnet
MegaETH0x89C5…a0C8mainnet
Robinhood0x89C5…a0C8mainnet
Tempo0x89C5…a0C8mainnet
Sepolia0x89C5…a0C8testnet
Base Sepolia0x89C5…a0C8testnet
Unichain Sepolia0x89C5…a0C8testnet
Arbitrum Sepolia0x89C5…a0C8testnet
Robinhood Testnet0x89C5…a0C8testnet
canonical 0x89C5e863e1CD6D0EfcA0dF1699Fbe5F67e30a0C8 — every network, no exceptions
the whole external surface
// launch launchPool(key, sqrtP, main, recipient, tickL, tickU, liq, owner, config) payable // pot initPot(key, main, recipient) setRecipient(poolId, recipient) donate(key, amount) payable flushDirect(poolId) // LP program addLiquidity(key, tickL, tickU, liq, owner) payable addLiquidityAdvanced(…, owner, config) payable addProgramLiquidity(key, liq) payable removeProgramLiquidity(key, liq, to) harvest(key) · claim(asset) setProgramConfig(poolId, config) setProgramOperator(poolId, op) transferProgramOwnership(poolId, owner) // views potOf(poolId) · programOf(poolId) quotePump(key, amountIn) · quoteShield(key, amount) owedOf(to, asset) · heldOf(asset) · parkedOf(asset)
good to know
  • the hook lives at the same canonical address on every network — Tempo included, no exceptions.
  • the hook address carries the V4 permission flags in its low bits (beforeInitialize · beforeSwap · afterSwap · beforeSwapReturnsDelta) — that's why the deployer key was mined.
  • failed payouts never brick anything: recipient deliveries park and retry (flushDirect), harvest payouts bank into owedOf and are pulled with claim(asset).
  • fee-on-transfer secondaries are measured on arrival — the pot books exactly what landed.
Full audit + formal math
adversarial suites, invariant campaigns, sandwich-economics proofs
read it ↗