Skip to Content
Integration Guide

Integration Guide

This guide shows how to connect to HyperEVM, read pool state, quote prices, swap, and manage concentrated‑liquidity positions

Before You Start

What this section does Explains the environment you need and how to set up a simple project that talks to Funnel DEX on HyperEVM using ethers.

Requirements

  • Node.js ≥ 18 (native ESM; allows top‑level await if desired)

  • npm or pnpm

npm init -y npm i ethers dotenv

Create a .env file:

HYPEREVM_RPC_URL=https://your-hyperevm-rpc.example PRIVATE_KEY=0xabc... # wallet that will send transactions TOKEN_A=0x... # e.g., some ERC20 TOKEN_B=0x... # e.g., some ERC20 WETH=0x... # wrapped native token on Hyperevm

Version note: examples below use ethers v6 (e.g., ethers.parseUnits). If you’re on ethers v5, replace ethers.parseUnits with ethers.utils.parseUnits, ethers.formatUnits with ethers.utils.formatUnits, and adapt imports accordingly.


Addresses

Funnel DEX core + helper contracts on Hyperevm:

export const FunDex = { QuoterV2: "0xD77f1164795d94a96a39A9e0855c529D22718603", SwapRouter02: "0x2caD8dc8C4afa881dc3d741117ca29527dC55dFB", CoreFactory: "0x881A87925D80fBed9618873D7E2dD34Cb9006595", TickLens: "0xed1C9609200ED4afD778AB38eeE147B1e314F6EE", NFTDescriptorLibrary: "0x9737A170792882514F0Fa8C66230D71d43f472Ec", NonfungibleTokenPositionDescriptor: "0xd2f214E6F2289c106452eb80A46D9a3bB1aC1701", PositionManager: "0xA6Bee4100Ba2FFD9e202f77fa499A10650583f66", };

Tokens (from .env):

export const TOKEN_A = process.env.TOKEN_A; // e.g., USDC export const TOKEN_B = process.env.TOKEN_B; // e.g., DAI export const WETH = process.env.WETH; // wrapped native token

1) Connect to contracts

Create a single index.js (ESM). If using TypeScript, the code is valid TS, too.

