# API Overview Source: https://docs.krokoswap.io/api/overview REST API for quotes, swaps, pools, tokens, and pricing # API Overview The Kroko DEX provides a REST API for swap routing, price quotes, pool data, and token information. The API handles complex routing logic off-chain and returns ready-to-use transaction calldata. ## Base URL ``` https://krokoswap.io/swap-api ``` ``` https://testnet.krokoswap.io/swap-api ``` All endpoints below are relative to the base URL above. ## Authentication No authentication is required. The API is currently publicly accessible. Authentication may be introduced in the future. If you are building a long-lived integration, check back here periodically for updates. ## Common Patterns ### Request Format * `GET` endpoints use query parameters * `POST` endpoints accept JSON body with `Content-Type: application/json` ### Response Format Successful responses return JSON directly. Error responses follow this format: ```json theme={null} { "error": "Human-readable error message" } ``` ### Token Amounts All token amounts are in **raw units** (wei). For a token with 18 decimals: | Human Amount | Raw Amount | | ------------ | ----------------------- | | 1.0 | `1000000000000000000` | | 0.5 | `500000000000000000` | | 100 | `100000000000000000000` | Convert with: `rawAmount = humanAmount × 10^decimals` ### Token Addresses Always use **checksummed or lowercase** ERC-20 addresses. For native KAS, use the **WKAS address** (not the zero address). ## Endpoints | Method | Endpoint | Description | | ------ | ------------------------------- | ----------------------------------- | | `GET` | [`/api/v1/quote`](/api/quote) | Get swap quote with optimal routing | | `POST` | [`/api/v1/swap`](/api/swap) | Generate Universal Router calldata | | `GET` | [`/api/v1/pools`](/api/pools) | List pools | | `GET` | [`/api/v1/tokens`](/api/tokens) | List tokens | ## Rate Limits There are currently no rate limits. This may change in the future — please use reasonable request frequencies. # Pools Source: https://docs.krokoswap.io/api/pools Query pool information and liquidity distribution # Pools API Query information about V3 liquidity pools. ## GET /api/v1/pools/v3/find Find all V3 pools for a specific token pair (one per fee tier). ### Parameters | Parameter | Type | Required | Description | | --------- | -------- | -------- | -------------------- | | `token0` | `string` | Yes | First token address | | `token1` | `string` | Yes | Second token address | ### Response ```json theme={null} [ { "address": "0x...", "fee": 500, "liquidity": "1234567890000000000", "sqrtPriceX96": "79228162514264337593543950336", "tick": 0, "price": "1.0" }, { "address": "0x...", "fee": 3000, "liquidity": "9876543210000000000", "sqrtPriceX96": "79228162514264337593543950336", "tick": 0, "price": "1.0" } ] ``` *** ## GET /api/v1/pools/:address/liquidity-distribution Get the tick-level liquidity distribution for a V3 pool. Used to render depth charts. ### Parameters | Parameter | Type | Required | Description | | --------- | --------------- | -------- | --------------- | | `address` | `string` (path) | Yes | V3 pool address | ### Response ```json theme={null} { "poolAddress": "0x...", "tickLiquidity": [ { "tick": -1000, "liquidityGross": "500000000000000000", "liquidityNet": "500000000000000000" }, { "tick": -940, "liquidityGross": "300000000000000000", "liquidityNet": "-300000000000000000" } ], "stats": { "totalLiquidity": "800000000000000000", "totalPositions": 42 } } ``` | Field | Description | | ---------------- | -------------------------------------------------------------------------------- | | `tick` | Tick index where liquidity changes | | `liquidityGross` | Total liquidity referencing this tick | | `liquidityNet` | Net liquidity change when crossing this tick (positive = add, negative = remove) | *** ## Additional Pool Endpoints | Endpoint | Description | | ------------------------------------------- | ----------------------- | | `GET /api/v1/pools/:address/price` | Current pool price | | `GET /api/v1/pools/:address/tvl` | Total value locked | | `GET /api/v1/pools/:address/volume` | Trading volume | | `GET /api/v1/pools/:address/apr` | Annual percentage rate | | `GET /api/v1/pools/:address/positions` | Active positions | | `GET /api/v1/pools/:address/tick-liquidity` | Raw tick liquidity data | # Quote Source: https://docs.krokoswap.io/api/quote Get optimal swap quotes with routing information # Quote API Get the best available swap quote for a token pair. The API finds the optimal route across V2 and V3 pools, including multi-hop paths. ## GET /api/v1/quote ### Parameters | Parameter | Type | Required | Description | | ----------- | -------- | ----------- | -------------------------------------------------------- | | `tokenIn` | `string` | Yes | Input token address | | `tokenOut` | `string` | Yes | Output token address | | `amountIn` | `string` | Conditional | Input amount in raw units (required when `tradeType=0`) | | `amountOut` | `string` | Conditional | Output amount in raw units (required when `tradeType=1`) | | `tradeType` | `string` | No | `0` = Exact Input (default), `1` = Exact Output | For native KAS, use the **WKAS address**, not the zero address. ### Response ```json theme={null} { "tokenIn": "0xb190...", "tokenOut": "0x3ac3...", "amountIn": "1000000000000000000", "amountOut": "1007249042881810956", "tradeType": 0, "route": { "path": ["0xb190...", "0x3ac3..."], "protocol": "v2", "hops": 1, "fees": [0] }, "priceImpact": 0.3, "gasCost": "100000", "executionPrice": 1.007249 } ``` | Field | Description | | ---------------- | --------------------------------------------------------------------------- | | `amountIn` | Input amount (user-specified for Exact Input, calculated for Exact Output) | | `amountOut` | Output amount (calculated for Exact Input, user-specified for Exact Output) | | `route.path` | Ordered list of token addresses in the swap path | | `route.protocol` | `"v2"`, `"v3"`, or `"mixed"` | | `route.hops` | Number of pools in the path | | `route.fees` | Fee tier for each hop (0 for V2, 100/500/3000/10000 for V3) | | `priceImpact` | Estimated price impact as a percentage | | `gasCost` | Estimated gas cost | | `executionPrice` | Output per input ratio | ### Examples ```bash Exact Input theme={null} # I want to sell 1 token — how much do I get? curl "https://dex.kasplex.org/swap-api/api/v1/quote?\ tokenIn=0xB190a6A7fC2873f1Abf145279eD664348d5Ef630&\ tokenOut=0x3Ac3B30b7f18AEFD4590D7FE4d9C5944aaeB7220&\ amountIn=1000000000000000000&\ tradeType=0" ``` ```bash Exact Output theme={null} # I want to buy 1 token — how much do I need? curl "https://dex.kasplex.org/swap-api/api/v1/quote?\ tokenIn=0xB190a6A7fC2873f1Abf145279eD664348d5Ef630&\ tokenOut=0x3Ac3B30b7f18AEFD4590D7FE4d9C5944aaeB7220&\ amountOut=1000000000000000000&\ tradeType=1" ``` ### Multi-Hop Routes When no direct pool exists or a multi-hop path provides better pricing, the API returns a multi-hop route: ```json theme={null} { "route": { "path": ["0xb190...", "0x0baf...", "0x3ac3..."], "protocol": "mixed", "hops": 2, "fees": [0, 500] } } ``` This means: token A → (V2 pool) → intermediate token → (V3 0.05% pool) → token B. # Swap Source: https://docs.krokoswap.io/api/swap Generate Universal Router calldata for swap execution # Swap API Generates ready-to-use transaction calldata for executing a swap via the Universal Router. The response can be sent directly as a transaction. ## POST /api/v1/swap ### Request Body ```json theme={null} { "tokenIn": "0xB190a6A7fC2873f1Abf145279eD664348d5Ef630", "tokenOut": "0x3Ac3B30b7f18AEFD4590D7FE4d9C5944aaeB7220", "amountIn": "1000000000000000000", "tradeType": 0, "slippage": 0.5, "recipient": "0xYourAddress", "deadline": 1200 } ``` | Field | Type | Required | Description | | ----------- | -------- | ----------- | --------------------------------------------------- | | `tokenIn` | `string` | Yes | Input token address | | `tokenOut` | `string` | Yes | Output token address | | `amountIn` | `string` | Conditional | Input amount (required when `tradeType=0`) | | `amountOut` | `string` | Conditional | Output amount (required when `tradeType=1`) | | `tradeType` | `number` | No | `0` = Exact Input (default), `1` = Exact Output | | `slippage` | `number` | No | Slippage tolerance as percentage (default: `0.5`) | | `recipient` | `string` | Yes | Address to receive output tokens | | `deadline` | `number` | No | Seconds until expiration (default: `1200` = 20 min) | ### Response ```json theme={null} { "to": "0x440d7f5FE865eFCcfdCB1ee9a000C114163689ba", "data": "0x3593564c...", "value": "0", "gasEstimate": "100000", "tradeType": 0, "quote": { "tokenIn": "0xb190...", "tokenOut": "0x3ac3...", "amountIn": "1000000000000000000", "amountOut": "1007249042881810956", "minAmountOut": "1002212797667401901", "priceImpact": 0.3, "protocol": "v2", "path": ["0xb190...", "0x3ac3..."], "fees": [0], "hops": 1 }, "deadline": "1764573986", "slippage": 0.5, "permit2": "0xc320bc492Bb56169aBE18D3C0a2048c45febC897" } ``` | Field | Description | | -------------------- | --------------------------------------------------------------------------- | | `to` | Universal Router address — use as transaction `to` | | `data` | Encoded calldata — use as transaction `data` | | `value` | Native KAS amount — use as transaction `value` (non-zero when input is KAS) | | `gasEstimate` | Estimated gas units | | `quote.minAmountOut` | Minimum output (Exact Input) — on-chain slippage protection | | `quote.maxAmountIn` | Maximum input (Exact Output) — on-chain slippage protection | | `permit2` | Permit2 contract address | ### Slippage Protection | Trade Type | Field | Formula | | ---------------- | -------------- | ---------------------------------- | | Exact Input (0) | `minAmountOut` | `amountOut × (1 - slippage / 100)` | | Exact Output (1) | `maxAmountIn` | `amountIn × (1 + slippage / 100)` | These limits are enforced **on-chain** by the Universal Router. The transaction reverts if the actual execution exceeds the slippage bounds. ### Usage ```typescript theme={null} // 1. Get calldata const res = await fetch('/swap-api/api/v1/swap', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tokenIn, tokenOut, amountIn, tradeType: 0, slippage: 0.5, recipient: userAddress, deadline: 1200 }) }); const swapData = await res.json(); // 2. Send transaction const tx = await signer.sendTransaction({ to: swapData.to, data: swapData.data, value: swapData.value }); ``` ### Examples ```json Exact Input Request theme={null} { "tokenIn": "0xB190a6A7fC2873f1Abf145279eD664348d5Ef630", "tokenOut": "0x3Ac3B30b7f18AEFD4590D7FE4d9C5944aaeB7220", "amountIn": "1000000000000000000", "tradeType": 0, "slippage": 0.5, "recipient": "0xUserAddress", "deadline": 1200 } ``` ```json Exact Output Request theme={null} { "tokenIn": "0xB190a6A7fC2873f1Abf145279eD664348d5Ef630", "tokenOut": "0x3Ac3B30b7f18AEFD4590D7FE4d9C5944aaeB7220", "amountOut": "1000000000000000000", "tradeType": 1, "slippage": 0.5, "recipient": "0xUserAddress", "deadline": 1200 } ``` # Tokens Source: https://docs.krokoswap.io/api/tokens Token list and search endpoints # Tokens API Query the list of tokens available on Kroko DEX, ranked by trading activity. ## GET /api/v1/tokens2 Returns the token list ranked by trading activity (total transaction count across V2 pairs and V3 pools). ### Parameters | Parameter | Type | Required | Description | | --------- | -------- | -------- | -------------------------------------------------- | | `limit` | `number` | No | Number of tokens to return (1–500, default: `100`) | ### Response ```json theme={null} { "tokens": [ { "address": "0x2c2Ae87Ba178F48637acAe54B87c3924F544a83e", "symbol": "WKAS", "name": "Wrapped KAS", "decimals": 18, "logoURI": "https://..." }, { "address": "0xB190a6A7fC2873f1Abf145279eD664348d5Ef630", "symbol": "HUB", "name": "Hub Token", "decimals": 18, "logoURI": null } ] } ``` | Field | Description | | ---------- | ------------------------------------ | | `address` | Token contract address (checksummed) | | `symbol` | Token ticker symbol | | `name` | Full token name | | `decimals` | Number of decimal places | | `logoURI` | Token icon URL (nullable) | ### Ranking Logic Tokens are ranked by total `txCount` aggregated across all V2 pairs and V3 pools they participate in. Pinned tokens (with admin-set priority) appear first regardless of activity. *** ## GET /api/v1/tokens2/search Search tokens by symbol or name. Useful for token selector UIs. ### Parameters | Parameter | Type | Required | Description | | --------- | -------- | -------- | ------------------------------- | | `q` | `string` | Yes | Search query (case-insensitive) | | `limit` | `number` | No | Max results (default: `10`) | ### Response Same format as `/api/v1/tokens2`. Searches both `symbol` and `name` fields using substring matching. ### Example ```bash theme={null} # Search for tokens with "SUN" in the name or symbol curl "https://dex.kasplex.org/api/v1/tokens2/search?q=SUN&limit=10" ``` ```json theme={null} { "tokens": [ { "address": "0x1234...", "symbol": "SUN", "name": "Sun Token", "decimals": 18, "logoURI": "https://..." } ] } ``` *** ## Token Address Discovery If you have a token contract address but it's not in the API: 1. **Search by address** is not currently supported via the API 2. **Read on-chain**: Call `symbol()`, `name()`, and `decimals()` on the ERC-20 contract directly 3. **Import**: Contact the team to have the token added to the official list # Glossary Source: https://docs.krokoswap.io/concepts/glossary Key terms used throughout the Kroko DEX documentation # Glossary ## A **AMM (Automated Market Maker)** A smart contract that holds token reserves and enables trading without an order book. Prices are determined algorithmically based on the ratio of reserves. ## C **Concentrated Liquidity** V3 feature where LPs provide liquidity within a specific price range instead of across all prices. Improves capital efficiency. **Constant Product Formula** The equation `x * y = k` used by V2 pools, where `x` and `y` are token reserves and `k` is a constant that only increases from fees. ## D **Deadline** A Unix timestamp after which a transaction automatically reverts. Protects users from delayed execution at stale prices. **dexAddress** The token address used for DEX operations. For native KAS, this is the WKAS contract address. For ERC-20 tokens, it equals the token's contract address. ## E **Exact Input** A trade type where the user specifies the input amount and the protocol calculates the output. "I want to sell exactly X tokens." **Exact Output** A trade type where the user specifies the desired output amount and the protocol calculates the required input. "I want to buy exactly Y tokens." ## F **Fee Tier** One of four selectable fee rates for V3 pools: 0.01%, 0.05%, 0.3%, or 1%. ## I **Impermanent Loss** The loss in value that LPs experience when the relative price of pooled tokens changes compared to simply holding them. Called "impermanent" because it reverses if prices return to the original ratio. ## K **KAS** The native currency of the Kasplex blockchain (18 decimals). Used for gas fees and value transfer. Wrapped as WKAS for DEX operations. ## L **Liquidity** In V3, a measure of the depth a position provides. Higher liquidity means less price impact for swaps through that range. Denoted as `L`. **LP (Liquidity Provider)** A user who deposits tokens into a pool to enable trading and earn fees. **LP Token** A fungible ERC-20 token (V2) representing a share of a pool's reserves. ## M **Multi-hop** A swap routed through multiple pools. For example, A → B → C uses two hops. ## N **NFT Position** An ERC-721 token (V3) representing a unique liquidity position with specific tick range and liquidity amount. ## P **Permit2** A shared token approval contract that manages allowances with expiration and per-spender scoping. **Pool** A smart contract holding reserves of two tokens. V2 pools use constant product; V3 pools support concentrated liquidity. **Price Impact** The change in price caused by a trade. Larger trades relative to pool reserves cause greater price impact. ## S **Slippage** The difference between the expected price and the actual execution price. Slippage tolerance sets the maximum acceptable deviation. **sqrtPriceX96** V3's on-chain price encoding: the square root of the price multiplied by 2^96. Enables efficient fixed-point arithmetic. ## T **Tick** A discrete price point in V3. Each tick `i` represents price `1.0001^i`. Positions must align to tick spacing boundaries. **Tick Spacing** The minimum distance between usable ticks, determined by fee tier. For example, a 0.3% fee pool has tick spacing of 60. ## U **Universal Router** A single contract that executes swaps across V2 and V3 pools, handling multi-hop and mixed-protocol routes. ## W **WKAS (Wrapped KAS)** An ERC-20 token pegged 1:1 to native KAS. Required for DEX smart contract interactions. The Universal Router handles wrapping/unwrapping automatically. # Native Token Handling Source: https://docs.krokoswap.io/concepts/native-token How KAS and WKAS work in the DEX # Native Token Handling **KAS** is the native currency of the Kasplex blockchain (similar to ETH on Ethereum). Since AMM smart contracts operate exclusively with ERC-20 tokens, KAS is wrapped as **WKAS** (Wrapped KAS) for on-chain operations. ## KAS vs WKAS | | KAS | WKAS | | ----------------- | ----------------------- | ----------------------- | | Type | Native currency | ERC-20 token | | Used for | Gas fees, transfers | DEX operations | | Address (display) | `0x0000...0000` | Contract address | | Decimals | 18 | 18 | | Conversion | 1 KAS = 1 WKAS (always) | 1 WKAS = 1 KAS (always) | ## WKAS Contract Addresses | Network | WKAS Address | | ------- | -------------------------------------------- | | Mainnet | `0x2c2Ae87Ba178F48637acAe54B87c3924F544a83e` | | Testnet | `0xC065C62a10fB363fD31CA394D632C4Df106566df` | ## How Wrapping Works WKAS is a simple deposit/withdraw contract: * **Wrap**: Send KAS to the WKAS contract → receive equal WKAS * **Unwrap**: Call `withdraw()` on WKAS → receive equal KAS The Universal Router handles this automatically during swaps. Users never need to wrap or unwrap manually. ## Token Representation In the Kroko DEX system, tokens have two address fields: | Field | KAS Value | ERC-20 Value | Used For | | ------------ | --------------- | ---------------------- | -------------------------------------------------------- | | `address` | `0x0000...0000` | Token contract address | Display, storage keys, balance queries | | `dexAddress` | WKAS address | Same as `address` | All DEX operations (swaps, quotes, liquidity, approvals) | ### When Calling APIs Always use the **WKAS address** (not the zero address) when calling the Quote or Swap APIs with KAS as input or output: ```typescript theme={null} // Correct: use WKAS address for KAS const WKAS = "0x2c2Ae87Ba178F48637acAe54B87c3924F544a83e"; fetch(`/api/v1/quote?tokenIn=${WKAS}&tokenOut=${USDC}&amountIn=1000000000000000000`); // Wrong: zero address will not work fetch(`/api/v1/quote?tokenIn=0x0000000000000000000000000000000000000000&...`); ``` ### Sending Native KAS in Swaps When KAS is the input token, include the amount as the transaction `value`: ```typescript theme={null} const tx = await signer.sendTransaction({ to: swapData.to, data: swapData.data, value: swapData.value // Non-zero when selling KAS }); ``` The Swap API automatically sets the `value` field when it detects WKAS as the input token. # Protocol Overview Source: https://docs.krokoswap.io/concepts/overview Architecture and design of the Kroko DEX # Protocol Overview Kroko DEX is a decentralized exchange on the Kasplex blockchain that combines two proven AMM models — **V2 (constant product)** and **V3 (concentrated liquidity)** — under a unified routing layer. ## Architecture ```mermaid theme={null} flowchart TD A[User Wallet] --> B[Permit2] B --> C[Universal Router] C --> D[V2 Pool] C --> E[V3 Pool] F[Swap API] -.->|calldata| C style C fill:#00D1D9,color:#fff style F fill:#f5f5f5,stroke:#999,stroke-dasharray: 5 5 ``` ### Components | Component | Role | | -------------------- | ----------------------------------------------------------------------------------------------------------------- | | **V2 Pools** | Constant-product AMM (`x * y = k`). Simple, gas-efficient, full-range liquidity. | | **V3 Pools** | Concentrated liquidity AMM. LPs choose a price range, improving capital efficiency. | | **Universal Router** | Executes swaps across V2 and V3 in a single transaction. Supports multi-hop and split routes. | | **Permit2** | Token approval manager. Users approve tokens once to Permit2, then grant per-spender permissions with expiration. | | **Swap API** | Off-chain routing engine that finds optimal paths and generates transaction calldata. | ## How a Swap Works 1. **Approve** — User approves their token to the Permit2 contract (one-time per token) 2. **Permit** — User grants the Universal Router permission via Permit2 (one-time per token) 3. **Quote** — Frontend requests an optimal route from the Swap API 4. **Calldata** — Frontend requests encoded transaction data from the Swap API 5. **Execute** — User sends the transaction to the Universal Router The routing engine automatically selects the best path — single-hop or multi-hop, V2 or V3 or mixed — based on available liquidity and price impact. ## V2 vs V3 | Feature | V2 | V3 | | ------------------ | -------------------------- | -------------------------------------- | | Liquidity range | Full range (0 to infinity) | Custom price range | | Fee | Fixed 0.3% | Selectable: 0.01%, 0.05%, 0.3%, 1% | | LP token | Fungible ERC-20 | Non-fungible ERC-721 (NFT) | | Capital efficiency | Lower | Higher (up to 4000x for narrow ranges) | | Complexity | Simple | Advanced | | Best for | Stable pairs, passive LPs | Active LPs seeking higher returns | ## Native Token Kasplex's native currency is **KAS** (18 decimals). Since AMM contracts require ERC-20 tokens, KAS is wrapped as **WKAS** for on-chain operations. The Universal Router handles wrapping and unwrapping automatically — users interact with native KAS directly. See [Native Token Handling](/concepts/native-token) for details. # Permit2 Source: https://docs.krokoswap.io/concepts/permit2 Unified token approval system for secure and efficient allowance management # Permit2 **Permit2** is a token approval contract that acts as a shared allowance manager. Instead of approving each DEX contract individually, users approve tokens to Permit2 once, then grant fine-grained permissions to specific spenders (like the Universal Router). ## Why Permit2? Traditional ERC-20 approvals have several problems: | Problem | Permit2 Solution | | ----------------------------------------- | ------------------------------------------------------- | | Unlimited approvals to every dApp | Single approval to Permit2, then scoped sub-permissions | | No expiration on approvals | Sub-permissions have configurable expiration | | Each new dApp needs a new approval tx | Permit2 already approved, only a sub-permission needed | | Revocation requires per-dApp transactions | Revoke Permit2 approval to cut off all dApps at once | ## How It Works ``` Step 1 (one-time per token): Token ──approve──▶ Permit2 Step 2 (one-time per token per spender): Permit2 ──approve──▶ Universal Router (with amount limit + expiration) Step 3 (every swap): Universal Router uses Permit2 to transfer tokens ``` ### Step 1: Approve Token to Permit2 Standard ERC-20 `approve()`. Only needs to happen once per token. ```solidity theme={null} token.approve(PERMIT2, type(uint256).max); ``` ### Step 2: Grant Permission via Permit2 Call `Permit2.approve()` to let the Universal Router spend your token through Permit2: ```solidity theme={null} permit2.approve( tokenAddress, // Which token UNIVERSAL_ROUTER, // Who can spend it type(uint160).max, // Amount limit expiration // When this permission expires (unix timestamp) ); ``` ### Step 3: Swap Execution When the Universal Router executes a swap, it pulls tokens from the user via Permit2 — no further user interaction required. ## Checking Allowances Query existing permissions: ```solidity theme={null} (uint160 amount, uint48 expiration, uint48 nonce) = permit2.allowance( owner, // User address tokenAddress, // Token spender // e.g., Universal Router ); ``` A swap will fail if: * `amount` is less than the swap input * `expiration` is in the past ## Security Benefits * **Scoped permissions**: Each spender gets its own allowance with its own expiration * **Single revocation point**: Revoking the ERC-20 approval to Permit2 disables all downstream spenders * **Nonce tracking**: Prevents replay attacks * **Expiration**: Permissions automatically become invalid after the set time # Universal Router Source: https://docs.krokoswap.io/concepts/universal-router Unified entry point for executing swaps across V2 and V3 protocols # Universal Router The **Universal Router** is a single smart contract that can execute swaps across both V2 and V3 pools. It serves as the unified entry point for all trading operations on Kroko DEX. ## Why a Universal Router? Without it, swapping through V2 and V3 would require separate contracts and separate transactions. The Universal Router: * **Unifies execution** — One contract handles V2, V3, and mixed-protocol swaps * **Enables complex routes** — Multi-hop swaps that cross between V2 and V3 pools * **Reduces gas** — Batches operations in a single transaction * **Integrates Permit2** — Pulls tokens via Permit2 for streamlined approvals ## How It Works The Universal Router receives encoded **commands** and **inputs** via its `execute()` function: ```solidity theme={null} function execute( bytes calldata commands, bytes[] calldata inputs, uint256 deadline ) external payable; ``` * **commands**: A byte string where each byte is a command code (e.g., V2\_SWAP\_EXACT\_IN, V3\_SWAP\_EXACT\_IN, WRAP\_ETH, UNWRAP\_WETH, etc.) * **inputs**: ABI-encoded parameters for each command * **deadline**: Unix timestamp after which the transaction reverts ## Typical Swap Flow ``` 1. Swap API finds the optimal route 2. Swap API encodes the route as Universal Router commands 3. Frontend receives { to, data, value } from the API 4. User signs and sends the transaction 5. Universal Router executes the commands sequentially ``` Developers do **not** need to encode commands manually — the Swap API handles this. You simply forward the response: ```typescript theme={null} const tx = await signer.sendTransaction({ to: swapData.to, // Universal Router address data: swapData.data, // Encoded execute() calldata value: swapData.value // Native KAS amount (0 for ERC-20 to ERC-20) }); ``` ## Slippage Protection The encoded calldata includes on-chain slippage checks: | Trade Type | Protection | Description | | ------------ | -------------- | ---------------------------------- | | Exact Input | `minAmountOut` | Reverts if output is below minimum | | Exact Output | `maxAmountIn` | Reverts if input exceeds maximum | These checks are enforced by the Universal Router on-chain, not just the API. ## Native KAS Handling When a swap involves native KAS (not WKAS), the Universal Router automatically: * **Selling KAS**: Wraps the KAS sent as `msg.value` into WKAS before the swap * **Buying KAS**: Unwraps WKAS to KAS and sends it to the recipient after the swap The `value` field in the Swap API response is non-zero when native KAS is the input token. # V2 Fees Source: https://docs.krokoswap.io/concepts/v2/fees Fee structure for V2 constant product pools # V2 Fees All V2 pools charge a **fixed 0.3% fee** on every swap. This fee is not configurable per pool. ## How Fees Work The fee is applied to the input token before the swap calculation: 1. Trader sends `amountIn` of token A 2. The effective input is `amountIn × 0.997` (0.3% deducted) 3. The output amount is calculated using the reduced input 4. The fee portion remains in the pool, increasing `k` ``` Input: 1000 tokens Fee: 1000 × 0.003 = 3 tokens Effective input: 997 tokens → used for swap calculation ``` ## Fee Accumulation Fees are not distributed separately — they accumulate directly in the pool reserves. This means: * `k` grows over time as fees are collected * LP token value increases proportionally * LPs claim their fees when they withdraw liquidity (burn LP tokens) ## Example A pool with 100,000 A / 100,000 B reserves: | Day | Swap Volume | Fees Collected | New k | | --- | ----------- | -------------- | --------------- | | 1 | 10,000 A | 30 A | Slightly higher | | 2 | 50,000 B | 150 B | Higher still | Over time, the pool reserves grow from accumulated fees, benefiting all LP token holders proportionally to their share. ## Comparison with V3 | | V2 | V3 | | -------------- | --------------------------------- | ------------------------------------- | | Fee rate | Fixed 0.3% | Selectable (0.01%, 0.05%, 0.3%, 1%) | | Fee collection | Auto-compounded into reserves | Collected separately, must be claimed | | Fee per LP | Distributed evenly across all LPs | Only earned by in-range positions | # How V2 Works Source: https://docs.krokoswap.io/concepts/v2/how-v2-works Constant product AMM — the foundation of decentralized trading # How V2 Works V2 pools use the **constant product formula**, the same model pioneered by Uniswap V2. Each pool holds reserves of exactly two tokens and maintains the invariant: $$ x \times y = k $$ Where: * `x` = reserve of token A * `y` = reserve of token B * `k` = constant (increases only from fees) ## Price Determination The price of token A in terms of token B is simply the ratio of reserves: $$ \text{Price}_A = \frac{y}{x} $$ When a trader swaps token A for token B, they deposit A and withdraw B. The new reserves must still satisfy `x * y = k` (after fees), which naturally adjusts the price. ## Swap Mechanics For a swap of `dx` amount of token A into the pool: 1. The input amount minus the 0.3% fee is added to reserve A 2. The output amount `dy` is calculated such that the invariant holds: $$ (x + dx \times 0.997) \times (y - dy) = k $$ Solving for `dy`: $$ dy = \frac{y \times dx \times 0.997}{x + dx \times 0.997} $$ ## Price Impact Larger trades relative to pool reserves cause greater price movement. This is called **price impact** and is inherent to the constant product curve — the price moves along a hyperbola. For a trade of size `dx` against reserve `x`: $$ \text{Price Impact} \approx \frac{dx}{x + dx} $$ A trade that equals 1% of the reserve causes roughly 1% price impact. ## Liquidity Provision LPs deposit both tokens in proportion to the current reserves. In return, they receive **LP tokens** representing their share of the pool. When adding liquidity: * If the pool exists: tokens must be deposited at the current price ratio * If the pool is new: the first depositor sets the initial price The LP's share of the pool is: $$ \text{Share} = \frac{\text{LP tokens minted}}{\text{Total LP token supply}} $$ See [LP Tokens](/concepts/v2/lp-tokens) for more details. ## Arbitrage and Price Discovery V2 pools rely on arbitrageurs to keep prices in line with external markets. When the pool price deviates from the true market price, arbitrageurs profit by trading against the pool, pushing the price back to equilibrium. # LP Tokens Source: https://docs.krokoswap.io/concepts/v2/lp-tokens Fungible liquidity provider tokens in V2 pools # LP Tokens When you add liquidity to a V2 pool, you receive **LP tokens** — fungible ERC-20 tokens that represent your proportional share of the pool. ## Minting LP tokens are minted when you deposit both tokens into a pool. **First deposit** (new pool): $$ \text{LP minted} = \sqrt{amount_0 \times amount_1} - \text{MINIMUM\_LIQUIDITY} $$ A small amount (`MINIMUM_LIQUIDITY = 1000 wei`) is permanently locked to prevent division-by-zero attacks. **Subsequent deposits**: $$ \text{LP minted} = \min\left(\frac{amount_0 \times \text{totalSupply}}{reserve_0},\; \frac{amount_1 \times \text{totalSupply}}{reserve_1}\right) $$ You must deposit tokens at the current reserve ratio. Any excess of one token is refunded. ## Burning To withdraw liquidity, you burn your LP tokens and receive both tokens back: $$ amount_0 = \frac{\text{LP burned}}{\text{totalSupply}} \times reserve_0 $$ $$ amount_1 = \frac{\text{LP burned}}{\text{totalSupply}} \times reserve_1 $$ Because fees accumulate in reserves, the amount you receive will be greater than what you deposited (assuming trading volume occurred). ## Properties | Property | Description | | ------------ | ----------------------------------------------------- | | Standard | ERC-20 | | Transferable | Yes — can be sent, traded, or used in other protocols | | Supply | Increases with deposits, decreases with withdrawals | | Value | Backed by the pool's token reserves | ## Impermanent Loss LP tokens are subject to **impermanent loss** — when the relative price of the two tokens changes from the time of deposit, the LP's position is worth less than simply holding both tokens. The loss is "impermanent" because it reverses if the price returns to the original ratio. The magnitude of impermanent loss depends on the price change: | Price Change | Impermanent Loss | | ------------ | ---------------- | | 1.25x | 0.6% | | 1.50x | 2.0% | | 2x | 5.7% | | 3x | 13.4% | | 5x | 25.5% | Trading fees earned by the position may offset impermanent loss, depending on volume. # Concentrated Liquidity Source: https://docs.krokoswap.io/concepts/v3/concentrated-liquidity How V3 enables capital-efficient liquidity provision # Concentrated Liquidity V3 introduces **concentrated liquidity** — LPs choose a specific price range `[priceLower, priceUpper]` in which to provide liquidity, rather than spreading it across the entire price spectrum (0 to infinity) as in V2. ## The Core Idea In V2, most liquidity sits idle. If ETH/USDC trades at $3,000, liquidity at $1 or \$100,000 is never used. V3 solves this by letting LPs concentrate their capital where it matters. ```mermaid theme={null} --- config: xyChart: xAxis: label: Price yAxis: label: Liquidity --- xychart-beta x-axis "Price" ["$0", "$500", "$1k", "$1.5k", "$2k", "$2.5k", "$3k", "$3.5k", "$4k", "$4.5k", "$5k"] y-axis "Liquidity" bar "V2 (full range)" [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30] bar "V3 (concentrated)" [0, 0, 0, 0, 0, 80, 100, 80, 0, 0, 0] ``` ## Capital Efficiency A V3 LP providing liquidity in the range `[$2,500, $3,500]` achieves the same depth as a V2 LP with **\~4.24x more capital**. For tighter ranges, the multiplier is even higher. $$ \text{Capital Efficiency} = \frac{\sqrt{p_{\text{upper}}}}{\sqrt{p_{\text{upper}}} - \sqrt{p_{\text{lower}}}} $$ | Range (relative to current price) | Efficiency vs V2 | | --------------------------------- | ---------------- | | Full range | 1x (same as V2) | | ±50% | \~3.5x | | ±10% | \~17x | | ±1% | \~170x | ## How It Works Under the hood, V3 uses the same `x * y = k` formula, but applies it only within each LP's chosen range using a **virtual reserves** model. Within a position's range: $$ (x + \frac{L}{\sqrt{p_{\text{upper}}}}) \times (y + L \times \sqrt{p_{\text{lower}}}) = L^2 $$ Where `L` is the position's liquidity — a measure of the depth the position provides. ## Active vs Inactive Liquidity * **In-range**: The current price is within the position's range. The position earns fees. * **Out-of-range**: The current price has moved outside the position's range. The position holds only one token and earns no fees until the price re-enters. When the price crosses a position's boundary: * **Upper bound crossed**: The position becomes 100% token0 (no token1 remaining) * **Lower bound crossed**: The position becomes 100% token1 (no token0 remaining) ## Single-Sided Liquidity You can create positions entirely above or below the current price: * **Range above current price**: Deposit only token0. Acts like a limit sell order. * **Range below current price**: Deposit only token1. Acts like a limit buy order. ## Trade-offs | Advantage | Trade-off | | --------------------------------------- | ------------------------------------------- | | Higher capital efficiency | Requires active management | | Higher fee earnings per unit of capital | Risk of position going out of range | | Limit-order-like behavior | More complex than V2 | | Custom strategies per LP | Impermanent loss amplified in narrow ranges | # Fee Tiers Source: https://docs.krokoswap.io/concepts/v3/fee-tiers Selectable fee levels for V3 pools # Fee Tiers V3 pools support four fee tiers, allowing the market to create pools optimized for different asset types. ## Available Tiers | Fee Tier | Fee Rate | Tick Spacing | Best For | | -------- | -------- | ------------ | ---------------------------------- | | 0.01% | 1 bps | 1 | Stablecoin pairs (e.g., USDC/USDT) | | 0.05% | 5 bps | 10 | Correlated assets | | 0.3% | 30 bps | 60 | Most pairs (default) | | 1% | 100 bps | 200 | Exotic / long-tail pairs | **bps** = basis points. 1 bps = 0.01%. ## Choosing a Fee Tier The fee tier affects both the swap cost for traders and the revenue for LPs: * **Lower fees** attract more trading volume but pay LPs less per trade * **Higher fees** compensate LPs more per trade but may deter volume ### Guidelines | Asset Type | Recommended Tier | | ----------------------- | ---------------- | | Stablecoin ↔ Stablecoin | 0.01% | | Blue chip ↔ Stablecoin | 0.05% or 0.3% | | Established tokens | 0.3% | | New or volatile tokens | 1% | ## Tick Spacing Each fee tier has a fixed **tick spacing** that determines the granularity of price points where liquidity can be added or removed. * Lower tick spacing = more precise price ranges but higher gas costs * Higher tick spacing = less precise ranges but lower gas costs The tick spacing is directly tied to the fee tier and cannot be configured independently. See [Ticks and Ranges](/concepts/v3/ticks-and-ranges) for details. ## Multiple Pools Per Pair The same token pair can have multiple V3 pools, one for each fee tier. The routing engine automatically selects the pool(s) with the best execution price for each swap. For example, KAS/USDC might have: * A 0.05% pool with deep liquidity for large trades * A 0.3% pool with moderate liquidity * A 1% pool for small speculative trades Liquidity providers choose which pool(s) to provide liquidity to based on their strategy and risk tolerance. # Positions Source: https://docs.krokoswap.io/concepts/v3/positions NFT-based liquidity positions in V3 # Positions In V3, each liquidity position is represented by a **non-fungible token (ERC-721 NFT)**. Unlike V2's fungible LP tokens, each V3 position is unique because it encodes a specific price range and liquidity amount. ## Position Properties Each position NFT stores: | Property | Description | | ----------- | ---------------------------------------- | | `token0` | Address of the lower-sorted token | | `token1` | Address of the higher-sorted token | | `fee` | Pool fee tier (100, 500, 3000, or 10000) | | `tickLower` | Lower bound of the price range | | `tickUpper` | Upper bound of the price range | | `liquidity` | Amount of liquidity provided | ## Position Lifecycle ### 1. Mint (Create) Create a new position by calling the Position Manager with: * Token pair and fee tier (identifies the pool) * Tick range (`tickLower`, `tickUpper`) * Desired token amounts The Position Manager mints an NFT and returns the `tokenId`. ### 2. Increase Liquidity Add more liquidity to an existing position without changing the range. The additional tokens must match the current price ratio within your range. ### 3. Decrease Liquidity Remove some or all liquidity from a position. This converts liquidity back to tokens, which are held by the Position Manager until collected. ### 4. Collect Fees Claim accumulated trading fees. Fees accrue in the exact tokens of the pool and must be explicitly collected — they do not auto-compound like V2. ### 5. Burn After removing all liquidity and collecting all fees, the NFT can be burned. This is optional — an empty position NFT has no economic value. ## Position States A position can be in one of three states depending on the current pool price: | State | Condition | Composition | Earning Fees? | | ----------- | ------------------------------------- | ------------------------ | ------------- | | In range | `tickLower < currentTick < tickUpper` | Mix of token0 and token1 | Yes | | Above range | `currentTick ≥ tickUpper` | 100% token0 | No | | Below range | `currentTick ≤ tickLower` | 100% token1 | No | ## Fee Calculation Fees earned by a position are proportional to: 1. **Liquidity share** — The position's liquidity relative to the total active liquidity at each tick 2. **Time in range** — Fees only accrue when the position is in range 3. **Trading volume** — More trades through the position's range means more fees $$ \text{Fees earned} \propto \frac{L_{\text{position}}}{L_{\text{total at tick}}} \times \text{Volume through ticks} $$ ## Key Differences from V2 | | V2 LP Token | V3 Position NFT | | -------------- | ----------------------------- | ------------------------- | | Token standard | ERC-20 | ERC-721 | | Fungible | Yes | No | | Price range | Full range (implicit) | Custom range (explicit) | | Fee collection | Auto-compounded | Manual claim | | Composability | Easy (fungible, transferable) | Possible but more complex | # Ticks and Ranges Source: https://docs.krokoswap.io/concepts/v3/ticks-and-ranges How V3 discretizes prices into ticks for concentrated liquidity # Ticks and Ranges V3 uses a **tick** system to discretize the continuous price space. Each tick represents a specific price point, and liquidity positions are defined by a range of ticks. ## Ticks A tick `i` maps to a price via: $$ p(i) = 1.0001^i $$ Each tick represents a **0.01% (1 basis point)** price change from the adjacent tick. Ticks are integers ranging from `-887272` to `887272`. | Tick | Price | | ------ | ------- | | 0 | 1.0 | | 1 | 1.0001 | | 100 | 1.01005 | | 10000 | 2.71828 | | -10000 | 0.36788 | ## Tick Spacing Not every tick is usable — positions must align to **tick spacing** boundaries determined by the pool's fee tier: | Fee Tier | Tick Spacing | Price Granularity | | -------- | ------------ | ----------------- | | 0.01% | 1 | Every 0.01% | | 0.05% | 10 | Every 0.10% | | 0.3% | 60 | Every 0.60% | | 1% | 200 | Every 2.02% | When creating a position, `tickLower` and `tickUpper` must be multiples of the pool's tick spacing. ## sqrtPriceX96 On-chain, prices are stored as **sqrtPriceX96** — the square root of the price multiplied by 2^96: $$ \text{sqrtPriceX96} = \sqrt{p} \times 2^{96} $$ This encoding enables efficient fixed-point arithmetic without floating-point operations. To convert back to a human-readable price: $$ p = \left(\frac{\text{sqrtPriceX96}}{2^{96}}\right)^2 $$ ## Price Ranges A liquidity position is defined by `[tickLower, tickUpper]`: ```mermaid theme={null} flowchart LR A["tickLower"] --- B["◀ Position earns fees in this range ▶"] B --- C["tickUpper"] D(("currentTick")) style A fill:#f59e0b,color:#fff style C fill:#f59e0b,color:#fff style B fill:#10b981,color:#fff style D fill:#ef4444,color:#fff ``` ### Converting Between Prices and Ticks **Price to tick**: $$ i = \lfloor \log_{1.0001}(p) \rfloor $$ Then round to the nearest usable tick (multiple of tick spacing). **Tick to price**: $$ p = 1.0001^i $$ ### Token0 and Token1 V3 defines prices as **token1 per token0** (`price = token1/token0`). The token with the lower address is always token0. For a KAS/USDC pool where KAS is token0: * `price = 0.05` means 1 KAS = 0.05 USDC * `tick ≈ -29959` ## Nearest Usable Tick When specifying a price range, the exact tick might not align with the pool's tick spacing. Use the **nearest usable tick**: ``` nearestUsableTick = round(tick / tickSpacing) × tickSpacing ``` The API and SDK handle this conversion automatically. # Contract Addresses Source: https://docs.krokoswap.io/contracts/addresses Deployed contract addresses on Kasplex Mainnet and Testnet # Contract Addresses All Kroko DEX smart contracts are deployed on both Kasplex Mainnet and Testnet. | Contract | Address | | ----------------------- | -------------------------------------------- | | **Permit2** | `0x2E1987F680FD7Bc8B33d3Bf94f12B988A0B50034` | | **Universal Router** | `0xefeCc1c2dE3BfE4C6D43030F2AcDD5C3cE279024` | | **WKAS** | `0x2c2Ae87Ba178F48637acAe54B87c3924F544a83e` | | **V2 Factory** | `0x4373b7Fcf5059A785843cD224129e01d243Aef71` | | **V2 Router** | `0xC7ca845B8302346e1C7227f03bb9EFb35ecD51fe` | | **V3 Factory** | `0x0dfb1Bb755d872EA1fa4d95E4ad0c2E6317Ce9B9` | | **V3 Position Manager** | `0x343b244bEDF133D57C61b241557bF29AA32ea4F9` | | **V3 Router** | `0x1F896179244C2675b6a1F36376cDF3B125d72B63` | | **V3 QuoterV2** | `0xC3D66b70F3BA12c1D1Ec5A20b0feB855b147812e` | ### Network Configuration ```json theme={null} { "chainId": 202555, "chainName": "Kasplex Mainnet", "rpcUrl": "https://evmrpc.kasplex.org", "blockExplorer": "https://explorer.kasplex.org", "nativeCurrency": { "name": "KAS", "symbol": "KAS", "decimals": 18 } } ``` | Contract | Address | | ----------------------- | -------------------------------------------- | | **Permit2** | `0xc320bc492Bb56169aBE18D3C0a2048c45febC897` | | **Universal Router** | `0x440d7f5FE865eFCcfdCB1ee9a000C114163689ba` | | **WKAS** | `0xC065C62a10fB363fD31CA394D632C4Df106566df` | | **V2 Factory** | `0x497152FfC1FEa1Ff31cc7cEeca4f4b9495b606fB` | | **V2 Router** | `0xf2ece243a0EFC0Cd1fcd3386b2f73f16D1378689` | | **V3 Factory** | `0x6ea7b69cDB0Af4DE7DF60DA08edE2F7E2b8d5924` | | **V3 Position Manager** | `0xDAF3700A7D80B26d5DD971C3C0e2fB93Ad73219f` | | **V3 Router** | `0xe3DC728050962343922A8E7b3E0cC158C94A1448` | | **V3 QuoterV2** | `0xfd0e09944444338Ab33b5b5eF66cd741331121e0` | ### Network Configuration ```json theme={null} { "chainId": 167012, "chainName": "Kasplex Testnet", "rpcUrl": "https://rpc.kasplextest.xyz", "blockExplorer": "https://explorer.testnet.kasplextest.xyz", "nativeCurrency": { "name": "KAS", "symbol": "KAS", "decimals": 18 } } ``` ## Contract Roles | Contract | Role | | ----------------------- | --------------------------------------------------------------------- | | **Permit2** | Token approval manager with expiration and per-spender scoping | | **Universal Router** | Unified swap execution across V2 and V3 | | **WKAS** | ERC-20 wrapper for native KAS | | **V2 Factory** | Creates and indexes V2 trading pairs | | **V2 Router** | Handles V2 liquidity operations (add/remove) | | **V3 Factory** | Creates and indexes V3 pools | | **V3 Position Manager** | Manages V3 liquidity positions (mint/burn/collect NFTs) | | **V3 Router** | Handles V3 swap routing | | **V3 QuoterV2** | On-chain quote estimation for V3 swaps (read-only, not gas efficient) | # Permit2 Source: https://docs.krokoswap.io/contracts/permit2 Token approval contract ABI and usage # Permit2 Contract Permit2 is the shared token approval manager. Users approve tokens to Permit2 once, then grant scoped permissions to specific spenders. For the concept overview, see [Permit2 Concepts](/concepts/permit2). ## Key Functions ### approve Grants a spender permission to transfer a specific token on behalf of the caller via Permit2. ```solidity theme={null} function approve( address token, address spender, uint160 amount, uint48 expiration ) external; ``` | Parameter | Type | Description | | ------------ | --------- | ------------------------------------------------------------- | | `token` | `address` | The ERC-20 token to approve | | `spender` | `address` | The address being granted permission (e.g., Universal Router) | | `amount` | `uint160` | Maximum amount the spender can transfer | | `expiration` | `uint48` | Unix timestamp when the permission expires | **Example:** ```typescript theme={null} const permit2 = new ethers.Contract(PERMIT2_ADDRESS, PERMIT2_ABI, signer); await permit2.approve( tokenAddress, UNIVERSAL_ROUTER, ethers.MaxUint160, // Max amount Math.floor(Date.now() / 1000) + 365 * 86400 // 1 year expiration ); ``` ### allowance Queries the current permission for a given owner-token-spender tuple. ```solidity theme={null} function allowance( address owner, address token, address spender ) external view returns ( uint160 amount, uint48 expiration, uint48 nonce ); ``` | Return | Type | Description | | ------------ | --------- | --------------------------------- | | `amount` | `uint160` | Remaining allowed amount | | `expiration` | `uint48` | Unix timestamp of expiration | | `nonce` | `uint48` | Current nonce for this permission | **Example:** ```typescript theme={null} const [amount, expiration, nonce] = await permit2.allowance( userAddress, tokenAddress, UNIVERSAL_ROUTER ); const needsApproval = amount < requiredAmount || expiration < Math.floor(Date.now() / 1000); ``` ## ABI ```json theme={null} [ "function approve(address token, address spender, uint160 amount, uint48 expiration)", "function allowance(address owner, address token, address spender) view returns (uint160 amount, uint48 expiration, uint48 nonce)" ] ``` ## Integration Pattern ```typescript theme={null} async function ensurePermit2Approval(signer, tokenAddress, spender, requiredAmount) { const permit2 = new ethers.Contract(PERMIT2_ADDRESS, PERMIT2_ABI, signer); const owner = await signer.getAddress(); // 1. Check existing Permit2 sub-approval const [amount, expiration] = await permit2.allowance(owner, tokenAddress, spender); const now = Math.floor(Date.now() / 1000); if (amount >= requiredAmount && expiration > now) { return; // Already approved } // 2. Check ERC-20 approval to Permit2 const token = new ethers.Contract(tokenAddress, ERC20_ABI, signer); const tokenAllowance = await token.allowance(owner, PERMIT2_ADDRESS); if (tokenAllowance < requiredAmount) { const tx = await token.approve(PERMIT2_ADDRESS, ethers.MaxUint256); await tx.wait(); } // 3. Grant Permit2 sub-approval const tx = await permit2.approve( tokenAddress, spender, ethers.MaxUint160, now + 365 * 86400 ); await tx.wait(); } ``` # Universal Router Source: https://docs.krokoswap.io/contracts/universal-router Unified swap execution contract ABI and usage # Universal Router Contract The Universal Router executes swaps across V2 and V3 pools via an encoded command interface. In practice, you do not need to encode commands yourself — the [Swap API](/api/swap) generates the calldata for you. For the concept overview, see [Universal Router Concepts](/concepts/universal-router). ## Key Functions ### execute Executes a sequence of commands in order. ```solidity theme={null} function execute( bytes calldata commands, bytes[] calldata inputs, uint256 deadline ) external payable; ``` | Parameter | Type | Description | | ---------- | --------- | -------------------------------------------------- | | `commands` | `bytes` | Sequence of command codes (1 byte each) | | `inputs` | `bytes[]` | ABI-encoded parameters for each command | | `deadline` | `uint256` | Unix timestamp after which the transaction reverts | You typically do not call `execute()` directly. Use the [Swap API](/api/swap) to generate the `{ to, data, value }` object, then send it as a transaction. ## Command Types | Command | Code | Description | | ------------------- | ------ | ---------------------------------------- | | `V2_SWAP_EXACT_IN` | `0x00` | V2 swap with exact input amount | | `V2_SWAP_EXACT_OUT` | `0x01` | V2 swap with exact output amount | | `V3_SWAP_EXACT_IN` | `0x02` | V3 swap with exact input amount | | `V3_SWAP_EXACT_OUT` | `0x03` | V3 swap with exact output amount | | `WRAP_ETH` | `0x0b` | Wrap native KAS to WKAS | | `UNWRAP_WETH` | `0x0c` | Unwrap WKAS to native KAS | | `PERMIT2_PERMIT` | `0x0a` | Execute a Permit2 signature-based permit | | `TRANSFER` | `0x04` | Transfer tokens | ## Usage Pattern The recommended pattern is to use the Swap API to generate calldata: ```typescript theme={null} // 1. Get calldata from Swap API const response = await fetch('/swap-api/api/v1/swap', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tokenIn: '0x...', tokenOut: '0x...', amountIn: '1000000000000000000', tradeType: 0, slippage: 0.5, recipient: userAddress, deadline: 1200 }) }); const swapData = await response.json(); // 2. Send the transaction directly const tx = await signer.sendTransaction({ to: swapData.to, // Universal Router address data: swapData.data, // Encoded execute() calldata value: swapData.value // Native KAS value (0 for ERC-20 swaps) }); ``` ## Error Handling Common revert reasons: | Error | Cause | Solution | | ----------------------- | -------------------------------- | ------------------------------------------ | | `EXPIRED` | Transaction deadline exceeded | Use a longer deadline or submit faster | | `V3_INVALID_AMOUNT_OUT` | Output below `minAmountOut` | Increase slippage tolerance | | `TRANSFER_FROM_FAILED` | Insufficient approval or balance | Check Permit2 approval and token balance | | `INSUFFICIENT_ETH` | Not enough native KAS sent | Ensure `value` matches the required amount | ## ABI ```json theme={null} [ "function execute(bytes calldata commands, bytes[] calldata inputs, uint256 deadline) external payable" ] ``` # V2 Factory & Pair Source: https://docs.krokoswap.io/contracts/v2-factory-and-pair V2 pool creation and pair contract interfaces # V2 Factory & Pair ## V2 Factory The V2 Factory creates and indexes V2 trading pairs. Each unique token pair has exactly one V2 pool. ### Key Functions #### getPair Returns the pair contract address for two tokens, or zero address if no pair exists. ```solidity theme={null} function getPair( address tokenA, address tokenB ) external view returns (address pair); ``` Token order does not matter — `getPair(A, B)` returns the same result as `getPair(B, A)`. #### allPairs Returns the pair address at a given index. Used for enumeration. ```solidity theme={null} function allPairs(uint256 index) external view returns (address pair); function allPairsLength() external view returns (uint256); ``` ### ABI ```json theme={null} [ "function getPair(address tokenA, address tokenB) view returns (address pair)", "function allPairs(uint256 index) view returns (address pair)", "function allPairsLength() view returns (uint256)" ] ``` *** ## V2 Pair Each V2 Pair is an ERC-20 contract (LP token) that also holds the pool reserves and executes swaps. ### Key Functions #### getReserves Returns the current token reserves and the last block timestamp. ```solidity theme={null} function getReserves() external view returns ( uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast ); ``` `token0` is always the token with the lower address. #### token0 / token1 Returns the addresses of the pool's tokens. ```solidity theme={null} function token0() external view returns (address); function token1() external view returns (address); ``` #### totalSupply / balanceOf Standard ERC-20 functions for LP token accounting. ```solidity theme={null} function totalSupply() external view returns (uint256); function balanceOf(address owner) external view returns (uint256); ``` ### ABI ```json theme={null} [ "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 owner) view returns (uint256)", "function approve(address spender, uint256 value) returns (bool)", "function transfer(address to, uint256 value) returns (bool)" ] ``` ### Example: Read Pool State ```typescript theme={null} const FACTORY_ABI = [ "function getPair(address, address) view returns (address)" ]; const PAIR_ABI = [ "function getReserves() view returns (uint112, uint112, uint32)", "function token0() view returns (address)", "function token1() view returns (address)", "function totalSupply() view returns (uint256)" ]; const factory = new ethers.Contract(V2_FACTORY, FACTORY_ABI, provider); const pairAddress = await factory.getPair(tokenA, tokenB); if (pairAddress === ethers.ZeroAddress) { console.log("Pool does not exist"); } else { const pair = new ethers.Contract(pairAddress, PAIR_ABI, provider); const [reserve0, reserve1] = await pair.getReserves(); const token0 = await pair.token0(); console.log(`Reserve ${token0 === tokenA ? 'A' : 'B'}: ${reserve0}`); console.log(`Reserve ${token0 === tokenA ? 'B' : 'A'}: ${reserve1}`); } ``` # V2 Router Source: https://docs.krokoswap.io/contracts/v2-router V2 liquidity management contract interface # V2 Router The V2 Router provides helper functions for adding and removing liquidity from V2 pools. It handles token sorting, pair creation (if needed), and optimal deposit calculations. For **swaps**, use the [Universal Router](/contracts/universal-router) instead. The V2 Router is primarily used for **liquidity operations**. ## Key Functions ### addLiquidity Adds liquidity to an ERC-20 / ERC-20 pair. Creates the pair if it doesn't exist. ```solidity theme={null} function addLiquidity( address tokenA, address tokenB, uint256 amountADesired, uint256 amountBDesired, uint256 amountAMin, uint256 amountBMin, address to, uint256 deadline ) external returns ( uint256 amountA, uint256 amountB, uint256 liquidity ); ``` | Parameter | Description | | ---------------------------------- | -------------------------------------- | | `tokenA`, `tokenB` | Token addresses | | `amountADesired`, `amountBDesired` | Ideal amounts to deposit | | `amountAMin`, `amountBMin` | Minimum accepted (slippage protection) | | `to` | Recipient of LP tokens | | `deadline` | Transaction deadline (Unix timestamp) | ### addLiquidityETH Adds liquidity to a KAS / ERC-20 pair. Send native KAS as `msg.value`. ```solidity theme={null} function addLiquidityETH( address token, uint256 amountTokenDesired, uint256 amountTokenMin, uint256 amountETHMin, address to, uint256 deadline ) external payable returns ( uint256 amountToken, uint256 amountETH, uint256 liquidity ); ``` ### removeLiquidity Burns LP tokens and returns both underlying tokens. ```solidity theme={null} function removeLiquidity( address tokenA, address tokenB, uint256 liquidity, uint256 amountAMin, uint256 amountBMin, address to, uint256 deadline ) external returns ( uint256 amountA, uint256 amountB ); ``` ### removeLiquidityETH Burns LP tokens from a KAS pair and returns native KAS + ERC-20 token. ```solidity theme={null} function removeLiquidityETH( address token, uint256 liquidity, uint256 amountTokenMin, uint256 amountETHMin, address to, uint256 deadline ) external returns ( uint256 amountToken, uint256 amountETH ); ``` ## ABI ```json theme={null} [ "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)" ] ``` ## Token Approval Before calling `addLiquidity`, approve both tokens to the V2 Router: ```typescript theme={null} await tokenA.approve(V2_ROUTER, amountA); await tokenB.approve(V2_ROUTER, amountB); ``` Before calling `removeLiquidity`, approve the LP token to the V2 Router: ```typescript theme={null} await lpToken.approve(V2_ROUTER, lpAmount); ``` # V3 Factory & Pool Source: https://docs.krokoswap.io/contracts/v3-factory-and-pool V3 pool creation and pool contract interfaces # V3 Factory & Pool ## V3 Factory The V3 Factory creates and indexes V3 pools. Unlike V2, the same token pair can have **multiple pools** with different fee tiers. ### Key Functions #### getPool Returns the pool address for a token pair and fee tier. ```solidity theme={null} function getPool( address tokenA, address tokenB, uint24 fee ) external view returns (address pool); ``` | Parameter | Description | | ------------------ | ------------------------------------------ | | `tokenA`, `tokenB` | Token addresses (order doesn't matter) | | `fee` | Fee tier: `100`, `500`, `3000`, or `10000` | Returns the zero address if the pool doesn't exist. ### ABI ```json theme={null} [ "function getPool(address tokenA, address tokenB, uint24 fee) view returns (address pool)" ] ``` *** ## V3 Pool Each V3 Pool contract manages concentrated liquidity positions and swap execution for a specific token pair and fee tier. ### Key Functions #### slot0 Returns the current pool state. ```solidity theme={null} function slot0() external view returns ( uint160 sqrtPriceX96, int24 tick, uint16 observationIndex, uint16 observationCardinality, uint16 observationCardinalityNext, uint8 feeProtocol, bool unlocked ); ``` | Return | Description | | -------------- | ------------------------------------ | | `sqrtPriceX96` | Current price as sqrt(price) \* 2^96 | | `tick` | Current tick index | | `feeProtocol` | Protocol fee setting | | `unlocked` | Reentrancy guard state | #### liquidity Returns the total active (in-range) liquidity. ```solidity theme={null} function liquidity() external view returns (uint128); ``` #### fee / tickSpacing ```solidity theme={null} function fee() external view returns (uint24); function tickSpacing() external view returns (int24); ``` #### token0 / token1 ```solidity theme={null} function token0() external view returns (address); function token1() external view returns (address); ``` ### ABI ```json theme={null} [ "function slot0() view returns (uint160 sqrtPriceX96, int24 tick, uint16 observationIndex, uint16 observationCardinality, uint16 observationCardinalityNext, uint8 feeProtocol, bool unlocked)", "function liquidity() view returns (uint128)", "function fee() view returns (uint24)", "function tickSpacing() view returns (int24)", "function token0() view returns (address)", "function token1() view returns (address)" ] ``` ### Example: Read Pool Price ```typescript theme={null} const FACTORY_ABI = [ "function getPool(address, address, uint24) view returns (address)" ]; const POOL_ABI = [ "function slot0() view returns (uint160, int24, uint16, uint16, uint16, uint8, bool)", "function token0() view returns (address)", "function token1() view returns (address)", "function fee() view returns (uint24)", "function liquidity() view returns (uint128)" ]; const factory = new ethers.Contract(V3_FACTORY, FACTORY_ABI, provider); const poolAddress = await factory.getPool(tokenA, tokenB, 3000); // 0.3% fee if (poolAddress === ethers.ZeroAddress) { console.log("Pool does not exist for this fee tier"); } else { const pool = new ethers.Contract(poolAddress, POOL_ABI, provider); const [sqrtPriceX96, tick] = await pool.slot0(); // Convert sqrtPriceX96 to price const price = (Number(sqrtPriceX96) / 2 ** 96) ** 2; console.log(`Current price (token1/token0): ${price}`); console.log(`Current tick: ${tick}`); } ``` ### Price Conversion The pool stores price as `sqrtPriceX96`. To convert: ```typescript theme={null} // sqrtPriceX96 → price (token1 per token0) const price = (sqrtPriceX96 / 2n ** 96n) ** 2n; // For BigInt // With decimal adjustment for different token decimals const adjustedPrice = price * 10n ** BigInt(decimals0 - decimals1); ``` For precise calculations, use the `@uniswap/v3-sdk` library or the formulas in [Ticks and Ranges](/concepts/v3/ticks-and-ranges). # V3 Position Manager Source: https://docs.krokoswap.io/contracts/v3-position-manager V3 liquidity position NFT management contract # V3 Position Manager The **NonfungiblePositionManager** manages V3 liquidity positions as ERC-721 NFTs. Each position represents a unique combination of pool, price range, and liquidity amount. ## Key Functions ### mint Creates a new liquidity position and mints an NFT. ```solidity theme={null} struct MintParams { address token0; address token1; uint24 fee; int24 tickLower; int24 tickUpper; uint256 amount0Desired; uint256 amount1Desired; uint256 amount0Min; uint256 amount1Min; address recipient; uint256 deadline; } function mint(MintParams calldata params) external payable returns ( uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1 ); ``` | Parameter | Description | | ---------------------------------- | --------------------------------------------------- | | `token0`, `token1` | Token addresses (token0 \< token1) | | `fee` | Fee tier (100, 500, 3000, 10000) | | `tickLower`, `tickUpper` | Price range boundaries (must align to tick spacing) | | `amount0Desired`, `amount1Desired` | Maximum amounts to deposit | | `amount0Min`, `amount1Min` | Minimum accepted (slippage protection) | | `recipient` | Who receives the NFT | | `deadline` | Transaction deadline | ### increaseLiquidity Adds more liquidity to an existing position. ```solidity theme={null} struct IncreaseLiquidityParams { uint256 tokenId; uint256 amount0Desired; uint256 amount1Desired; uint256 amount0Min; uint256 amount1Min; uint256 deadline; } function increaseLiquidity(IncreaseLiquidityParams calldata params) external payable returns ( uint128 liquidity, uint256 amount0, uint256 amount1 ); ``` ### decreaseLiquidity Removes liquidity from a position. Tokens are held by the contract until `collect()` is called. ```solidity theme={null} struct DecreaseLiquidityParams { uint256 tokenId; uint128 liquidity; uint256 amount0Min; uint256 amount1Min; uint256 deadline; } function decreaseLiquidity(DecreaseLiquidityParams calldata params) external payable returns ( uint256 amount0, uint256 amount1 ); ``` ### collect Collects tokens owed to a position (from decreased liquidity and/or earned fees). ```solidity theme={null} struct CollectParams { uint256 tokenId; address recipient; uint128 amount0Max; uint128 amount1Max; } function collect(CollectParams calldata params) external payable returns ( uint256 amount0, uint256 amount1 ); ``` Set `amount0Max` and `amount1Max` to `type(uint128).max` to collect everything owed. ### positions Queries the full state of a position by its token ID. ```solidity theme={null} function positions(uint256 tokenId) external view returns ( uint96 nonce, address operator, address token0, address token1, uint24 fee, int24 tickLower, int24 tickUpper, uint128 liquidity, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, uint128 tokensOwed0, uint128 tokensOwed1 ); ``` ### burn Burns an empty position NFT (after all liquidity is removed and fees collected). ```solidity theme={null} function burn(uint256 tokenId) external payable; ``` ## ABI ```json theme={null} [ "function mint((address token0, address token1, uint24 fee, int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired, uint256 amount0Min, uint256 amount1Min, address recipient, uint256 deadline)) payable returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1)", "function increaseLiquidity((uint256 tokenId, uint256 amount0Desired, uint256 amount1Desired, uint256 amount0Min, uint256 amount1Min, uint256 deadline)) payable returns (uint128 liquidity, uint256 amount0, uint256 amount1)", "function decreaseLiquidity((uint256 tokenId, uint128 liquidity, uint256 amount0Min, uint256 amount1Min, uint256 deadline)) payable returns (uint256 amount0, uint256 amount1)", "function collect((uint256 tokenId, address recipient, uint128 amount0Max, uint128 amount1Max)) payable returns (uint256 amount0, uint256 amount1)", "function positions(uint256 tokenId) view returns (uint96 nonce, address operator, address token0, address token1, uint24 fee, int24 tickLower, int24 tickUpper, uint128 liquidity, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, uint128 tokensOwed0, uint128 tokensOwed1)", "function burn(uint256 tokenId) payable" ] ``` ## Token Approval Before minting or increasing liquidity, approve both tokens to the Position Manager: ```typescript theme={null} await token0.approve(V3_POSITION_MANAGER, amount0); await token1.approve(V3_POSITION_MANAGER, amount1); ``` For positions involving native KAS, send KAS as `msg.value` instead of approving WKAS. ## Multicall The Position Manager supports `multicall()` to batch multiple operations in a single transaction: ```solidity theme={null} function multicall(bytes[] calldata data) external payable returns (bytes[] memory results); ``` Common use case: decrease liquidity + collect in one transaction. # V3 QuoterV2 Source: https://docs.krokoswap.io/contracts/v3-quoter-v2 On-chain quote estimation for V3 swaps # V3 QuoterV2 The **QuoterV2** contract allows getting the expected amount out or amount in for a given V3 swap without executing the swap. It simulates the swap logic on-chain to return accurate quotes. These functions are **not gas efficient** and should **not** be called on-chain. Instead, use them via `eth_call` (static call) from off-chain code. For on-chain swap execution, use the [V3 Router](/contracts/v3-router) or [Universal Router](/contracts/universal-router). ## Key Functions ### quoteExactInputSingle Returns the expected output amount for a single-pool exact input swap. ```solidity theme={null} function quoteExactInputSingle( QuoteExactInputSingleParams memory params ) public override returns ( uint256 amountOut, uint160 sqrtPriceX96After, uint32 initializedTicksCrossed, uint256 gasEstimate ); ``` **Parameters** (struct `QuoteExactInputSingleParams`): | Parameter | Type | Description | | ------------------- | --------- | ---------------------------------------- | | `tokenIn` | `address` | Input token address | | `tokenOut` | `address` | Output token address | | `amountIn` | `uint256` | The desired input amount | | `fee` | `uint24` | Pool fee tier (100, 500, 3000, or 10000) | | `sqrtPriceLimitX96` | `uint160` | Price limit (0 for no limit) | **Returns:** | Return | Type | Description | | ------------------------- | --------- | ----------------------------------- | | `amountOut` | `uint256` | The expected output amount | | `sqrtPriceX96After` | `uint160` | The pool price after the swap | | `initializedTicksCrossed` | `uint32` | Number of initialized ticks crossed | | `gasEstimate` | `uint256` | Estimated gas cost of the swap | ### quoteExactInput Returns the expected output amount for a multi-hop exact input swap. ```solidity theme={null} function quoteExactInput( bytes memory path, uint256 amountIn ) public override returns ( uint256 amountOut, uint160[] memory sqrtPriceX96AfterList, uint32[] memory initializedTicksCrossedList, uint256 gasEstimate ); ``` | Parameter | Type | Description | | ---------- | --------- | -------------------------------------------- | | `path` | `bytes` | Encoded swap path (token addresses and fees) | | `amountIn` | `uint256` | The desired input amount | **Returns:** | Return | Type | Description | | ----------------------------- | ----------- | -------------------------- | | `amountOut` | `uint256` | The expected output amount | | `sqrtPriceX96AfterList` | `uint160[]` | Pool prices after each hop | | `initializedTicksCrossedList` | `uint32[]` | Ticks crossed per hop | | `gasEstimate` | `uint256` | Total estimated gas cost | ### quoteExactOutputSingle Returns the required input amount for a single-pool exact output swap. ```solidity theme={null} function quoteExactOutputSingle( QuoteExactOutputSingleParams memory params ) public override returns ( uint256 amountIn, uint160 sqrtPriceX96After, uint32 initializedTicksCrossed, uint256 gasEstimate ); ``` **Parameters** (struct `QuoteExactOutputSingleParams`): | Parameter | Type | Description | | ------------------- | --------- | ---------------------------- | | `tokenIn` | `address` | Input token address | | `tokenOut` | `address` | Output token address | | `amount` | `uint256` | The desired output amount | | `fee` | `uint24` | Pool fee tier | | `sqrtPriceLimitX96` | `uint160` | Price limit (0 for no limit) | ### quoteExactOutput Returns the required input amount for a multi-hop exact output swap. ```solidity theme={null} function quoteExactOutput( bytes memory path, uint256 amountOut ) public override returns ( uint256 amountIn, uint160[] memory sqrtPriceX96AfterList, uint32[] memory initializedTicksCrossedList, uint256 gasEstimate ); ``` | Parameter | Type | Description | | ----------- | --------- | --------------------------------------------- | | `path` | `bytes` | Encoded swap path (reversed for exact output) | | `amountOut` | `uint256` | The desired output amount | ## Callback ### uniswapV3SwapCallback ```solidity theme={null} function uniswapV3SwapCallback( int256 amount0Delta, int256 amount1Delta, bytes data ) external view override; ``` Called by the pool during quote simulation. This is an internal mechanism — you do not need to call this directly. | Parameter | Type | Description | | -------------- | -------- | ------------------------------------------------------------------- | | `amount0Delta` | `int256` | Amount of token0 sent (negative) or received (positive) by the pool | | `amount1Delta` | `int256` | Amount of token1 sent (negative) or received (positive) by the pool | | `data` | `bytes` | Callback data passed through from the swap call | ## ABI ```json theme={null} [ "function quoteExactInputSingle((address tokenIn, address tokenOut, uint256 amountIn, uint24 fee, uint160 sqrtPriceLimitX96)) returns (uint256 amountOut, uint160 sqrtPriceX96After, uint32 initializedTicksCrossed, uint256 gasEstimate)", "function quoteExactInput(bytes path, uint256 amountIn) returns (uint256 amountOut, uint160[] sqrtPriceX96AfterList, uint32[] initializedTicksCrossedList, uint256 gasEstimate)", "function quoteExactOutputSingle((address tokenIn, address tokenOut, uint256 amount, uint24 fee, uint160 sqrtPriceLimitX96)) returns (uint256 amountIn, uint160 sqrtPriceX96After, uint32 initializedTicksCrossed, uint256 gasEstimate)", "function quoteExactOutput(bytes path, uint256 amountOut) returns (uint256 amountIn, uint160[] sqrtPriceX96AfterList, uint32[] initializedTicksCrossedList, uint256 gasEstimate)" ] ``` ## Example: Get a Quote ```typescript theme={null} const QUOTER_ABI = [ 'function quoteExactInputSingle((address,address,uint256,uint24,uint160)) returns (uint256,uint160,uint32,uint256)', ]; const quoter = new ethers.Contract(QUOTER_V2_ADDRESS, QUOTER_ABI, provider); // Quote: how much tokenOut for 1 tokenIn via the 0.3% pool? const [amountOut, sqrtPriceAfter, ticksCrossed, gasEstimate] = await quoter.quoteExactInputSingle.staticCall({ tokenIn: '0xTokenA', tokenOut: '0xTokenB', amountIn: ethers.parseEther('1'), fee: 3000, sqrtPriceLimitX96: 0, }); console.log(`Expected output: ${ethers.formatEther(amountOut)}`); console.log(`Gas estimate: ${gasEstimate}`); ``` Always use `staticCall` (ethers v6) or `callStatic` (ethers v5) — these functions revert if called as regular transactions. # V2 Add Liquidity Example Source: https://docs.krokoswap.io/examples/add-liquidity-v2-example Complete example of adding liquidity to a V2 pool # V2 Add Liquidity Example A complete example of adding liquidity to a V2 constant-product pool. ## Full Code ```typescript theme={null} import { ethers } from 'ethers'; // === Configuration (Mainnet) === const RPC_URL = 'https://evmrpc.kasplex.org'; const V2_FACTORY = '0x4373b7Fcf5059A785843cD224129e01d243Aef71'; const V2_ROUTER = '0xC7ca845B8302346e1C7227f03bb9EFb35ecD51fe'; // === ABIs === const ERC20_ABI = [ 'function approve(address,uint256) returns (bool)', 'function allowance(address,address) view returns (uint256)', 'function balanceOf(address) view returns (uint256)', 'function symbol() view returns (string)', 'function decimals() view returns (uint8)', ]; const FACTORY_ABI = [ 'function getPair(address,address) view returns (address)', ]; const PAIR_ABI = [ 'function getReserves() view returns (uint112, uint112, uint32)', 'function token0() view returns (address)', 'function totalSupply() view returns (uint256)', 'function balanceOf(address) view returns (uint256)', ]; const ROUTER_ABI = [ 'function addLiquidity(address,address,uint256,uint256,uint256,uint256,address,uint256) returns (uint256,uint256,uint256)', 'function addLiquidityETH(address,uint256,uint256,uint256,address,uint256) payable returns (uint256,uint256,uint256)', ]; /** * Add liquidity to a V2 pool. * * @param signer - Connected ethers Signer * @param tokenA - First token address * @param tokenB - Second token address * @param amountA - Desired amount of tokenA (raw units) * @param slippage - Slippage tolerance (e.g., 1 for 1%) */ async function addLiquidityV2( signer: ethers.Signer, tokenA: string, tokenB: string, amountA: bigint, slippage: number = 1 ) { const provider = signer.provider!; const userAddress = await signer.getAddress(); const deadline = Math.floor(Date.now() / 1000) + 1200; const slippageFactor = BigInt(Math.floor((100 - slippage) * 100)) ; // e.g., 9900 for 1% // --- Check if pool exists and calculate amountB --- const factory = new ethers.Contract(V2_FACTORY, FACTORY_ABI, provider); const pairAddress = await factory.getPair(tokenA, tokenB); let amountB: bigint; if (pairAddress === ethers.ZeroAddress) { // New pool — user sets the initial price console.log('Pool does not exist. You are setting the initial price.'); console.log('Enter amountB manually (this example uses 1:1 ratio):'); amountB = amountA; // Change this to your desired ratio } else { // Existing pool — calculate proportional amountB const pair = new ethers.Contract(pairAddress, PAIR_ABI, provider); const [reserve0, reserve1] = await pair.getReserves(); const token0 = await pair.token0(); const isAToken0 = tokenA.toLowerCase() === token0.toLowerCase(); const reserveA = isAToken0 ? reserve0 : reserve1; const reserveB = isAToken0 ? reserve1 : reserve0; amountB = (amountA * reserveB) / reserveA; console.log(`Pool exists. Calculated amountB: ${amountB}`); } // --- Approve both tokens to V2 Router --- console.log('Approving tokens...'); const tokenAContract = new ethers.Contract(tokenA, ERC20_ABI, signer); const tokenBContract = new ethers.Contract(tokenB, ERC20_ABI, signer); const allowanceA = await tokenAContract.allowance(userAddress, V2_ROUTER); const allowanceB = await tokenBContract.allowance(userAddress, V2_ROUTER); if (allowanceA < amountA) { const tx = await tokenAContract.approve(V2_ROUTER, ethers.MaxUint256); await tx.wait(); } if (allowanceB < amountB) { const tx = await tokenBContract.approve(V2_ROUTER, ethers.MaxUint256); await tx.wait(); } // --- Add liquidity --- console.log('Adding liquidity...'); const router = new ethers.Contract(V2_ROUTER, ROUTER_ABI, signer); const tx = await router.addLiquidity( tokenA, tokenB, amountA, amountB, (amountA * slippageFactor) / 10000n, (amountB * slippageFactor) / 10000n, userAddress, deadline ); const receipt = await tx.wait(); console.log(`Liquidity added. Tx: ${receipt!.hash}`); // --- Check LP balance --- const newPairAddress = pairAddress === ethers.ZeroAddress ? await factory.getPair(tokenA, tokenB) : pairAddress; const pair = new ethers.Contract(newPairAddress, PAIR_ABI, provider); const lpBalance = await pair.balanceOf(userAddress); console.log(`LP token balance: ${lpBalance}`); } // === Usage === async function main() { const provider = new ethers.JsonRpcProvider(RPC_URL); const signer = new ethers.Wallet('YOUR_PRIVATE_KEY', provider); const TOKEN_A = '0xB190a6A7fC2873f1Abf145279eD664348d5Ef630'; const TOKEN_B = '0x3Ac3B30b7f18AEFD4590D7FE4d9C5944aaeB7220'; await addLiquidityV2( signer, TOKEN_A, TOKEN_B, ethers.parseEther('10'), // 10 tokens 1 // 1% slippage ); } main().catch(console.error); ``` ## With Native KAS To add liquidity with native KAS, use `addLiquidityETH`: ```typescript theme={null} const tx = await router.addLiquidityETH( tokenAddress, // The ERC-20 token (not WKAS) amountTokenDesired, amountTokenMin, amountKASMin, userAddress, deadline, { value: amountKASDesired } // Send KAS as value ); ``` ## Removing Liquidity ```typescript theme={null} const REMOVE_ABI = [ 'function removeLiquidity(address,address,uint256,uint256,uint256,address,uint256) returns (uint256,uint256)', ]; // Approve LP token to router const lpToken = new ethers.Contract(pairAddress, ERC20_ABI, signer); await (await lpToken.approve(V2_ROUTER, lpAmount)).wait(); // Remove const router = new ethers.Contract(V2_ROUTER, REMOVE_ABI, signer); const tx = await router.removeLiquidity( tokenA, tokenB, lpAmount, 0, // amountAMin 0, // amountBMin userAddress, deadline ); ``` # V3 Add Liquidity Example Source: https://docs.krokoswap.io/examples/add-liquidity-v3-example Complete example of creating a V3 concentrated liquidity position # V3 Add Liquidity Example A complete example of minting a V3 concentrated liquidity position with a custom price range. ## Full Code ```typescript theme={null} import { ethers } from 'ethers'; // === Configuration (Mainnet) === const RPC_URL = 'https://evmrpc.kasplex.org'; const V3_FACTORY = '0x0dfb1Bb755d872EA1fa4d95E4ad0c2E6317Ce9B9'; const V3_POSITION_MANAGER = '0x343b244bEDF133D57C61b241557bF29AA32ea4F9'; // === ABIs === const ERC20_ABI = [ 'function approve(address,uint256) returns (bool)', 'function allowance(address,address) view returns (uint256)', 'function decimals() view returns (uint8)', 'function symbol() view returns (string)', ]; const FACTORY_ABI = [ 'function getPool(address,address,uint24) view returns (address)', ]; const POOL_ABI = [ 'function slot0() view returns (uint160, int24, uint16, uint16, uint16, uint8, bool)', 'function tickSpacing() view returns (int24)', 'function token0() view returns (address)', 'function token1() view returns (address)', 'function liquidity() view returns (uint128)', ]; const PM_ABI = [ 'function mint(tuple(address token0, address token1, uint24 fee, int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired, uint256 amount0Min, uint256 amount1Min, address recipient, uint256 deadline)) payable returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1)', 'function positions(uint256 tokenId) view returns (uint96, address, address, address, uint24, int24, int24, uint128, uint256, uint256, uint128, uint128)', ]; // === Helpers === function nearestUsableTick(tick: number, tickSpacing: number): number { return Math.round(tick / tickSpacing) * tickSpacing; } function sortTokens(a: string, b: string): [string, string] { return a.toLowerCase() < b.toLowerCase() ? [a, b] : [b, a]; } /** * Create a V3 liquidity position. * * @param signer - Connected ethers Signer * @param tokenA - First token address * @param tokenB - Second token address * @param fee - Fee tier (100, 500, 3000, or 10000) * @param amount0 - Desired amount of token0 (raw units) * @param amount1 - Desired amount of token1 (raw units) * @param rangePercent - Price range as ± percentage (e.g., 10 for ±10%) */ async function addLiquidityV3( signer: ethers.Signer, tokenA: string, tokenB: string, fee: number, amount0: bigint, amount1: bigint, rangePercent: number = 10 ) { const provider = signer.provider!; const userAddress = await signer.getAddress(); const deadline = Math.floor(Date.now() / 1000) + 1200; // Sort tokens (token0 < token1 by address) const [token0, token1] = sortTokens(tokenA, tokenB); const isSwapped = token0.toLowerCase() !== tokenA.toLowerCase(); if (isSwapped) [amount0, amount1] = [amount1, amount0]; // --- Get pool state --- const factory = new ethers.Contract(V3_FACTORY, FACTORY_ABI, provider); const poolAddress = await factory.getPool(token0, token1, fee); if (poolAddress === ethers.ZeroAddress) { throw new Error(`No pool exists for this pair with fee ${fee}`); } const pool = new ethers.Contract(poolAddress, POOL_ABI, provider); const [sqrtPriceX96, currentTick] = await pool.slot0(); const tickSpacing = Number(await pool.tickSpacing()); console.log(`Pool: ${poolAddress}`); console.log(`Current tick: ${currentTick}`); console.log(`Tick spacing: ${tickSpacing}`); // --- Calculate tick range --- // Convert percentage to approximate tick offset // 1% price change ≈ 100 ticks (since 1.0001^100 ≈ 1.01) const tickOffset = Math.floor(rangePercent * 100); const tickLower = nearestUsableTick(Number(currentTick) - tickOffset, tickSpacing); const tickUpper = nearestUsableTick(Number(currentTick) + tickOffset, tickSpacing); console.log(`Range: tick [${tickLower}, ${tickUpper}]`); // --- Approve tokens --- console.log('Approving tokens...'); const token0Contract = new ethers.Contract(token0, ERC20_ABI, signer); const token1Contract = new ethers.Contract(token1, ERC20_ABI, signer); const allow0 = await token0Contract.allowance(userAddress, V3_POSITION_MANAGER); const allow1 = await token1Contract.allowance(userAddress, V3_POSITION_MANAGER); if (allow0 < amount0) { await (await token0Contract.approve(V3_POSITION_MANAGER, ethers.MaxUint256)).wait(); } if (allow1 < amount1) { await (await token1Contract.approve(V3_POSITION_MANAGER, ethers.MaxUint256)).wait(); } // --- Mint position --- console.log('Minting position...'); const pm = new ethers.Contract(V3_POSITION_MANAGER, PM_ABI, signer); const mintParams = { token0, token1, fee, tickLower, tickUpper, amount0Desired: amount0, amount1Desired: amount1, amount0Min: 0n, // Set to 0 for simplicity; use slippage in production amount1Min: 0n, recipient: userAddress, deadline, }; const tx = await pm.mint(mintParams); const receipt = await tx.wait(); console.log(`Position minted. Tx: ${receipt!.hash}`); // Parse tokenId from events (simplified) for (const log of receipt!.logs) { try { const parsed = pm.interface.parseLog({ topics: [...log.topics], data: log.data }); if (parsed && parsed.name === 'IncreaseLiquidity') { console.log(`Token ID: ${parsed.args.tokenId}`); console.log(`Liquidity: ${parsed.args.liquidity}`); console.log(`Amount0: ${parsed.args.amount0}`); console.log(`Amount1: ${parsed.args.amount1}`); } } catch { // Not a PM event, skip } } } // === Usage === async function main() { const provider = new ethers.JsonRpcProvider(RPC_URL); const signer = new ethers.Wallet('YOUR_PRIVATE_KEY', provider); const TOKEN_A = '0xB190a6A7fC2873f1Abf145279eD664348d5Ef630'; const TOKEN_B = '0x3Ac3B30b7f18AEFD4590D7FE4d9C5944aaeB7220'; await addLiquidityV3( signer, TOKEN_A, TOKEN_B, 3000, // 0.3% fee tier ethers.parseEther('10'), // 10 token0 ethers.parseEther('10'), // 10 token1 10 // ±10% range ); } main().catch(console.error); ``` ## Key Points 1. **Token order matters**: `token0` must have a lower address than `token1`. The helper `sortTokens()` handles this. 2. **Tick alignment**: `tickLower` and `tickUpper` must be multiples of `tickSpacing`. Use `nearestUsableTick()`. 3. **Fee tier**: Must match an existing pool. Check with `factory.getPool()` first. 4. **Slippage**: Set `amount0Min` / `amount1Min` to non-zero values in production. 5. **Native KAS**: If one token is KAS, send it as `{ value: kasAmount }` in the mint call. ## Managing the Position After minting, use the `tokenId` for: ```typescript theme={null} // Collect fees await pm.collect({ tokenId, recipient: userAddress, amount0Max: ethers.MaxUint128, amount1Max: ethers.MaxUint128, }); // Remove liquidity await pm.decreaseLiquidity({ tokenId, liquidity: liquidityAmount, amount0Min: 0, amount1Min: 0, deadline, }); // Then collect the tokens await pm.collect({ tokenId, recipient: userAddress, amount0Max: ethers.MaxUint128, amount1Max: ethers.MaxUint128 }); ``` # Swap Example Source: https://docs.krokoswap.io/examples/swap-example Complete, runnable swap integration example # Swap Example A minimal but complete example of executing a token swap on Kroko DEX using ethers.js v6. ## Full Code ```typescript theme={null} import { ethers } from 'ethers'; // === Configuration === const RPC_URL = 'https://evmrpc.kasplex.org'; const API_BASE = 'https://dex.kasplex.org/swap-api'; const PERMIT2 = '0x2E1987F680FD7Bc8B33d3Bf94f12B988A0B50034'; const UNIVERSAL_ROUTER = '0xefeCc1c2dE3BfE4C6D43030F2AcDD5C3cE279024'; // === ABIs === const ERC20_ABI = [ 'function allowance(address,address) view returns (uint256)', 'function approve(address,uint256) returns (bool)', 'function balanceOf(address) view returns (uint256)', 'function decimals() view returns (uint8)', 'function symbol() view returns (string)', ]; const PERMIT2_ABI = [ 'function approve(address token, address spender, uint160 amount, uint48 expiration)', 'function allowance(address owner, address token, address spender) view returns (uint160, uint48, uint48)', ]; // === Trade Types === enum TradeType { EXACT_INPUT = 0, EXACT_OUTPUT = 1, } /** * Execute a complete swap on Kroko DEX. * * @param signer - Connected ethers Signer * @param tokenIn - Input token address (use WKAS address for native KAS) * @param tokenOut - Output token address * @param amount - Amount in raw units (amountIn for EXACT_INPUT, amountOut for EXACT_OUTPUT) * @param tradeType - 0 for Exact Input, 1 for Exact Output * @param slippage - Slippage tolerance as percentage (e.g., 0.5 for 0.5%) */ async function executeSwap( signer: ethers.Signer, tokenIn: string, tokenOut: string, amount: string, tradeType: TradeType = TradeType.EXACT_INPUT, slippage: number = 0.5 ): Promise { const userAddress = await signer.getAddress(); const token = new ethers.Contract(tokenIn, ERC20_ABI, signer); const permit2 = new ethers.Contract(PERMIT2, PERMIT2_ABI, signer); // --- Step 1: Approve token → Permit2 --- const tokenAllowance = await token.allowance(userAddress, PERMIT2); if (tokenAllowance < BigInt(amount)) { console.log('Step 1: Approving token to Permit2...'); const tx = await token.approve(PERMIT2, ethers.MaxUint256); await tx.wait(); console.log(' Done.'); } else { console.log('Step 1: Token already approved to Permit2.'); } // --- Step 2: Permit2 → approve Universal Router --- const [p2Amount, p2Expiration] = await permit2.allowance( userAddress, tokenIn, UNIVERSAL_ROUTER ); const now = Math.floor(Date.now() / 1000); if (p2Amount < BigInt(amount) || Number(p2Expiration) < now) { console.log('Step 2: Approving Universal Router via Permit2...'); const tx = await permit2.approve( tokenIn, UNIVERSAL_ROUTER, ethers.MaxUint160, now + 365 * 24 * 60 * 60 ); await tx.wait(); console.log(' Done.'); } else { console.log('Step 2: Universal Router already approved via Permit2.'); } // --- Step 3: Get quote --- console.log('Step 3: Fetching quote...'); const quoteParams = new URLSearchParams({ tokenIn, tokenOut, tradeType: tradeType.toString(), }); if (tradeType === TradeType.EXACT_INPUT) { quoteParams.set('amountIn', amount); } else { quoteParams.set('amountOut', amount); } const quoteRes = await fetch(`${API_BASE}/api/v1/quote?${quoteParams}`); const quote = await quoteRes.json(); if (quote.error) throw new Error(`Quote failed: ${quote.error}`); console.log(` Route: ${quote.route.protocol} (${quote.route.hops} hop)`); console.log(` Amount in: ${quote.amountIn}`); console.log(` Amount out: ${quote.amountOut}`); console.log(` Price impact: ${quote.priceImpact}%`); // --- Step 4: Get swap calldata --- console.log('Step 4: Generating calldata...'); const swapBody: Record = { tokenIn, tokenOut, tradeType, slippage, recipient: userAddress, deadline: 1200, }; if (tradeType === TradeType.EXACT_INPUT) { swapBody.amountIn = amount; } else { swapBody.amountOut = amount; } const swapRes = await fetch(`${API_BASE}/api/v1/swap`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(swapBody), }); const swapData = await swapRes.json(); if (swapData.error) throw new Error(`Swap API failed: ${swapData.error}`); if (tradeType === TradeType.EXACT_INPUT) { console.log(` Min output: ${swapData.quote.minAmountOut}`); } else { console.log(` Max input: ${swapData.quote.maxAmountIn}`); } // --- Step 5: Execute --- console.log('Step 5: Sending transaction...'); const tx = await signer.sendTransaction({ to: swapData.to, data: swapData.data, value: swapData.value, }); console.log(` Tx hash: ${tx.hash}`); const receipt = await tx.wait(); console.log(` Confirmed in block ${receipt!.blockNumber}`); return receipt!; } // === Usage === async function main() { // Connect wallet (example using private key — use a secure method in production) const provider = new ethers.JsonRpcProvider(RPC_URL); const signer = new ethers.Wallet('YOUR_PRIVATE_KEY', provider); const TOKEN_A = '0xB190a6A7fC2873f1Abf145279eD664348d5Ef630'; const TOKEN_B = '0x3Ac3B30b7f18AEFD4590D7FE4d9C5944aaeB7220'; // Exact Input: sell 1 TOKEN_A for TOKEN_B await executeSwap( signer, TOKEN_A, TOKEN_B, ethers.parseEther('1').toString(), TradeType.EXACT_INPUT, 0.5 ); } main().catch(console.error); ``` ## Key Points 1. **Steps 1 & 2 are one-time** per token — you can skip them for subsequent swaps 2. **Native KAS**: Use the WKAS address as `tokenIn`, and skip Steps 1 & 2. The API sets `value` automatically. 3. **Error handling**: Always check for `quote.error` and `swapData.error` before proceeding 4. **Slippage**: The on-chain `minAmountOut` / `maxAmountIn` protection is encoded in the calldata # Error Handling Source: https://docs.krokoswap.io/guides/error-handling Common errors, error codes, and handling strategies # Error Handling This guide covers common errors you'll encounter when integrating with Kroko DEX and how to handle them. ## API Errors When an API call fails, the response includes an error message: ```json theme={null} { "error": "No route found" } ``` | Error | Cause | Solution | | ------------------------- | ------------------------------------ | ---------------------------------------------- | | `No route found` | No liquidity path between the tokens | Verify token addresses; check that pools exist | | `Invalid token address` | Malformed address | Use checksummed or lowercase hex addresses | | `Amount must be positive` | Zero or negative amount | Provide a valid positive amount string | | `Invalid tradeType` | tradeType is not 0 or 1 | Use `0` (Exact Input) or `1` (Exact Output) | ## Transaction Errors ### On-Chain Reverts These errors occur when the transaction is submitted but reverts on-chain: | Error | Cause | Solution | | ----------------------- | -------------------------------------- | ------------------------------------------------------ | | `TRANSFER_FROM_FAILED` | Insufficient token approval or balance | Check Permit2 approval (Steps 1 & 2) and token balance | | `EXPIRED` | Transaction deadline exceeded | Use a longer deadline or resubmit quickly | | `V3_INVALID_AMOUNT_OUT` | Output below `minAmountOut` (slippage) | Increase slippage tolerance or reduce trade size | | `INSUFFICIENT_ETH` | Not enough native KAS sent as `value` | Ensure `value` matches the required input amount | ### Wallet Errors ```typescript theme={null} try { const tx = await signer.sendTransaction(swapData); await tx.wait(); } catch (error) { if (error.code === 'ACTION_REJECTED' || error.code === 4001) { // User rejected the transaction in their wallet console.log('Transaction cancelled by user'); return; } if (error.code === 'ERR_NETWORK') { // Network connectivity issue console.log('Network error — check your connection'); return; } // Other errors console.error('Transaction failed:', error.message); } ``` ## Approval Errors Common issues with the approval flow: | Symptom | Cause | Fix | | ---------------------------------------- | ---------------------------------------- | ------------------------------------------------------ | | Swap fails after successful approval | Approved wrong contract | Ensure Step 1 approves Permit2 (not Universal Router) | | Permit2 approval succeeds but swap fails | Permit2 sub-approval expired | Check expiration and re-approve if needed | | "Insufficient allowance" | Amount approved is less than swap amount | Approve `MaxUint256` (Step 1) or `MaxUint160` (Step 2) | ### Checking Approval State ```typescript theme={null} // Check ERC-20 → Permit2 approval const tokenAllowance = await token.allowance(userAddress, PERMIT2); console.log('Token → Permit2:', tokenAllowance.toString()); // Check Permit2 → Universal Router approval const [amount, expiration] = await permit2.allowance( userAddress, tokenAddress, UNIVERSAL_ROUTER ); console.log('Permit2 → Router:', amount.toString()); console.log('Expiration:', new Date(Number(expiration) * 1000)); ``` ## Best Practices ### Pre-flight Checks Before executing a swap, verify: ```typescript theme={null} async function preflightCheck(userAddress, tokenIn, amountIn) { // 1. Check balance const balance = await token.balanceOf(userAddress); if (balance < amountIn) { throw new Error(`Insufficient balance: have ${balance}, need ${amountIn}`); } // 2. Check ERC-20 approval to Permit2 const allowance = await token.allowance(userAddress, PERMIT2); if (allowance < amountIn) { return { needsApproval: 'token' }; } // 3. Check Permit2 sub-approval const [amount, expiration] = await permit2.allowance( userAddress, tokenIn, UNIVERSAL_ROUTER ); if (amount < amountIn || expiration < Math.floor(Date.now() / 1000)) { return { needsApproval: 'permit2' }; } return { needsApproval: null }; } ``` ### Retry Strategy * **API errors**: Safe to retry immediately (transient network issues) * **On-chain reverts**: Do NOT retry blindly — diagnose the cause first * **User rejection**: Do not retry — wait for user to initiate again * **Slippage errors**: Re-fetch the quote (price may have changed) and retry with fresh calldata # Execute a Swap Source: https://docs.krokoswap.io/guides/execute-a-swap Step-by-step guide to executing a token swap # Execute a Swap This guide walks through the complete 5-step process to execute a token swap on Kroko DEX. ## Overview ``` Step 1: Approve token → Permit2 (one-time per token) Step 2: Permit2 → approve Universal Router (one-time per token) Step 3: Get quote from API Step 4: Get swap calldata from API Step 5: Send transaction ``` ## Prerequisites ```typescript theme={null} import { ethers } from 'ethers'; const PERMIT2 = '0x2E1987F680FD7Bc8B33d3Bf94f12B988A0B50034'; const UNIVERSAL_ROUTER = '0xefeCc1c2dE3BfE4C6D43030F2AcDD5C3cE279024'; const API_BASE = 'https://dex.kasplex.org/swap-api'; const ERC20_ABI = [ 'function allowance(address,address) view returns (uint256)', 'function approve(address,uint256) returns (bool)' ]; const PERMIT2_ABI = [ 'function approve(address token, address spender, uint160 amount, uint48 expiration)', 'function allowance(address owner, address token, address spender) view returns (uint160, uint48, uint48)' ]; ``` ## Step 1: Approve Token to Permit2 Each token only needs to be approved to Permit2 once. We recommend approving the maximum amount. ```typescript theme={null} const tokenContract = new ethers.Contract(tokenIn, ERC20_ABI, signer); const userAddress = await signer.getAddress(); // Check existing approval const allowance = await tokenContract.allowance(userAddress, PERMIT2); if (allowance < amountIn) { console.log('Approving token to Permit2...'); const tx = await tokenContract.approve(PERMIT2, ethers.MaxUint256); await tx.wait(); } ``` ## Step 2: Permit2 Approve Universal Router Grant the Universal Router permission to spend your token through Permit2. ```typescript theme={null} const permit2 = new ethers.Contract(PERMIT2, PERMIT2_ABI, signer); // Check existing Permit2 approval const [amount, expiration] = await permit2.allowance( userAddress, tokenIn, UNIVERSAL_ROUTER ); const now = Math.floor(Date.now() / 1000); if (amount < amountIn || expiration < now) { console.log('Approving Universal Router via Permit2...'); const tx = await permit2.approve( tokenIn, UNIVERSAL_ROUTER, ethers.MaxUint160, // Max uint160 now + 365 * 24 * 60 * 60 // 1 year ); await tx.wait(); } ``` ## Step 3: Get Quote Fetch the optimal swap route and expected output. ```typescript Exact Input theme={null} // "I want to sell 1 token — how much do I get?" const params = new URLSearchParams({ tokenIn, tokenOut, amountIn: '1000000000000000000', tradeType: '0' }); const res = await fetch(`${API_BASE}/api/v1/quote?${params}`); const quote = await res.json(); console.log(`Expected output: ${quote.amountOut}`); console.log(`Price impact: ${quote.priceImpact}%`); console.log(`Route: ${quote.route.protocol} (${quote.route.hops} hop)`); ``` ```typescript Exact Output theme={null} // "I want to buy 1 token — how much do I need?" const params = new URLSearchParams({ tokenIn, tokenOut, amountOut: '1000000000000000000', tradeType: '1' }); const res = await fetch(`${API_BASE}/api/v1/quote?${params}`); const quote = await res.json(); console.log(`Required input: ${quote.amountIn}`); ``` ## Step 4: Get Swap Calldata Request the encoded transaction data from the Swap API. ```typescript theme={null} const swapRes = await fetch(`${API_BASE}/api/v1/swap`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tokenIn, tokenOut, amountIn: '1000000000000000000', tradeType: 0, slippage: 0.5, // 0.5% slippage tolerance recipient: userAddress, deadline: 1200 // 20 minutes }) }); const swapData = await swapRes.json(); // Verify slippage protection console.log(`Min output: ${swapData.quote.minAmountOut}`); ``` ## Step 5: Execute the Swap Send the transaction using the calldata from the API. ```typescript theme={null} const tx = await signer.sendTransaction({ to: swapData.to, // Universal Router data: swapData.data, // Encoded calldata value: swapData.value // 0 for ERC-20, or KAS amount for native token }); const receipt = await tx.wait(); console.log(`Swap executed: ${receipt.hash}`); ``` ## Native KAS Swaps When selling native KAS, the flow is the same except: * Use the **WKAS address** as `tokenIn` in the API calls * **Skip Steps 1 and 2** (no approval needed for native KAS) * The API returns a non-zero `value` — include it in the transaction ```typescript theme={null} // Selling KAS for a token const swapData = await getSwapData({ tokenIn: WKAS_ADDRESS, // Use WKAS address, not zero address tokenOut: targetToken, amountIn: '1000000000000000000', // ... }); // value will be non-zero const tx = await signer.sendTransaction({ to: swapData.to, data: swapData.data, value: swapData.value // "1000000000000000000" (1 KAS) }); ``` ## Error Handling | Error | Cause | Solution | | ----------------------------- | ---------------------------------------- | ---------------------------------------- | | `TRANSFER_FROM_FAILED` | Missing approval or insufficient balance | Check Steps 1 & 2 | | `EXPIRED` | Deadline passed | Use a longer deadline or resubmit | | `V3_INVALID_AMOUNT_OUT` | Slippage exceeded | Increase slippage tolerance | | `No route found` | No liquidity path exists | Check token addresses and pool existence | | `ACTION_REJECTED` (code 4001) | User rejected in wallet | No action needed | See [Error Handling](/guides/error-handling) for a comprehensive reference. # Getting Started Source: https://docs.krokoswap.io/guides/getting-started Set up your environment to integrate with Kroko DEX # Getting Started This guide covers everything you need to start integrating with Kroko DEX — connecting to the network, setting up contract references, and making your first API call. ## Prerequisites * A wallet library (e.g., [ethers.js](https://docs.ethers.org/v6/) or [viem](https://viem.sh/)) * Node.js 18+ (for server-side integration) or a modern browser (for frontend) ## 1. Connect to Kasplex Add the Kasplex network to your wallet or provider: ```typescript ethers.js theme={null} import { ethers } from 'ethers'; // Mainnet const provider = new ethers.JsonRpcProvider('https://evmrpc.kasplex.org'); // Testnet const testnetProvider = new ethers.JsonRpcProvider('https://rpc.kasplextest.xyz'); ``` ```typescript viem theme={null} import { createPublicClient, http, defineChain } from 'viem'; const kasplexMainnet = defineChain({ id: 202555, name: 'Kasplex Mainnet', nativeCurrency: { name: 'KAS', symbol: 'KAS', decimals: 18 }, rpcUrls: { default: { http: ['https://evmrpc.kasplex.org'] } }, blockExplorers: { default: { name: 'Explorer', url: 'https://explorer.kasplex.org' } } }); const client = createPublicClient({ chain: kasplexMainnet, transport: http() }); ``` ## 2. Contract Addresses Reference the deployed contracts for the network you're targeting: ```typescript Mainnet theme={null} const CONTRACTS = { PERMIT2: '0x2E1987F680FD7Bc8B33d3Bf94f12B988A0B50034', UNIVERSAL_ROUTER: '0xefeCc1c2dE3BfE4C6D43030F2AcDD5C3cE279024', WKAS: '0x2c2Ae87Ba178F48637acAe54B87c3924F544a83e', V2_FACTORY: '0x4373b7Fcf5059A785843cD224129e01d243Aef71', V2_ROUTER: '0xC7ca845B8302346e1C7227f03bb9EFb35ecD51fe', V3_FACTORY: '0x0dfb1Bb755d872EA1fa4d95E4ad0c2E6317Ce9B9', V3_POSITION_MANAGER: '0x343b244bEDF133D57C61b241557bF29AA32ea4F9', }; ``` ```typescript Testnet theme={null} const CONTRACTS = { PERMIT2: '0xc320bc492Bb56169aBE18D3C0a2048c45febC897', UNIVERSAL_ROUTER: '0x440d7f5FE865eFCcfdCB1ee9a000C114163689ba', WKAS: '0xC065C62a10fB363fD31CA394D632C4Df106566df', V2_FACTORY: '0x497152FfC1FEa1Ff31cc7cEeca4f4b9495b606fB', V2_ROUTER: '0xf2ece243a0EFC0Cd1fcd3386b2f73f16D1378689', V3_FACTORY: '0x6ea7b69cDB0Af4DE7DF60DA08edE2F7E2b8d5924', V3_POSITION_MANAGER: '0xDAF3700A7D80B26d5DD971C3C0e2fB93Ad73219f', }; ``` See [Contract Addresses](/contracts/addresses) for the full table. ## 3. API Base URL The Swap API is available at: ``` https://dex.kasplex.org/swap-api ``` ## 4. Your First API Call Fetch the token list to verify connectivity: ```typescript theme={null} const response = await fetch('https://dex.kasplex.org/api/v1/tokens2?limit=10'); const data = await response.json(); console.log(data.tokens); // [{ address: '0x...', symbol: 'WKAS', name: 'Wrapped KAS', decimals: 18 }, ...] ``` ## 5. Get a Quote Try fetching a swap quote: ```typescript theme={null} const params = new URLSearchParams({ tokenIn: CONTRACTS.WKAS, tokenOut: '0xB190a6A7fC2873f1Abf145279eD664348d5Ef630', // Example token amountIn: '1000000000000000000', // 1 KAS tradeType: '0' }); const quote = await fetch(`https://dex.kasplex.org/swap-api/api/v1/quote?${params}`); const data = await quote.json(); console.log(`1 KAS = ${data.executionPrice} tokens`); ``` ## Next Steps Complete walkthrough of the 5-step swap process. Add liquidity to V2 or V3 pools. # Provide Liquidity (V2) Source: https://docs.krokoswap.io/guides/provide-liquidity-v2 Add and remove liquidity from V2 constant product pools # Provide Liquidity (V2) V2 pools use a simple constant-product model. LPs deposit both tokens in the current reserve ratio and receive fungible LP tokens representing their share. ## Add Liquidity ### 1. Approve Tokens Approve both tokens to the V2 Router: ```typescript theme={null} const V2_ROUTER = '0xC7ca845B8302346e1C7227f03bb9EFb35ecD51fe'; // Mainnet await tokenA.approve(V2_ROUTER, amountA); await tokenB.approve(V2_ROUTER, amountB); ``` ### 2. Call addLiquidity ```typescript theme={null} const V2_ROUTER_ABI = [ '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)' ]; const router = new ethers.Contract(V2_ROUTER, V2_ROUTER_ABI, signer); const deadline = Math.floor(Date.now() / 1000) + 1200; // 20 minutes // ERC-20 / ERC-20 pair const tx = await router.addLiquidity( tokenA.target, tokenB.target, amountADesired, amountBDesired, amountADesired * 99n / 100n, // 1% slippage amountBDesired * 99n / 100n, userAddress, deadline ); ``` ### 3. With Native KAS Use `addLiquidityETH` and send KAS as `value`: ```typescript theme={null} const tx = await router.addLiquidityETH( tokenAddress, amountTokenDesired, amountTokenDesired * 99n / 100n, amountKASDesired * 99n / 100n, userAddress, deadline, { value: amountKASDesired } ); ``` ## Remove Liquidity ### 1. Approve LP Token ```typescript theme={null} const lpToken = new ethers.Contract(pairAddress, ERC20_ABI, signer); await lpToken.approve(V2_ROUTER, lpAmount); ``` ### 2. Call removeLiquidity ```typescript theme={null} const REMOVE_ABI = [ '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)' ]; const router = new ethers.Contract(V2_ROUTER, REMOVE_ABI, signer); // ERC-20 / ERC-20 pair const tx = await router.removeLiquidity( tokenA, tokenB, lpAmount, 0, // amountAMin (set to 0 for simplicity; use slippage in production) 0, // amountBMin userAddress, deadline ); ``` ### 3. With Native KAS Use `removeLiquidityETH` to receive KAS instead of WKAS: ```typescript theme={null} const tx = await router.removeLiquidityETH( tokenAddress, lpAmount, 0, 0, userAddress, deadline ); ``` ## Calculating Amounts To add liquidity at the current price ratio, first query the pool reserves: ```typescript theme={null} const PAIR_ABI = [ 'function getReserves() view returns (uint112, uint112, uint32)', 'function token0() view returns (address)' ]; const pair = new ethers.Contract(pairAddress, PAIR_ABI, provider); const [reserve0, reserve1] = await pair.getReserves(); const token0 = await pair.token0(); // If you want to deposit amountA of tokenA: const isToken0 = tokenA.toLowerCase() === token0.toLowerCase(); const amountB = isToken0 ? (amountA * reserve1) / reserve0 : (amountA * reserve0) / reserve1; ``` ## Key Points * First LP sets the initial price — deposit tokens at the desired ratio * Subsequent LPs must match the current reserve ratio * LP tokens are ERC-20 and can be transferred or used in other protocols * Fees (0.3%) auto-compound into the pool — no claiming needed * Subject to [impermanent loss](/concepts/v2/lp-tokens#impermanent-loss) # Provide Liquidity (V3) Source: https://docs.krokoswap.io/guides/provide-liquidity-v3 Create, manage, and close V3 concentrated liquidity positions # Provide Liquidity (V3) V3 allows LPs to concentrate liquidity within a specific price range for higher capital efficiency. Each position is an NFT with unique parameters. ## Create a Position ### 1. Choose Pool Parameters * **Token pair**: The two tokens * **Fee tier**: 0.01%, 0.05%, 0.3%, or 1% (see [Fee Tiers](/concepts/v3/fee-tiers)) * **Price range**: `[tickLower, tickUpper]` (see [Ticks and Ranges](/concepts/v3/ticks-and-ranges)) ### 2. Get Current Pool State ```typescript theme={null} const V3_FACTORY = '0x0dfb1Bb755d872EA1fa4d95E4ad0c2E6317Ce9B9'; // Mainnet const V3_POSITION_MANAGER = '0x343b244bEDF133D57C61b241557bF29AA32ea4F9'; const FACTORY_ABI = ['function getPool(address,address,uint24) view returns (address)']; const POOL_ABI = [ 'function slot0() view returns (uint160, int24, uint16, uint16, uint16, uint8, bool)', 'function tickSpacing() view returns (int24)' ]; const factory = new ethers.Contract(V3_FACTORY, FACTORY_ABI, provider); const poolAddress = await factory.getPool(token0, token1, 3000); const pool = new ethers.Contract(poolAddress, POOL_ABI, provider); const [sqrtPriceX96, currentTick] = await pool.slot0(); const tickSpacing = await pool.tickSpacing(); ``` ### 3. Calculate Tick Range Align your desired price range to the pool's tick spacing: ```typescript theme={null} function nearestUsableTick(tick, tickSpacing) { return Math.round(tick / tickSpacing) * tickSpacing; } // Example: ±10% around current price const tickLower = nearestUsableTick(currentTick - 2000, tickSpacing); const tickUpper = nearestUsableTick(currentTick + 2000, tickSpacing); ``` ### 4. Approve Tokens ```typescript theme={null} await token0Contract.approve(V3_POSITION_MANAGER, amount0Desired); await token1Contract.approve(V3_POSITION_MANAGER, amount1Desired); ``` ### 5. Mint Position ```typescript theme={null} const PM_ABI = [ 'function mint((address,address,uint24,int24,int24,uint256,uint256,uint256,uint256,address,uint256)) payable returns (uint256,uint128,uint256,uint256)' ]; const pm = new ethers.Contract(V3_POSITION_MANAGER, PM_ABI, signer); const deadline = Math.floor(Date.now() / 1000) + 1200; const tx = await pm.mint({ token0, token1, fee: 3000, tickLower, tickUpper, amount0Desired, amount1Desired, amount0Min: amount0Desired * 99n / 100n, amount1Min: amount1Desired * 99n / 100n, recipient: userAddress, deadline }); const receipt = await tx.wait(); // Parse the tokenId from the receipt events ``` `token0` must have a lower address than `token1`. Sort them before calling. ### With Native KAS If one token is KAS, send it as `value` instead of approving WKAS: ```typescript theme={null} const tx = await pm.mint(params, { value: kasAmount }); ``` ## Increase Liquidity Add more tokens to an existing position (same tick range): ```typescript theme={null} const tx = await pm.increaseLiquidity({ tokenId, amount0Desired, amount1Desired, amount0Min: 0, amount1Min: 0, deadline }); ``` ## Collect Fees Claim accumulated trading fees: ```typescript theme={null} const tx = await pm.collect({ tokenId, recipient: userAddress, amount0Max: ethers.MaxUint128, amount1Max: ethers.MaxUint128 }); ``` ## Remove Liquidity ### 1. Decrease Liquidity ```typescript theme={null} const tx = await pm.decreaseLiquidity({ tokenId, liquidity: liquidityToRemove, // Use full liquidity for complete removal amount0Min: 0, amount1Min: 0, deadline }); ``` ### 2. Collect the Tokens After decreasing, tokens are held by the Position Manager. Call `collect()` to withdraw: ```typescript theme={null} const tx = await pm.collect({ tokenId, recipient: userAddress, amount0Max: ethers.MaxUint128, amount1Max: ethers.MaxUint128 }); ``` ### 3. Burn the NFT (Optional) After removing all liquidity and collecting all tokens/fees: ```typescript theme={null} const tx = await pm.burn(tokenId); ``` ## Single-Sided Positions You can create positions entirely above or below the current price: | Range | Deposit | Behavior | | ----------------------------------------------- | ----------- | -------------------------- | | Above current price (`tickLower > currentTick`) | Token0 only | Acts as a limit sell order | | Below current price (`tickUpper < currentTick`) | Token1 only | Acts as a limit buy order | ```typescript theme={null} // Limit sell: provide only token0, range above current price const tx = await pm.mint({ token0, token1, fee: 3000, tickLower: currentTick + 60, // Above current tickUpper: currentTick + 600, amount0Desired: sellAmount, amount1Desired: 0, // No token1 needed amount0Min: 0, amount1Min: 0, recipient: userAddress, deadline }); ``` ## Key Points * Each position is an NFT — track `tokenId` for future operations * Fees must be explicitly collected (they don't auto-compound) * Out-of-range positions earn no fees until price re-enters the range * Narrower ranges = higher capital efficiency but more active management needed * Use `multicall()` on the Position Manager to batch decrease + collect in one transaction # Query Prices Source: https://docs.krokoswap.io/guides/query-prices How to get token prices and swap quotes # Query Prices There are multiple ways to query token prices on Kroko DEX, depending on your use case. ## Method 1: Quote API (Recommended) The most accurate way to get a price is to request a swap quote. This considers all available routes and gives you the actual execution price. ```typescript theme={null} const params = new URLSearchParams({ tokenIn: '0xTokenA', tokenOut: '0xTokenB', amountIn: '1000000000000000000', // 1 token tradeType: '0' }); const res = await fetch(`https://dex.kasplex.org/swap-api/api/v1/quote?${params}`); const quote = await res.json(); console.log(`Price: 1 TokenA = ${quote.executionPrice} TokenB`); console.log(`Price impact: ${quote.priceImpact}%`); ``` **Best for:** Real-time swap pricing, showing users what they'll receive. The execution price varies with trade size due to price impact. For reference prices, use a small amount. ## Method 2: Price API Get the price directly without specifying an amount: ```typescript theme={null} // Single token price const res = await fetch('https://dex.kasplex.org/swap-api/api/v1/price/0xTokenAddress'); // Pair price const res = await fetch('https://dex.kasplex.org/swap-api/api/v1/price/pair/0xToken0/0xToken1'); ``` **Best for:** Dashboard displays, portfolio valuation. ## Method 3: On-Chain (V2) Read pool reserves directly for V2 pools: ```typescript theme={null} const PAIR_ABI = [ 'function getReserves() view returns (uint112, uint112, uint32)', 'function token0() view returns (address)' ]; const pair = new ethers.Contract(pairAddress, PAIR_ABI, provider); const [reserve0, reserve1] = await pair.getReserves(); const token0 = await pair.token0(); // Price of token0 in terms of token1 const price = Number(reserve1) / Number(reserve0); ``` **Best for:** Trustless on-chain price reads, oracle-like use cases. ## Method 4: On-Chain (V3) Read the current price from a V3 pool: ```typescript theme={null} const POOL_ABI = [ 'function slot0() view returns (uint160, int24, uint16, uint16, uint16, uint8, bool)' ]; const pool = new ethers.Contract(poolAddress, POOL_ABI, provider); const [sqrtPriceX96, tick] = await pool.slot0(); // Convert sqrtPriceX96 to price const price = (Number(sqrtPriceX96) / 2 ** 96) ** 2; // Adjust for decimals if tokens have different decimal places const adjustedPrice = price * (10 ** (decimals0 - decimals1)); ``` **Best for:** V3-specific price feeds, tick-level precision. ## Historical Prices For historical K-line data: ```typescript theme={null} const res = await fetch( 'https://dex.kasplex.org/swap-api/api/v1/price/history/0xPoolAddress?limit=100' ); ``` ## Comparison | Method | Accuracy | Latency | Trust Model | Best For | | ----------- | --------------------------- | ---------- | --------------- | ------------------ | | Quote API | Highest (considers routing) | \~100ms | Centralized API | Swap UIs | | Price API | High | \~50ms | Centralized API | Dashboards | | On-chain V2 | Pool-level | \~2s (RPC) | Trustless | Oracles | | On-chain V3 | Pool-level | \~2s (RPC) | Trustless | Advanced analytics |