Appearance
Swapping
The formula
For an exact-input swap:
amountInWithFee = amountIn × 998
amountOut = (amountInWithFee × reserveOut)
/ (reserveIn × 1000 + amountInWithFee)The 998 / 1000 is the 0.2% fee. Uniswap V2 uses 997; this chain does not. Multi-hop swaps chain this through each pair in the path.
Quote first
Never guess the output. Ask the router:
js
import { ethers } from 'ethers';
const ROUTER = '0x0C7A38D71E351eBd4cc2c1B2988c7dF025fc28A4';
const router = new ethers.Contract(ROUTER, [
'function getAmountsOut(uint amountIn, address[] path) view returns (uint[])',
'function getAmountsIn(uint amountOut, address[] path) view returns (uint[])',
], provider);
const path = [WKNYX, KUSD];
const amounts = await router.getAmountsOut(ethers.parseEther('1'), path);
console.log('out:', ethers.formatEther(amounts[amounts.length - 1]));getAmountsOut answers "if I spend this, what do I get". getAmountsIn answers the reverse.
Approve the router
Approve before every first swap of a token
The router moves your tokens with transferFrom, so it needs an allowance. Without it the swap reverts with TRANSFER_FROM_FAILED.
js
const token = new ethers.Contract(tokenIn, [
'function approve(address spender, uint256 amount) returns (bool)',
'function allowance(address owner, address spender) view returns (uint256)',
], signer);
const current = await token.allowance(await signer.getAddress(), ROUTER);
if (current < amountIn) {
await (await token.approve(ROUTER, amountIn)).wait();
}Approving an exact amount rather than an unlimited one limits the damage if the spender is ever compromised. It costs one extra transaction per swap.
Execute
js
const router = new ethers.Contract(ROUTER, [
'function swapExactTokensForTokens(uint amountIn, uint amountOutMin, address[] path, address to, uint deadline) returns (uint[])',
], signer);
const [, expected] = await router.getAmountsOut(amountIn, path);
const amountOutMin = expected * 995n / 1000n; // 0.5% tolerance
const deadline = Math.floor(Date.now() / 1000) + 600;
const tx = await router.swapExactTokensForTokens(
amountIn, amountOutMin, path, await signer.getAddress(), deadline,
);
const receipt = await tx.wait();
console.log('swapped in block', receipt.blockNumber);Slippage and deadline
amountOutMin is your protection. Between quoting and mining, someone else may trade the same pool and move the price. If the output would fall below your minimum the swap reverts rather than filling at a bad rate.
deadline protects against a transaction sitting unmined and executing much later at a stale price. Ten minutes is a reasonable default.
Never pass zero
amountOutMin: 0 means "accept any price". On a thin pool that can mean losing most of the input.
Router methods
| Method | Use |
|---|---|
swapExactTokensForTokens | Spend exactly X |
swapTokensForExactTokens | Receive exactly Y |
swapExactETHForTokens | Spend exactly X native KNYX |
swapTokensForExactETH | Receive exactly Y native KNYX |
swapExactTokensForETH | Spend exactly X tokens, receive KNYX |
swapETHForExactTokens | Receive exactly Y tokens, paying KNYX |
The ETH in these names is inherited from Uniswap; here it means native KNYX, wrapped to WKNYX inside the router.
For fee-on-transfer tokens use the …SupportingFeeOnTransferTokens variants; the standard methods revert because the amount received differs from the amount sent.
Events
Every swap emits, on the pair:
Swap(address sender, uint amount0In, uint amount1In,
uint amount0Out, uint amount1Out, address to)One side of each pair is always zero. This is the event the explorer's DeFi panel sums to produce pool volume — see Explorer services API.
A real swap
0x153e2de079a183a1f7be85413dbcdad429b850b1e9d9a1c3545042aabdee72abBlock 6,169,510, on the WKNYX/KUSD pool.