// index.js (ESM) import "dotenv/config"; import { ethers } from "ethers"; // 1) Provider & Signer const provider = new ethers.JsonRpcProvider(process.env.HYPEREVM_RPC_URL); const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider); // 2) FunDex Addresses (on Hyperevm) export const FunDex = { QuoterV2: "0xD77f1164795d94a96a39A9e0855c529D22718603", SwapRouter02: "0x2caD8dc8C4afa881dc3d741117ca29527dC55dFB", CoreFactory: "0x881A87925D80fBed9618873D7E2dD34Cb9006595", TickLens: "0xed1C9609200ED4afD778AB38eeE147B1e314F6EE", NFTDescriptorLibrary: "0x9737A170792882514F0Fa8C66230D71d43f472Ec", NonfungibleTokenPositionDescriptor: "0xd2f214E6F2289c106452eb80A46D9a3bB1aC1701", PositionManager: "0xA6Bee4100Ba2FFD9e202f77fa499A10650583f66", }; // 3) Tokens export const TOKEN_A = process.env.TOKEN_A; // e.g., USDC export const TOKEN_B = process.env.TOKEN_B; // e.g., DAI export const WETH = process.env.WETH; // wrapped native token // 4) Minimal ABIs used in the examples export const ERC20_ABI = [ "function approve(address spender, uint256 value) returns (bool)", "function allowance(address owner, address spender) view returns (uint256)", "function balanceOf(address) view returns (uint256)", "function decimals() view returns (uint8)", "function symbol() view returns (string)", ]; export const WETH9_ABI = [ "function deposit() payable", "function withdraw(uint256)", "function approve(address,uint256) returns (bool)", "function allowance(address,address) view returns (uint256)", "function balanceOf(address) view returns (uint256)", "function decimals() view returns (uint8)", ]; export const FACTORY_ABI = [ "function getPool(address,address,uint24) view returns (address)", ]; export const POOL_ABI = [ "function slot0() view returns (uint160 sqrtPriceX96,int24 tick,uint16,uint16,uint16,uint8,bool)", "function liquidity() view returns (uint128)", "function tickSpacing() view returns (int24)", "function fee() view returns (uint24)", "function token0() view returns (address)", "function token1() view returns (address)", ]; export const TICK_LENS_ABI = [ "function getPopulatedTicksInWord(address pool, int16 word) view returns (tuple(int24 tick,int128 liquidityNet,uint128 liquidityGross)[])", ]; export const QUOTER_V2_ABI = [ "function quoteExactInputSingle((address tokenIn,address tokenOut,uint24 fee,uint256 amountIn,uint160 sqrtPriceLimitX96)) view returns (uint256 amountOut,uint160 sqrtPriceX96After,uint32 initializedTicksCrossed,uint256 gasEstimate)", "function quoteExactInput(bytes path, uint256 amountIn) view returns (uint256 amountOut,uint160[] sqrtPriceX96AfterList,uint32[] initializedTicksCrossedList,uint256 gasEstimate)", ]; export const ROUTER_ABI = [ "function exactInputSingle((address tokenIn,address tokenOut,uint24 fee,address recipient,uint256 deadline,uint256 amountIn,uint256 amountOutMinimum,uint160 sqrtPriceLimitX96)) payable returns (uint256 amountOut)", "function refundETH() payable", // optional, supported on many routers "function unwrapWETH9(uint256,address) payable", // optional helper ]; export const POSITION_MANAGER_ABI = [ "function mint((address token0,address token1,uint24 fee,int24 tickLower,int24 tickUpper,uint256 amount0Desired,uint256 amount1Desired,uint256 amount0Min,uint256 amount1Min,address recipient,uint256 deadline)) returns (uint256 tokenId,uint128 liquidity,uint256 amount0,uint256 amount1)", "function increaseLiquidity((uint256 tokenId,uint256 amount0Desired,uint256 amount1Desired,uint256 amount0Min,uint256 amount1Min,uint256 deadline)) returns (uint128 liquidity,uint256 amount0,uint256 amount1)", "function decreaseLiquidity((uint256 tokenId,uint128 liquidity,uint256 amount0Min,uint256 amount1Min,uint256 deadline)) returns (uint256 amount0,uint256 amount1)", "function collect((uint256 tokenId,address recipient,uint128 amount0Max,uint128 amount1Max)) 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 ownerOf(uint256 tokenId) view returns (address)", "function setApprovalForAll(address operator,bool approved)", ];

2) View Pool Info & State (price, liquidity, ticks)

What this function does Given two token addresses and a fee tier, finds the pool address and reads key state: current price (via sqrtPriceX96), current tick, total liquidity, and a sample of populated ticks via TickLens.

function sqrtPriceX96ToPrice(sqrtPriceX96, decimals0, decimals1) { // returns price of token1 in terms of token0 (float) const Q96 = 2n ** 96n; const num = sqrtPriceX96 * sqrtPriceX96 * 10n ** BigInt(decimals0) * 10n ** 18n; const den = Q96 * Q96 * 10n ** BigInt(decimals1); return Number(ethers.formatUnits(num / den, 18)); // numeric display } function nearestUsableTick(tick, tickSpacing) { return Math.floor(tick / tickSpacing) * tickSpacing; } export async function viewPoolState(tokenA, tokenB, fee) { const factory = new ethers.Contract( FunDex.CoreFactory, FACTORY_ABI, provider ); const poolAddr = await factory.getPool(tokenA, tokenB, fee); if (poolAddr === ethers.ZeroAddress) throw new Error("Pool not initialized"); const pool = new ethers.Contract(poolAddr, POOL_ABI, provider); const [token0, token1, poolFee, tickSpacing, liquidity, slot0] = await Promise.all([ pool.token0(), pool.token1(), pool.fee(), pool.tickSpacing(), pool.liquidity(), pool.slot0(), ]); const [dec0, dec1] = await Promise.all([ new ethers.Contract(token0, ERC20_ABI, provider).decimals(), new ethers.Contract(token1, ERC20_ABI, provider).decimals(), ]); const price1Per0 = sqrtPriceX96ToPrice(slot0.sqrtPriceX96, dec0, dec1); const price0Per1 = 1 / price1Per0; console.log("pool:", poolAddr); console.log("fee (hundredths of a bip):", poolFee); console.log("tickSpacing:", tickSpacing.toString()); console.log("liquidity:", liquidity.toString()); console.log("currentTick:", slot0.tick); console.log(`token1 per token0: ${price1Per0}`); console.log(`token0 per token1: ${price0Per1}`); // Sample populated ticks in the current 256-tick word const lens = new ethers.Contract(FunDex.TickLens, TICK_LENS_ABI, provider); const word = Math.floor(Number(slot0.tick) / 256); const ticks = await lens.getPopulatedTicksInWord(poolAddr, word); console.log("populated ticks (sample):", ticks.slice(0, 5)); } // example // await viewPoolState(TOKEN_A, TOKEN_B, 3000);

3) Quote Prices Before Execution (Quoter)

