Skip to main content

Trading tax hook

The ETH/STANDARD pool is a hooked Uniswap v4 pool: a dedicated tax-hook contract runs on every swap and applies a buy-side and sell-side tax, on top of whatever the standard pool fee is.

Sourcify-verified source

The TaxHook contract has Sourcify-verified source (exact match, 2026-09-16), alongside the Standard token contract. Everything below is readable directly in Solidity, the strongest basis this documentation reports: the mechanics described here match the verified source exactly, and this page calls out the few places the source shows detail beyond what measurement alone would reveal.

Launch decay to floors

At launch, the tax starts high on both sides and decays toward a permanent floor rate. The decay curve was measured directly against the live contract at 12 historical blocks inside the launch hour (basis: measured, decay_model_basis: measured_on_chain):

t      = block.timestamp − tax_decay_start
bps(t) = floor( decay_floor_bps + (decay_start_bps − decay_floor_bps) · 0.5^(t / tax_half_life) )

The key detail: it's the excess over the floor that halves every half-life (a chain-read constant), not the whole rate. A "whole-rate" reading (max(floor, start · 0.5^(t/half_life))) was directly ruled out by the measurements; at the same timestamps, the whole-rate curve and the excess-over-floor curve diverge by hundreds of basis points, and the live contract tracked the excess-over-floor curve exactly, truncated (not rounded) at each step. Once elapsed time reaches the decay duration (also chain-read), the hook returns the floor rate outright and the launch schedule flips to inactive.

GET /v1/tax/schedule reports the currently measured buy/sell bps, the decay configuration, and (only while the schedule is still active and not overridden) a labeled projection at a few forward horizons, computed from the same chain-read constants (not illustrative whitepaper numbers). Each response also runs decay_model_check, comparing the measured current rate against the excess-over-floor curve: consistent is the expected case; inconsistent would mean the measured rate instead matches the ruled-out whole-rate curve, which would only happen if the hook were redeployed with different logic. The route's freshness_basis tells you whether as_of_block/serving_state/lag_blocks come from the snapshot step's own per-pass marker (the normal, healthy case) or from a fallback read of the newest constant timestamp, which matters once the rate stops changing (see below) and the normal marker alone would otherwise look stale.

The launch schedule itself ran on a fixed clock: a half-life (a chain-read constant, currently 240 seconds) and a total decay duration (currently 3600 seconds) anchored at pool initialization, with no admin input while it runs. That window closed around 2026-09-15 ~02:00Z, so launch_schedule_active reads false and the live buy/sell rates sitting at their floors (roughly 2% and 3%) are what actually gets charged, unless the owner has called setTaxes to override them (see below).

Where the tax is taken

  • Buy (ETH → STANDARD): taken in the hook's pre-swap step, as bps / 10000 of the ETH input. The pool itself only ever sees the post-tax amount.
  • Sell (STANDARD → ETH): taken in the hook's post-swap step, as bps / 10000 of the ETH output. The seller receives the post-tax amount; the pool-side swap itself moves the full pre-tax amount.

This was confirmed against real transaction receipts by matching the tax-collection event's log index and amount against the adjacent pool Swap event, on both buy and sell sides, and reproduced by fork replay. It matters for any price-impact estimate: applying the buy tax to ETH going in versus the sell tax to ETH coming out, at the correct point in the swap, is what makes an exit quote's realizable_eth or a license's ETH-converted cost line up with what actually settles.

