V2 pools
Good old constant-product pairs. If you've integrated a Uniswap V2 style DEX before, you already know this page. The router speaks the Router02 interface and pairs use the classic 0.3% fee math.
The Swap API already routes through V2 when V2 gives the best price. Come here when you specifically want one pool: LP tooling, arbitrage, a bot, or a contract that swaps on chain.
Addresses
| Contract | Address |
|---|---|
| Factory | 0x4680BCe1632824d30D2F53656dD610736c3e312e |
| Router | 0xF456737D17C2Bbb348fd4F7D1b000D62A46FB3b5 |
| WETH() on the router | wzkLTC 0x3153…A77e |
| Pair init code hash | 0x01888feb01db41d97ad6fb1883d7e286650d46c410b82338aeb4a37554c28bcd |
The router's ETH functions (swapExactETHForTokens, addLiquidityETH and so on) mean zkLTC here. Paths
always use wzkLTC.
The ABI bits you'll need
import { parseAbi } from 'viem'
export const ROUTER = '0xF456737D17C2Bbb348fd4F7D1b000D62A46FB3b5'
export const WZKLTC = '0x315374AA9b5536037Cc1Efeea2439CCC0913A77e'
export const routerAbi = parseAbi([
'function getAmountsOut(uint256 amountIn, address[] path) view returns (uint256[] amounts)',
'function getAmountsIn(uint256 amountOut, address[] path) view returns (uint256[] amounts)',
'function swapExactETHForTokens(uint256 amountOutMin, address[] path, address to, uint256 deadline) payable returns (uint256[] amounts)',
'function swapExactTokensForETH(uint256 amountIn, uint256 amountOutMin, address[] path, address to, uint256 deadline) returns (uint256[] amounts)',
'function swapExactTokensForTokens(uint256 amountIn, uint256 amountOutMin, address[] path, address to, uint256 deadline) returns (uint256[] amounts)',
'function addLiquidity(address tokenA, address tokenB, uint256 amountADesired, uint256 amountBDesired, uint256 amountAMin, uint256 amountBMin, address to, uint256 deadline) returns (uint256 amountA, uint256 amountB, uint256 liquidity)',
'function addLiquidityETH(address token, uint256 amountTokenDesired, uint256 amountTokenMin, uint256 amountETHMin, address to, uint256 deadline) payable returns (uint256 amountToken, uint256 amountETH, uint256 liquidity)',
'function removeLiquidity(address tokenA, address tokenB, uint256 liquidity, uint256 amountAMin, uint256 amountBMin, address to, uint256 deadline) returns (uint256 amountA, uint256 amountB)',
'function removeLiquidityETH(address token, uint256 liquidity, uint256 amountTokenMin, uint256 amountETHMin, address to, uint256 deadline) returns (uint256 amountToken, uint256 amountETH)',
])
export const pairAbi = parseAbi([
'function token0() view returns (address)',
'function token1() view returns (address)',
'function getReserves() view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast)',
'function totalSupply() view returns (uint256)',
'function balanceOf(address) view returns (uint256)',
])Get a price
getAmountsOut runs the pool math for a path. It's a free read.
import { createPublicClient, formatUnits, http, parseEther } from 'viem'
import { litvm } from './litvm'
import { ROUTER, WZKLTC, routerAbi } from './v2'
const client = createPublicClient({ chain: litvm, transport: http() })
const ZKUSDC = '0xdf69970B2fE416339187aA41D39882e864984CE9'
const amounts = await client.readContract({
address: ROUTER,
abi: routerAbi,
functionName: 'getAmountsOut',
args: [parseEther('0.01'), [WZKLTC, ZKUSDC]],
})
console.log(`0.01 zkLTC → ${formatUnits(amounts[1]!, 18)} ZKUSDC`)Swap zkLTC for a token
const amountIn = parseEther('0.01')
const [, quoted] = await client.readContract({
address: ROUTER,
abi: routerAbi,
functionName: 'getAmountsOut',
args: [amountIn, [WZKLTC, ZKUSDC]],
})
const minOut = (quoted! * 995n) / 1000n // 0.5% slippage
const deadline = BigInt(Math.floor(Date.now() / 1000) + 10 * 60) // 10 minutes
const hash = await wallet.writeContract({
account,
address: ROUTER,
abi: routerAbi,
functionName: 'swapExactETHForTokens',
args: [minOut, [WZKLTC, ZKUSDC], account, deadline],
value: amountIn,
})Selling a token instead? Approve the router for amountIn, then call swapExactTokensForETH or
swapExactTokensForTokens with the same minOut and deadline idea.
Add liquidity
// approve the router for the token side first
const tokenAmount = parseUnits('100', 18)
const zkltcAmount = parseEther('0.4')
await wallet.writeContract({
account,
address: ROUTER,
abi: routerAbi,
functionName: 'addLiquidityETH',
args: [
ZKUSDC,
tokenAmount,
(tokenAmount * 99n) / 100n, // min token, 1% wiggle room
(zkltcAmount * 99n) / 100n, // min zkLTC
account, // LP tokens go here
deadline,
],
value: zkltcAmount,
})The router adds at the pool's current ratio and refunds any extra zkLTC. To remove, approve the pair (the LP
token) to the router and call removeLiquidityETH or removeLiquidity.
Reading a pair
import { v2PairAddress } from './addresses' // see Contract addresses
const pair = v2PairAddress(WZKLTC, ZKUSDC)
const [token0, [reserve0, reserve1]] = await Promise.all([
client.readContract({ address: pair, abi: pairAbi, functionName: 'token0' }),
client.readContract({ address: pair, abi: pairAbi, functionName: 'getReserves' }),
])The math is the classic one, if you'd rather do it yourself:
function getAmountOut(amountIn: bigint, reserveIn: bigint, reserveOut: bigint) {
const withFee = amountIn * 997n
return (withFee * reserveOut) / (reserveIn * 1000n + withFee)
}From a contract
Same calls, from Solidity. Remember the pair, not the router, is where the reserves live.
interface IRouter02 {
function getAmountsOut(uint256 amountIn, address[] calldata path) external view returns (uint256[] memory);
function swapExactTokensForTokens(
uint256 amountIn,
uint256 amountOutMin,
address[] calldata path,
address to,
uint256 deadline
) external returns (uint256[] memory);
}amountOutMin = 0 means "I accept any price", and bots will happily give you a terrible one. Always quote first
and set a minimum.