Simulate a swap off‑chain to get a quote for amountIn (no slippage applied). This is read‑only and does not move funds.

export async function quoteExactInputSingle( tokenIn, tokenOut, fee, amountInHuman ) { const quoter = new ethers.Contract(FunDex.QuoterV2, QUOTER_V2_ABI, provider); const decIn = await new ethers.Contract( tokenIn, ERC20_ABI, provider ).decimals(); const decOut = await new ethers.Contract( tokenOut, ERC20_ABI, provider ).decimals(); const amountIn = ethers.parseUnits(amountInHuman, decIn); const { 0: amountOut } = await quoter.quoteExactInputSingle.staticCall({ tokenIn, tokenOut, fee, amountIn, sqrtPriceLimitX96: 0n, }); console.log( `quote: ${amountInHuman} in -> ~${ethers.formatUnits( amountOut, decOut )} out` ); return amountOut; // bigint } // example // await quoteExactInputSingle(TOKEN_A, TOKEN_B, 3000, '1.0');

4) Swap Tokens for Tokens

Approve the router to spend tokenIn, obtains a quote, computes a minimum amount‑out based on slippage tolerance, then executes a single‑hop swap.

export async function swapTokensForTokens( tokenIn, tokenOut, fee, amountInHuman, slippageBps = 50 ) { const ercIn = new ethers.Contract(tokenIn, ERC20_ABI, wallet); const ercOut = new ethers.Contract(tokenOut, ERC20_ABI, provider); const [decIn, decOut] = await Promise.all([ ercIn.decimals(), ercOut.decimals(), ]); const amountIn = ethers.parseUnits(amountInHuman, decIn); // 1) Approve router if needed const allowance = await ercIn.allowance(wallet.address, FunDex.SwapRouter02); if (allowance < amountIn) { const txApprove = await ercIn.approve(FunDex.SwapRouter02, amountIn); await txApprove.wait(); } // 2) Quote and set min-out const quotedOut = await (async () => { const quoter = new ethers.Contract( FunDex.QuoterV2, QUOTER_V2_ABI, provider ); const { 0: out } = await quoter.quoteExactInputSingle.staticCall({ tokenIn, tokenOut, fee, amountIn, sqrtPriceLimitX96: 0n, }); return out; })(); const amountOutMinimum = (quotedOut * BigInt(10_000 - slippageBps)) / 10_000n; // 3) Execute swap const router = new ethers.Contract(FunDex.SwapRouter02, ROUTER_ABI, wallet); const params = { tokenIn, tokenOut, fee, recipient: wallet.address, deadline: BigInt(Math.floor(Date.now() / 1000) + 1200), amountIn, amountOutMinimum, sqrtPriceLimitX96: 0n, }; const tx = await router.exactInputSingle(params); const receipt = await tx.wait(); console.log("swap tx hash:", receipt.hash); console.log( "min out enforced:", ethers.formatUnits(amountOutMinimum, decOut) ); }

5) Swap Native (ETH) for Token

Perform a native→token swap in one transaction by calling the router’s payable exactInputSingle with msg.value. You do not approve or pre‑wrap; the router handles the native funds internally when tokenIn is the wrapped‑native token address (WETH). We first quote to calculate a safe amountOutMinimum, then call the swap with { value }.