The "buy before / sell after" rule above holds for exact-input swaps only. The verified source shows the hook actually keys off which side of the swap ETH is the specified (caller-stated) leg, not off buy-vs-sell: an exact-input buy and an exact-output sell both state the ETH leg directly, so both are taxed pre-swap on the requested amount; an exact-output buy and an exact-input sell both leave ETH as the leg the pool determines, so both are taxed post-swap on the realized amount. For an exact-input swap (the common case for a router-driven trade) that collapses to exactly the buy-before/sell-after rule above. For an exact-output swap it flips: an exact-output buy is taxed post-swap (so its TaxCollected event can log after the pool's own Swap event), and an exact-output sell is taxed pre-swap. A price-impact tool that infers "buy" or "sell" purely from event log order, rather than from the swap's actual direction, will mis-tag exact-output trades.

An exact-output swap also can't tax the requested amount directly, since it only specifies the net leg it wants; the hook grosses the fee up (fee = net · rate / (1 − rate)) so the effective rate on the full gross ETH leg still matches what an exact-input swap would have paid at the same rate. Nothing in this API distinguishes exact-input from exact-output trades in its price-impact estimates today, since realizable_eth and the license-cost estimate both model a simple constant-liquidity swap; treat this as a property of the underlying mechanism rather than something the API's assumptions[] currently calls out swap-shape by swap-shape.

Net flow excludes the protocol's own swaps

The hook also tracks epochNetFlow, the pool-crossing ETH used to compute the Central Bank's issuance signal (see Protocol overview and issuance). Every taxed swap still pays its tax, but two senders are excluded from that net-flow tally even though their swaps go through the same pool: the Contraction Vault's buyback swaps and the POL Manager's pairing swaps. This keeps a buyback from flipping the very signal that triggered it, and keeps a permissionless POL-pairing crank from injecting positive flow using the protocol's own ETH. Any reconstruction of the net-flow signal from raw swap events needs to apply the same sender exclusion, or it will overstate net inflow whenever a buyback or POL pairing swap ran during the window.

Taxes sit in the hook until forwarded

Collected taxes are held in the TaxHook contract itself, not pushed anywhere automatically. address(hook).balance at any moment is exactly the taxes accrued since the last forward. The permissionless forwardTaxes() function pushes that balance to the FeeSplitter, but only after an epoch boundary closes the running window (before the first boundary is armed, it forwards everything held). "Permissionless" here means anyone can call it, not that it happens automatically: until it's called (or the boundary closes and the bank pulls the window), collected taxes just accumulate in the hook rather than reaching the FeeSplitter. GET /v1/buybacks/readiness reports the ETH currently sitting in both the hook and the FeeSplitter (hook_eth_balance_raw, fee_splitter_eth_balance_raw); see Reserves, vaults, POL, and buybacks for why that balance is protocol revenue in transit rather than reserve backing.

LP withdrawals are taxed on principal

Withdrawing liquidity from the pool is also a taxable event for anyone other than the POL Manager. On afterRemoveLiquidity, the principal portion of a non-POL withdrawal (not the LP fees earned, which are untaxed) pays the same live rates as a trade: the ETH leg pays the sell rate and stays in the hook, and the STANDARD leg pays the buy rate and is burned immediately by the hook rather than returned to the withdrawer. This closes off using a concentrated-liquidity range position as a way to exit at the taxed traders' expense without paying the tax yourself. Relatedly, third-party liquidity provision is closed only while the launch schedule is active: the POL Manager is the only address that can add liquidity to the pool during that window. The schedule no longer runs, so anyone can add liquidity, subject to the ordinary tick-reservation rule that protects the POL Manager's own position.

Launch holding cap

A separate launch-only control, a per-wallet holding cap of 1,200,000 STANDARD, applies only while the launch schedule is active; like the third-party-LP restriction, the holding cap no longer binds once the schedule ends.

The override flag

The hook exposes a taxOverridden flag. When set, currentTaxBps returns a directly-configured buy/sell rate instead of running the decay formula. This is an owner-controlled escape hatch from the launch schedule, independent of elapsed time. GET /v1/tax/schedule's tax_overridden field reports this live; when true, don't expect the decay projection to describe what happens next, since the decayed rate isn't what's actually being charged.

What's confirmed: the decay curve, the truncation behavior, and the buy/sell tax-collection points all carry basis: measured against real mainnet activity, and all match the hook's verified Solidity source exactly. The override flag's existence and its effect on currentTaxBps read the same way from both the measured calls and the source. That match is why this page can describe the exact-output grossing-up, the net-flow exclusion, and the forwarding mechanics in mechanism terms rather than only in terms of what was observed. The API's own basis field on /v1/tax/schedule doesn't distinguish a source-confirmed read from a measured-only one; treat everything on this page as the stronger of the two either way.

Where to look

QuestionRouteMCP tool
What's the current buy/sell tax, and where is it in its decay?GET /v1/tax/schedule1s_std_tax_schedule
How does the tax affect an exit quote's realizable ETH?GET /v1/charters/{id}/exit-quote1s_std_exit_quote
How does the tax affect a license's ETH-converted cost?GET /v1/branches/license-cost1s_std_license_cost
How much ETH is sitting in the hook, unforwarded?GET /v1/buybacks/readiness1s_std_buyback_readiness