Distribute & Reinvest Fees
Triggering fee distribution for any token. Permissionless — anyone can call it, for any graduated token, at any time.
Who calls this
Nobody is obliged to. Distribution is a public good: it credits the deployer, feeds the protocol balance, and deepens the pool. Deployers call it to unlock their share; keepers and bots call it to keep pools growing; anyone can call it out of self-interest as a holder.
Basic call
const HOOK_ABI = [
"function distributeFees(address token)",
"function processProtocolFees()",
"function canDistribute(address token) view returns (bool)",
"function canProcessProtocolFees() view returns (bool)",
"function getPendingFees(address token) view returns (uint256)",
"function getFeeInfo(address token) view returns (uint256 feeBps, uint256 nativeInPool)",
"function getPoolLiquidity(address token) view returns (uint128, uint160, int24)"
];
const hook = new ethers.Contract(HOOK_ADDRESS, HOOK_ABI, signer);
if (await hook.canDistribute(token)) {
const receipt = await (await hook.distributeFees(token)).wait();
}from web3 import Web3
HOOK_ABI = json.load(open("PrimaryHook.abi.json"))
w3 = Web3(Web3.HTTPProvider(RPC_URL))
acct = w3.eth.account.from_key(PRIVATE_KEY)
hook = w3.eth.contract(address=HOOK_ADDRESS, abi=HOOK_ABI)
if hook.functions.canDistribute(token).call():
receipt = send(hook.functions.distributeFees(token))What the call does
- Credits the deployer share to
deployerBalance[token] - Credits the protocol share to
protocolBalance - Zeroes
pendingFees[token] - Reinvests the remainder: swaps half into tokens, adds both halves as liquidity
- Burns leftover token dust
If the pool holds too few tokens for a meaningful liquidity add, step 4 flips to a buy-and-burn instead. You do not choose which — it is selected from live pool state.
Reading which branch ran
const parsed = receipt.logs
.map(l => { try { return hook.interface.parseLog(l); } catch { return null; } })
.filter(Boolean);
const distributed = parsed.find(e => e.name === "FeesDistributed");
const reinvested = parsed.find(e => e.name === "Reinvested");
const burned = parsed.find(e => e.name === "BuyAndBurn");
// FeesDistributed always fires; exactly one of the other two follows# process_receipt ignores logs that do not match the event, so each call
# returns only its own occurrences.
distributed = hook.events.FeesDistributed().process_receipt(receipt)
reinvested = hook.events.Reinvested().process_receipt(receipt)
burned = hook.events.BuyAndBurn().process_receipt(receipt)
# FeesDistributed always fires; exactly one of the other two followsPartial processing is normal
The reinvest swap is capped so it cannot be sandwiched profitably. When pending fees exceed that cap, the excess is not rejected — it rolls forward into protocolBalance and is processed on later calls.
Two consequences for integrators:
FeesDistributed.totalis what was credited this call. Compare it against the pending balance you read beforehand to see whether a backlog remains.- Repeated calls are safe and are the intended way to drain a backlog on a thin pool. There is no penalty for calling when there is little to do beyond the dust guard reverting.
Protocol fees
if (await hook.canProcessProtocolFees()) {
await (await hook.processProtocolFees()).wait();
}if hook.functions.canProcessProtocolFees().call():
send(hook.functions.processProtocolFees())This handles $LP24 separately: all of the protocol balance goes into buying and burning $LP24 when its pool is token-rich, or into deepening that pool when it is not. distributeFees rejects $LP24 with Use processProtocolFees.
Gnosis
// V3 fees must be pulled out of the position first
await (await manager.collectFees(token)).wait();
await (await manager.distributeFees(token)).wait();# V3 fees must be pulled out of the position first
send(manager.functions.collectFees(token))
send(manager.functions.distributeFees(token))collectFees is permissionless and splits the collected amount into two buckets — native into pendingFees, token side into pendingTokens. Distribution then takes the DEEP or THIN path depending on pool depth. Additional events to watch for there: FeesCollected, TokensBurned, TokenFeesLPed.
Monitoring for a keeper
// Cheap poll: which tokens are worth distributing right now
const ready = [];
for (const token of watchlist) {
if (await hook.canDistribute(token)) {
const pending = await hook.getPendingFees(token);
ready.push({ token, pending });
}
}
ready.sort((a, b) => (b.pending > a.pending ? 1 : -1));# Cheap poll: which tokens are worth distributing right now
ready = []
for token in watchlist:
if hook.functions.canDistribute(token).call():
pending = hook.functions.getPendingFees(token).call()
ready.append((token, pending))
ready.sort(key=lambda row: row[1], reverse=True)canDistribute() checks that fees are pending, a position exists, and the token is not $LP24. It does not simulate the swap, so a call can still revert with Pool too thin for any safe swap on a pool with essentially no liquidity.
Common reverts
| Revert | Cause |
|---|---|
No fees / Nothing to distribute | Pending balance at or below the dust threshold |
Use processProtocolFees | Token is $LP24 |
No position | Token has not graduated |
Nothing to process | Protocol balance at or below 1,000,000 wei |
Pool not initialized | $LP24 pool has no price yet |
Pool too thin for any safe swap | Pool cannot absorb even a capped swap |