export async function swapEthForTokenPayable( tokenOut, fee, ethInHuman, slippageBps = 50 ) { // read decimals for formatting logs / min-out display const decOut = await new ethers.Contract( tokenOut, ERC20_ABI, provider ).decimals(); // parse native amount to send as msg.value const value = ethers.parseEther(ethInHuman); // quote expected output for slippage-protected min-out const quoter = new ethers.Contract(FunDex.QuoterV2, QUOTER_V2_ABI, provider); const { 0: outQuoted } = await quoter.quoteExactInputSingle.staticCall({ tokenIn: WETH, // wrapped native as input tokenOut, // target ERC20 fee, // e.g., 500, 3000, 10000 amountIn: value, // same as msg.value sqrtPriceLimitX96: 0n, // no price limit }); const amountOutMinimum = (outQuoted * BigInt(10_000 - slippageBps)) / 10_000n; // build swap params const params = { tokenIn: WETH, tokenOut, fee, recipient: wallet.address, deadline: BigInt(Math.floor(Date.now() / 1000) + 1200), // 20 minutes amountIn: value, // must match msg.value amountOutMinimum, sqrtPriceLimitX96: 0n, }; // call the payable variant with msg.value in a single tx (no prior wrap/approve) const router = new ethers.Contract(FunDex.SwapRouter02, ROUTER_ABI, wallet); const tx = await router.exactInputSingle(params, { value }); const receipt = await tx.wait(); console.log("swap ETH->token tx:", receipt.hash); console.log( "min out enforced:", ethers.formatUnits(amountOutMinimum, decOut) ); } /* usage example: await swapEthForTokenPayable('0xTokenOut...', 3000, '0.01', 50); */

6) Provide Liquidity in a Chosen Price Range

Mint a new liquidity position NFT between tickLower and tickUpper. The example centers around the current tick and creates a symmetric range whose width you control in multiples of tickSpacing.

export async function provideLiquidity( tokenA, tokenB, fee, amountAHuman, amountBHuman, widthSteps = 60 ) { // 1) Discover the pool and its current tick & tickSpacing const factory = new ethers.Contract( FunDex.CoreFactory, FACTORY_ABI, provider ); const poolAddr = await factory.getPool(tokenA, tokenB, fee); if (poolAddr === ethers.ZeroAddress) throw new Error("Pool not initialized"); const pool = new ethers.Contract(poolAddr, POOL_ABI, provider); const [token0, token1, slot0, tickSpacing] = await Promise.all([ pool.token0(), pool.token1(), pool.slot0(), pool.tickSpacing(), ]); // 2) Order amounts to match token0/token1 const isA0 = tokenA.toLowerCase() === token0.toLowerCase(); const amount0Human = isA0 ? amountAHuman : amountBHuman; const amount1Human = isA0 ? amountBHuman : amountAHuman; const erc0 = new ethers.Contract(token0, ERC20_ABI, wallet); const erc1 = new ethers.Contract(token1, ERC20_ABI, wallet); const [dec0, dec1] = await Promise.all([erc0.decimals(), erc1.decimals()]); const amount0Desired = ethers.parseUnits(amount0Human, dec0); const amount1Desired = ethers.parseUnits(amount1Human, dec1); // 3) Build a symmetric price range around current tick const currentTick = Number(slot0.tick); const ts = Number(tickSpacing); const halfWidth = widthSteps * ts; const tickLower = nearestUsableTick(currentTick - halfWidth, ts); const tickUpper = nearestUsableTick(currentTick + halfWidth, ts); if (tickLower >= tickUpper) throw new Error("tickLower must be < tickUpper"); // 4) Approvals for PositionManager const pm = new ethers.Contract( FunDex.PositionManager, POSITION_MANAGER_ABI, wallet ); const [allow0, allow1] = await Promise.all([ erc0.allowance(wallet.address, FunDex.PositionManager), erc1.allowance(wallet.address, FunDex.PositionManager), ]); if (allow0 < amount0Desired) await (await erc0.approve(FunDex.PositionManager, amount0Desired)).wait(); if (allow1 < amount1Desired) await (await erc1.approve(FunDex.PositionManager, amount1Desired)).wait(); // 5) Prepare params const params = { token0, token1, fee, tickLower, tickUpper, amount0Desired, amount1Desired, amount0Min: 0n, // set >0 for tighter slippage control amount1Min: 0n, recipient: wallet.address, deadline: BigInt(Math.floor(Date.now() / 1000) + 1200), }; // Optional: simulate to preview returns (tokenId, liquidity, amounts actually used) const preview = await pm.mint.staticCall(params); console.log("preview mint:", preview); // 6) Execute mint const tx = await pm.mint(params); const rc = await tx.wait(); console.log("mint liquidity tx:", rc.hash); }

7) Remove Liquidity from a Position

