Skip to content

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.

NameKnyxSwap LPs
SymbolKNYX-LP
Decimals18

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 created

Computing it locally requires INIT_CODE_PAIR_HASH0xf1bf8e2a6157730155748d7dda53c849265620eeef1bd1a3a9da5430eedc8fb6. 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

OperationTransactionBlock
Mint0x1a74abb90c2706f74ff7eb6f1207653309bcc0ef6907d147da3547f816c8d0876,164,429
Burn0xc3c16d5f9105ae7419892eb80fc91053418fd5216c23269330bfc4106a85f0a76,164,694

KriptoNyx testnet — chain ID 3009 (kriptonyx_3009-1)