Muup.fun
Open the app

Claiming fees

One function, on both factories, and anybody may call it. What the caller cannot do is change where the money goes — there is no recipient argument, so there is nothing to misroute.

function collectFees(address token) external;// emitted by both factories, same signature, same topic0event FeesCollected(    address indexed token,    address indexed creator,    uint256 creatorNative,   // creatorWeth on the V3 factory — same slot    uint256 platformNative,    uint256 creatorToken);

Permissionless on purpose

No owner key is involved and there is no access check. Anyone can trigger a payout for any coin — a bot, a block explorer, a curious stranger — and the money still goes to the creator’s wallet and the platform recipient, split by the factory’s own creatorFeeBps. A creator who loses interest does not strand their own fees, and nobody who calls it gains anything by doing so.

Three regimes, and the hook decides which

Where fees come from — and therefore what tokens arrive — depends on whether the pool has a quote-side hook, not on the Uniswap version. Those are different questions and conflating them is how the launch panel once promised Base something it does not do.

RegimeFee comes fromCreator receives
V3the liquidity position, both sidescreatorFeeBps of the quote side, and 100% of the coin side
V4 with hook1% of the quote side, taken outside the poolcreatorFeeBps of that — quote asset only, no coin side at all
V4 hooklessthe liquidity position, both sidescreatorFeeBps of the quote side, and 100% of the coin side

The hook runs on Arc and Robinhood Mainnet. Everywhere else the third row applies, which is why a flat “90% in the quote asset” is true on one chain and not on another. The live split per network is on Fees and creator rewards.

A coin may send the creator's side somewhere else entirely

collectFees pays creatorOf[token], and on a coin whose creator chose holder fee sharing that address is a vault contract, not a wallet — holders then claim from it individually. The call is unchanged either way, which is the point: nothing in the fee path branches on it.

A creator's own trade tax is separate and is paid to taxPayeeOf[token], which stays the human even on a sharing coin. So one collectFees can pay three different addresses. Reading creatorOf alone and calling it “the creator” will misattribute both. See Trade tax and holder rewards.

Reading what is owed, before claiming it

V3 has collect with a static call and you get a number. V4 has nothing equivalent: there is no view that answers “how much does this position owe”. You compute it.

Fees are a per-pool accumulator minus the snapshot taken when the position last moved, multiplied by liquidity. The subtraction must wrap:

// Solidity computes this unchecked, so it wraps past 2**256.// A plain subtraction in JS or Python goes negative and reports// a huge NEGATIVE fee on a position that is perfectly healthy.owed = ((growthNow - growthAtSnapshot) & ((1n << 256n) - 1n)) * liquidity >> 128n;

feesOwedFromGrowth in @muupfun/shared does this, with a test that fails if the mask is removed. Reuse it rather than reimplementing the wrap.

A wrong position key reports no fees, not an error

The key is keccak(positionManager, tickLower, tickUpper, bytes32(tokenId)). Get any part of it wrong and StateView returns a position with zero liquidity rather than reverting — so the integration reports “nothing owed” on a position earning steadily, and there is nothing in the response that looks wrong. Check the liquidity you read against the PositionManager’s own figure before trusting any number derived from it.

When a claim can fail for reasons that are not ours

  • A blocklisted wallet. On a chain whose issuer can freeze an address, a transfer to or from one reverts — and collectFees pays a creator wallet, so the whole call reverts rather than skipping that leg. Nothing in our contract is broken when that happens.
  • A position that has never traded. collectFees on a coin with no accrued fees succeeds and pays nothing. It is not an error, and repeated calls are harmless.
  • Calling from inside another V4 lock. The factory does not guard for it, deliberately — the selector that would have given a nicer error is not in the deployed PoolManager. The call still fails, just with V4’s error instead of ours.

Fees are claimable, not automatic

Nothing sweeps them for you on a schedule. They accrue in the position — or in the hook — until somebody calls the function, and they are not at risk while they sit there.