Reduce (burns) some or all liquidity from a position NFT via decreaseLiquidity, then calls collect to withdraw the tokens.

const MAX_UINT128 = (1n << 128n) - 1n; // helper for collect all export async function removeLiquidity(tokenId, percentBps = 10_000) { const pm = new ethers.Contract( FunDex.PositionManager, POSITION_MANAGER_ABI, wallet ); // 1) Read current liquidity const pos = await pm.positions(tokenId); const liq = pos.liquidity; // 2) Portion to remove (bps: 10_000 = 100%) const liqToRemove = (liq * BigInt(percentBps)) / 10_000n; // Optional: preview expected token amounts const preview = await pm.decreaseLiquidity.staticCall({ tokenId, liquidity: liqToRemove, amount0Min: 0n, amount1Min: 0n, deadline: BigInt(Math.floor(Date.now() / 1000) + 1200), }); console.log("preview decrease:", preview); // 3) Decrease const tx = await pm.decreaseLiquidity({ tokenId, liquidity: liqToRemove, amount0Min: 0n, amount1Min: 0n, deadline: BigInt(Math.floor(Date.now() / 1000) + 1200), }); const rc = await tx.wait(); console.log("decrease liquidity tx:", rc.hash); // 4) Collect tokens (and any fees owed) const tx2 = await pm.collect({ tokenId, recipient: wallet.address, amount0Max: MAX_UINT128, amount1Max: MAX_UINT128, }); const rc2 = await tx2.wait(); console.log("collect after decrease tx:", rc2.hash); }

8) Get Position Details

Fetch a position’s on‑chain data: the token pair, fee tier, tick range, current liquidity, and uncollected (owed) tokens.

export async function getPositionDetails(tokenId) { const pm = new ethers.Contract( FunDex.PositionManager, POSITION_MANAGER_ABI, provider ); const p = await pm.positions(tokenId); console.log("token0:", p.token0); console.log("token1:", p.token1); console.log("fee:", p.fee); console.log("tickLower:", p.tickLower); console.log("tickUpper:", p.tickUpper); console.log("liquidity:", p.liquidity.toString()); console.log("tokensOwed0:", p.tokensOwed0.toString()); console.log("tokensOwed1:", p.tokensOwed1.toString()); return p; }

9) Collect Fees from a Liquidity Position

Transfer any accrued fees and uncollected tokens for a position NFT to the chosen recipient.

export async function collectFees(tokenId, recipient = wallet.address) { const pm = new ethers.Contract( FunDex.PositionManager, POSITION_MANAGER_ABI, wallet ); const tx = await pm.collect({ tokenId, recipient, amount0Max: (1n << 128n) - 1n, // claim all amount1Max: (1n << 128n) - 1n, }); const rc = await tx.wait(); console.log("collect fees tx:", rc.hash); }

Example: Running a Swap Script End‑to‑End Shows a simple main() that wires setup + swap flow, mirroring the style above (initialize, set params, run, print hash).

async function main() { try { console.log("Initializing wallet:", wallet.address); // Example: swap TOKEN_A -> TOKEN_B with min-out from fresh quote const fee = 3000; // 0.3% pool const amountInHuman = "1.0"; // 1 TOKEN_A const slippageBps = 50; // 0.50% console.log("Quoting..."); const quoted = await quoteExactInputSingle( TOKEN_A, TOKEN_B, fee, amountInHuman ); console.log("Quoted out (raw):", quoted.toString()); console.log("Swapping tokens..."); await swapTokensForTokens( TOKEN_A, TOKEN_B, fee, amountInHuman, slippageBps ); console.log("Swap completed!"); } catch (err) { console.error("Error:", err); } } // If you want to run this file directly: // (uncomment the line below) // main();

Run:

node index.js

Copy‑Paste Helpers (Optional)

// Slippage helper: returns amountOutMinimum function applySlippageBps(amountOutQuoted, slippageBps) { return (amountOutQuoted * BigInt(10_000 - slippageBps)) / 10_000n; } // Deadline helper: 20 minutes from now function deadlineSecs(mins = 20) { return BigInt(Math.floor(Date.now() / 1000) + 60 * mins); }
Last updated on