etherflow.fun
mechanism9 min read

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.

Wallet swaps, aggregator routes and solver fills all reach the same Uniswap v4 pool, where the hook takes its cut of the ETH before splitting it between the nominated wallet and the treasury.wallet swapaggregator routesolver fillUNISWAP V4 POOLFLOWHOOKtakes its cut of the ETHthe curve, untouchedliquidity locked at launchTHE WALLET YOU NAMEDclaimable any timeTREASURYkeeps the site runningno route reaches the curve without passing the hook first

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 inETH is the named currency, so the fee comes off the amount before it reaches the curve. Charged in beforeSwap.
Sell, exact ETH outETH 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 outThe 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 inSame 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

src/FlowHook.sol
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.

src/FlowHook.sol
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

Sending ETH during a swap means a recipient that cannot receive it can make the swap fail or burn gas silently. Crediting a balance means the worst case is that some ETH waits until an address that can take it asks. There is a test for exactly this: a beneficiary contract that reverts on receive still cannot wedge anyone's trade.

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.