Fees
Two layers: pool fee (steward) and protocol fee (network). Both come out of the user's output. The pool owner always receives the full pool fee.
FeePolicy
Proxy: yes.
defaultFeein PPM,<= 1_000_000.- Optional per-pair override, direction-sensitive (
A→B≠B→A). - Pair fee of
0is treated as unset → falls back to default. You cannot set a literal zero pair fee;removePairFeeinstead.
getFee(tokenIn, tokenOut)
calculateFee(tokenIn, tokenOut, amount) // amount * fee / PPM
setDefaultFee / setPairFee / removePairFee // ownerisActive() always returns true (interface stub).
ProtocolFeeController
Proxy: yes. One per network, shared by pools.
initialize(owner, initialFee, initialRecipient) // recipient ≠ 0, starts active
getProtocolFee() // 0 if inactive
getProtocolFeeRecipient()
setProtocolFee / setProtocolFeeRecipient / setActiveImplementation: 0x302E6d52…23B4d.
How SwapPool combines them
totalFee = quoted * feePpm / PPM
assumedFee = quoted * 10_000 / PPM // 1% floor
effectiveFee = max(totalFee, assumedFee)
protocolFee = effectiveFee * protocolFeePpm / PPM
net = quoted - totalFee - protocolFeeSkipped if controller unset, protocolFeePpm == 0, or recipient is zero.
Joint constraint: feePpm * (PPM + protocolFeePpm) < PPM² whenever feePpm >= 10_000. Else FeeTooHigh on quote and swap, including dust sizes.
Splitter
Proxy: yes. Pays ETH or ERC-20 to a fixed recipient set.
Allocations in PPM must sum to exactly 1_000_000. ≥2 recipients, no duplicates/zero/self. Only the hash of (accounts, percentAllocations) is stored; distribute calls must pass the same arrays (InvalidHash otherwise).
Fractional remainders accrue. Empty balance is a no-op.