Section 15 of 16
Router: Fee-on-Transfer Support
Key takeaway: Fee-on-transfer tokens (USDT-style fee mechanisms, SafeMoon-style burns, reflection tokens) deduct a percentage on every transfer, so the pair receives less than the Router pre-calculated. Standard
_swap()reverts withUniswapV2: Kbecause the invariant check runs against the assumed input amount. TheswapExactTokensForTokensSupportingFeeOnTransferTokensvariant fixes this by measuring the pair's actualbalanceOfbefore and after each hop, computing the real input/output, and usinggetAmountOutagainst measured values rather than pre-calculations.
What You Are Building
Some ERC20 tokens deduct a fee on every transfer. When you call transfer(recipient, 100), the recipient might only receive 98 tokens. The missing 2 tokens are burned, sent to a treasury, or redistributed to holders. This behavior is called "fee on transfer" (FOT), and it breaks the standard Uniswap V2 swap functions in a way that is not immediately obvious.
How FOT Tokens Break Standard Swaps
Consider a normal swap where a user sends 100 FOT tokens to a pair. The Router calls getAmountOut(100, reserveIn, reserveOut) to pre calculate the expected output. It then calls pair.swap() with that output amount. But the pair only received 98 tokens (the FOT token deducted 2%). When the pair checks the K invariant, it compares the actual balances against the expected product. The math was calculated assuming 100 tokens arrived, but only 98 did. The adjusted K is smaller than the old K, and the transaction reverts with UniswapV2: K.
This is not a bug in Uniswap V2. The pair contract works correctly. The problem is in the Router: it pre calculates amounts based on the assumption that 100% of transferred tokens arrive at the destination. For FOT tokens, that assumption is false.
Real world examples of FOT tokens: early versions of USDT had a fee mechanism (though it was set to 0%), SafeMoon and its many forks burn a percentage on transfer, and various "reflection" tokens redistribute fees to holders. Any protocol integrating with tokens in the wild must handle this.
_swapSupportingFeeOnTransferTokens: The Core Fix
The standard _swap() function receives a pre calculated amounts[] array and uses it to determine how much output each hop should produce. The FOT version throws away pre calculation entirely. Instead, it measures reality.
For each hop in the path:
-
Get reserves: Fetch the current reserves for the pair from the Library.
-
Read actual balance: Call
IERC20(input).balanceOf(pair)to see exactly how many input tokens the pair currently holds. -
Compute real input:
amountInput = balance - reserveInput. The stored reserve reflects the pair's balance before the transfer. The current balance reflects the balance after. The difference is the actual amount that arrived, after any FOT deduction. If you sent 100 and 98 arrived, amountInput is 98. -
Calculate output from real input: Call
getAmountOut(98, reserveIn, reserveOut)instead of using a pre calculated value based on 100. The output is smaller, but it is mathematically correct. -
Execute the swap: Determine swap direction (is input token0 or token1?) and call
pair.swap()with the correctly calculated output.
The pair's K invariant check passes because the output was calculated from the actual input, not the assumed input. The math is consistent.
This approach requires an extra balanceOf call per hop (one STATICCALL, roughly 2600 gas). That is the gas cost of correctness for FOT tokens.
Why FOT Functions Cannot Replace Standard Functions
If the FOT version works with all tokens (including normal ones), why not use it everywhere? Two reasons.
First, gas efficiency. The standard swap pre calculates all amounts in one Library call, then executes each hop using those cached values. The FOT version makes an additional balanceOf call at every hop. For a three hop swap, that is three extra external calls. For high frequency traders and arbitrage bots executing thousands of swaps, those extra costs add up.
Second, upfront validation. The standard swapExactTokensForTokens calls getAmountsOut before any transfers happen. If the calculated output is below amountOutMin, the transaction reverts immediately without moving any tokens. The FOT version cannot do this because it does not know the real amounts until tokens are transferred. It can only check amountOutMin at the very end, after all hops have executed. If the slippage check fails, the entire multi hop swap is unwound, wasting more gas.
The recommendation is straightforward: use the standard functions for normal tokens, use the FOT functions only when you know a token in the path deducts fees on transfer.
FOT Swap Variants
Three external functions use the FOT safe internal swap:
swapExactTokensForTokensSupportingFeeOnTransferTokens: The user specifies an exact input amount and a minimum acceptable output. The function transfers input tokens to the first pair, runs the FOT swap through all hops, then checks the recipient's balance. It snapshots the recipient's balance of the output token before and after the swap, requiring that the increase is at least amountOutMin. This balance diff approach is the only way to measure the real output when FOT tokens are involved, since the Router cannot predict how much will arrive.
swapExactETHForTokensSupportingFeeOnTransferTokens: The user sends ETH as msg.value. The function wraps it to WETH, deposits WETH directly to the first pair (WETH is not a FOT token, so this transfer is safe), runs the FOT swap, and validates the recipient's output token balance increased by at least amountOutMin.
swapExactTokensForETHSupportingFeeOnTransferTokens: Runs the FOT swap with the Router itself as the final recipient. The Router receives WETH (the output of the last hop), measures how much it got, unwraps it to ETH, and sends the ETH to the user. The path must end with WETH for this to work.
Notice there are no "exact output" FOT variants (no swapTokensForExactTokens equivalent). Exact output functions work backward: "I want exactly 1000 USDC, how much input do I need?" With FOT tokens, you cannot guarantee the exact output because the fee deduction is unpredictable. Even if you calculate the correct input, the output transfer itself might lose tokens to the fee. Only "exact input" variants are possible.
FOT Safe Liquidity Removal
removeLiquidityETHSupportingFeeOnTransferTokens: When you remove liquidity from a token/WETH pair, the pair burns your LP tokens and sends you both tokens. The standard removeLiquidityETH trusts the return value from the pair's burn() function to know how many tokens the Router received, then forwards that exact amount to the user. But if the token is FOT, the transfer from the pair to the Router deducts a fee. The Router received fewer tokens than burn() reported. When it tries to transfer the full reported amount to the user, it does not have enough and the transaction reverts.
The FOT version fixes this by reading the Router's actual balance of the FOT token after the burn, then transferring that real balance to the user. The WETH side does not need this treatment because WETH is a standard token with no transfer fees.
removeLiquidityETHWithPermitSupportingFeeOnTransferTokens: Identical to the above, but adds EIP2612 permit support so the user can approve and remove liquidity in a single transaction instead of requiring a separate approve call.
Library Wrapper View Functions
The Router exposes five of the Library's calculation functions as public view methods: quote, getAmountOut, getAmountIn, getAmountsOut, and getAmountsIn. Each simply delegates to UniswapV2Library with no additional logic.
These wrappers exist so that external contracts and frontends can call them directly on the Router address without importing the Library. A frontend can call router.getAmountsOut(1e18, [WETH, USDC]) to display the expected swap output to a user, without needing to deploy or reference the Library contract separately.
Your Task
Implement the FOT safe swap internal function, all five FOT external functions, and the five library wrapper view functions. The starter code provides the function signatures and the infrastructure from previous sections.