Appearance
Liquidity and LP tokens
Providing liquidity deposits both sides of a pair and mints LP tokens representing your share. Burning them returns the underlying reserves.
The LP token
Each pair contract is its own ERC-20.
| Name | KnyxSwap LPs |
| Symbol | KNYX-LP |
| Decimals | 18 |
So LP tokens are transferable, can be held by a contract, and can be staked in the farm.
Adding liquidity
You must add both tokens at the pool's current ratio. The router works out the amounts.
js
const router = new ethers.Contract(ROUTER, [
'function addLiquidity(address tokenA, address tokenB, uint amountADesired, uint amountBDesired, uint amountAMin, uint amountBMin, address to, uint deadline) returns (uint,uint,uint)',
], signer);
// approve both tokens for the router first
await router.addLiquidity(
WKNYX, KUSD,
amountADesired, amountBDesired,
amountADesired * 995n / 1000n, // slippage floors
amountBDesired * 995n / 1000n,
await signer.getAddress(),
Math.floor(Date.now() / 1000) + 600,
);Why "desired" and "min" both exist
If the price moves between your call and execution, the router adds what the ratio allows and returns the surplus. The Min values are the floor below which it reverts instead of filling.
The first provider sets the price
WARNING
Adding the first liquidity to an empty pool defines its price — the ratio you deposit is the market rate. Deposit at the wrong ratio and an arbitrageur corrects it immediately, at your expense.
The first provider also permanently locks MINIMUM_LIQUIDITY, 1000 wei of LP, which prevents the pool from being drained to zero. Standard V2 behaviour.
Removing liquidity
js
const router = new ethers.Contract(ROUTER, [
'function removeLiquidity(address tokenA, address tokenB, uint liquidity, uint amountAMin, uint amountBMin, address to, uint deadline) returns (uint,uint)',
], signer);
// approve the LP token for the router first
await router.removeLiquidity(
WKNYX, KUSD, lpAmount, amountAMin, amountBMin,
await signer.getAddress(),
Math.floor(Date.now() / 1000) + 600,
);You receive both tokens in proportion to your share, including accumulated fees.
Reading a pool
js
const pair = new ethers.Contract(PAIR, [
'function getReserves() view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast)',
'function token0() view returns (address)',
'function token1() view returns (address)',
'function totalSupply() view returns (uint256)',
'function balanceOf(address) view returns (uint256)',
], provider);
const [r0, r1] = await pair.getReserves();
const share = (await pair.balanceOf(me)) * 10000n / (await pair.totalSupply());
console.log('share:', Number(share) / 100, '%');token0 is always the numerically lower address, not the order you passed.
Finding a pair address
Pairs deploy via CREATE2, so the address is computable before the pair exists:
js
const factory = new ethers.Contract(FACTORY, [
'function getPair(address tokenA, address tokenB) view returns (address)',
], provider);
const pair = await factory.getPair(WKNYX, KUSD);
// 0x0000…0000 means the pair has not been createdComputing it locally requires INIT_CODE_PAIR_HASH — 0xf1bf8e2a6157730155748d7dda53c849265620eeef1bd1a3a9da5430eedc8fb6. Take it from the deployed factory, never a local compile.
Impermanent loss
If the price of one token moves relative to the other, withdrawing returns a different mix than you deposited, and often less value than simply holding. Fees offset this over time. It is inherent to constant-product AMMs, not a property of this implementation.
A real example
| Operation | Transaction | Block |
|---|---|---|
| Mint | 0x1a74abb90c2706f74ff7eb6f1207653309bcc0ef6907d147da3547f816c8d087 | 6,164,429 |
| Burn | 0xc3c16d5f9105ae7419892eb80fc91053418fd5216c23269330bfc4106a85f0a7 | 6,164,694 |