chapters· Payouts
20 · Auto-harvest

Payouts & the owed ledger

Bounded-gas pushes, refusals that book instead of revert, and pulling with claim().

The split decides who is owed what; this chapter is how the money actually leaves. One design rule governs all of it: a recipient's behaviour is never allowed to become the pool's problem — refusals book, they don't revert.

The push, bounded

harvest books every leg first — checks-effects-interactions· state is final before the first external call
push to the recipient with bounded gas
success → Paid(to, asset, amount)
refusal → the exact amount books to the owed ledger· Owed(to, asset, amount) — the harvest continues

Native legs are pushed with a fixed 30,000-gas stipend, ERC20 legs with a normal transfer — either way a hostile or merely heavy recipient (a blocklist, a reverting receive(), a gas-guzzling callback) cannot brick the harvest or the swap carrying it. The refused amount is booked in a per-(recipient, asset) ledger, identical to the wei to the leg that bounced — nothing is re-derived after a failure.

The owed ledger

two ways out — both permissionless for the money's rightful owner
// 1 · it folds itself into the next successful push automatically: next payout to (to, asset) = new leg + owedOf(to, asset) // 2 · or the recipient pulls it, any time, with FULL gas: hook.claim(asset); // msg.sender's whole owed balance — reverts on failure, caller chose the destination
owed money is real money

Every owed booking is a term of the hook's obligationOf(asset) accounting, and the hook's balance covers that sum at all times — the solvency invariant of the whole delivery system. "Owed" is a parking state, never a haircut.

Why pull-over-push is the right default

The push-first, book-on-refusal shape gives well-behaved recipients zero friction (money just arrives) while making hostile ones pay for their own hostility: the only party inconvenienced by a reverting treasury is the treasury. Compare the alternatives — pushing with full gas lets one recipient revert everyone's harvest; pulling exclusively makes every honest treasury run a claim bot. The hybrid is strictly better than both.

What to index

the payout event surface
Harvested(poolId, mainFees, secondaryFees, burned, fueled) Compounded(poolId, liquidity, amount0Used, amount1Used) Paid(to, asset, amount) Owed(to, asset, amount) // a refusal — watch these to know when to claim()

A dashboard that sums Paid + Owed per recipient reconstructs every wei a program ever distributed; owedOf(to, asset) is the live outstanding balance at any moment.

FAQ

Why did my payout not arrive as a transfer?+

Pushes carry a bounded gas stipend. If your receiver needs more (or reverted), the amount was booked to the owed ledger — call claim() to pull it with your own gas.

Who can call claim()?+

Only the booked recipient, for their own (recipient, asset) balance. There's no admin sweep and no expiry — owed balances wait forever.

What's the difference between owed and parked?+

Owed = harvest payouts refused by a recipient, pulled via claim(). Parked = pot deliveries refused by the pot recipient, retryable BY ANYONE via flushDirect.

Can a booked balance be redirected?+

No — bookings are keyed to the recipient at the moment of the harvest. Changing the config redirects FUTURE harvests only; history is immutable.

Is the venue solvent for these ledgers?+

Yes — the obligation invariant checks that the hook's custody always covers pot balances + carry + owed + parked, across every randomized campaign.