Where the fee is taken
A transfer tax fires when the pool is the counterparty, which an aggregator route makes it not. A hook runs inside the pool, which no route can avoid. That difference is the whole product.
Why the old way leaked
Between 2021 and roughly 2024, sharing trading fees meant putting a tax in the token's transfer function. The token checked whether one side of the transfer was the pool address and skimmed a percentage if it was.
It worked for direct swaps. It did not work for anything routed through an intermediate contract, because then the pool was never the counterparty of the user's transfer and the tax simply never fired. 1inch, Paraswap, 0x and every solver network route that way. Anyone who wanted to avoid paying could, by clicking a different button.
The pattern also aged badly for a second reason: a token that can rewrite balances mid transfer is exactly the shape a honeypot takes, so by 2026 every launch checklist on this chain treats a transfer tax as a warning sign rather than a feature.
What a hook changes
Uniswap v4 moved the extension point from the token to the pool. A pool can carry a hook contract, and the pool manager calls into it around every swap it serves. The route in stops mattering, because by the time the swap reaches the curve it has already passed the hook.
Etherflow's hook only ever touches currency0, which is always native ETH. Your coin is a plain ERC-20 with no transfer logic at all, which means anything that can integrate an ERC-20 can integrate it without reading a tax table first.
The four shapes a swap can take
A v4 swap names one currency and one amount. If the amount is negative it is exact input, otherwise exact output. Combined with direction, that gives four cases, and which of them the hook can charge in beforeSwap depends on whether the currency the trader named is ETH.
| Buy, exact ETH in | ETH is the named currency, so the fee comes off the amount before it reaches the curve. Charged in beforeSwap. |
|---|---|
| Sell, exact ETH out | ETH is named again. The curve pays out a little extra and the hook keeps it, so the seller receives exactly what they asked for and the buyer covers the difference. Charged in beforeSwap. |
| Buy, exact coin out | The ETH amount is only known once the swap has run, so the fee is taken afterwards and the trader owes slightly more ETH. Charged in afterSwap. |
| Sell, exact coin in | Same situation mirrored: the ETH payout is computed, then the fee comes off it. Charged in afterSwap. |
The test suite exercises all four and asserts a fee lands in every one of them, which is the point: there is no shape of swap that pays nothing.
The branch that decides
bool exactIn = params.amountSpecified < 0;
// ETH is currency0. It is the specified currency exactly when the swap is
// exact-input in the zeroForOne direction, or exact-output the other way.
if (c.feeBps == 0 || exactIn != params.zeroForOne) {
return (IHooks.beforeSwap.selector, ZERO_DELTA, 0);
}
uint256 named = exactIn
? uint256(-params.amountSpecified)
: uint256(params.amountSpecified);
uint256 fee = (named * c.feeBps) / BPS;
poolManager.take(key.currency0, address(this), fee);
_credit(id, c, fee);
return (
IHooks.beforeSwap.selector,
toBeforeSwapDelta(int128(uint128(fee)), 0),
0
);The take pulls real ETH out of the pool manager, creating a negative delta for the hook. The positive delta it returns cancels that out, so the unlock closes at zero and the swap settles normally.
The split, and why it is a credit
The collected ETH is divided by the share you set at launch and added to two running balances: the wallet you nominated, and the treasury. Nothing is transferred during the swap.
function _credit(PoolId id, PoolConfig memory c, uint256 fee) private {
uint256 toBeneficiary = (fee * c.beneficiaryShareBps) / BPS;
uint256 toTreasury = fee - toBeneficiary;
if (toBeneficiary != 0) earned[c.beneficiary] += toBeneficiary;
if (toTreasury != 0) earned[treasury] += toTreasury;
feesByPool[id] += fee;
feesTotal += fee;
emit FeeTaken(id, c.beneficiary, toBeneficiary, toTreasury);
}Pull, never push
The limit worth stating plainly
The hook governs the pool it is attached to. Nothing prevents someone opening a second pool for the same coin with no hook on it, and no fee would apply there.
In practice they would be trading against liquidity they supplied themselves, because all of the launch liquidity is permanently inside the Etherflow pool. But it is a real gap and it is the honest counterpart to the aggregator gap the old tax model had. The difference is that this one requires deliberately building a competing market rather than clicking a different router.
Claiming
claim(address to) sends the caller's whole balance wherever they point it. Only the address that earned a balance can claim it. The fees page is a thin wrapper around that one function and you never have to use it.