Integration
Quotes and swaps
Quote a trade with V4Quoter and execute it through the Universal Router, with a tested example.
View as MarkdownTrading a Bull Launch coin is an ordinary Uniswap v4 swap on the coin's pool. Quote it with V4Quoter, then send it through the UniversalRouter as one V4_SWAP command. Both addresses are under Contract addresses.
1. Build the PoolKey
Every launch has the same key shape: the two currencies sorted by address, fee 0x800000 (the dynamic-fee flag), tick spacing 60, and the hook. poolKeyOf in Reading token state builds it. For a native-USDC coin, currency0 is always address(0), so a buy is zeroForOne = true. For an ERC-20 base, check which slot the base landed in, or read Hook.baseIsCurrency0(poolId).
2. Quote
V4Quoter.quoteExactInputSingle returns (amountOut, gasEstimate). It is not a view: call it with eth_call (viem's simulateContract), never in a transaction. It runs the hook, so the swap fee and the creator tax are already in the answer.
3. Swap
Encode three v4 actions, SWAP_EXACT_IN_SINGLE (0x06), SETTLE_ALL (0x0c) and TAKE_ALL (0x0f), wrap them in one V4_SWAP command (0x10), and call UniversalRouter.execute(commands, inputs, deadline). TAKE_ALL enforces amountOutMinimum, so the router, not your code, is what holds the slippage bound.
minHopPriceX36, a per-hop price floor that is newer than most Uniswap quickstarts. Encoding the older tuple without it produces calldata the router decodes into garbage. Pass 0 to leave it unset.// Buy a native-USDC coin: quote, then swap. Tested against Arc Testnet.
import { createPublicClient, createWalletClient, http, encodeAbiParameters, encodePacked,
parseAbi, parseAbiParameters, parseEther, zeroAddress } from "viem";
const arc = {
id: 5042002,
name: "Arc Testnet",
nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 },
rpcUrls: { default: { http: ["https://rpc.testnet.arc.io"] } },
contracts: { multicall3: { address: "0xcA11bde05977b3631167028862bE2a173976CA11" } },
};
const client = createPublicClient({ chain: arc, transport: http() });
// const wallet = createWalletClient({ account, chain: arc, transport: http() });
const HOOK = "0xe332196A3bf409899E990846373cf47255e7b044";
const QUOTER = "0xb07209ef64E49ef41E6DEC9B01D0dBa3248d09e8";
const ROUTER = "0x1be3dB20c64C02CD97DdD15c3A65C4F461A43eC8";
const TOKEN = "0x…"; // the coin
const POOL_KEY = "struct PoolKey { address currency0; address currency1; uint24 fee; int24 tickSpacing; address hooks; }";
const quoterAbi = parseAbi([
POOL_KEY,
"struct QuoteExactSingleParams { PoolKey poolKey; bool zeroForOne; uint128 exactAmount; bytes hookData; }",
"function quoteExactInputSingle(QuoteExactSingleParams params) returns (uint256 amountOut, uint256 gasEstimate)",
]);
const routerAbi = parseAbi(["function execute(bytes commands, bytes[] inputs, uint256 deadline) payable"]);
const SWAP_PARAMS = parseAbiParameters([
"ExactInputSingleParams params",
"struct ExactInputSingleParams { PoolKey poolKey; bool zeroForOne; uint128 amountIn; uint128 amountOutMinimum; uint256 minHopPriceX36; bytes hookData; }",
POOL_KEY,
]);
const poolKey = { currency0: zeroAddress, currency1: TOKEN, fee: 0x800000, tickSpacing: 60, hooks: HOOK };
const amountIn = parseEther("0.01");
// 1. Quote.
const { result: [amountOut] } = await client.simulateContract({
address: QUOTER, abi: quoterAbi, functionName: "quoteExactInputSingle",
args: [{ poolKey, zeroForOne: true, exactAmount: amountIn, hookData: "0x" }],
});
const amountOutMinimum = (amountOut * 99n) / 100n; // 1% slippage
// 2. Swap: SWAP_EXACT_IN_SINGLE, SETTLE_ALL, TAKE_ALL inside one V4_SWAP.
const actions = encodePacked(["uint8", "uint8", "uint8"], [0x06, 0x0c, 0x0f]);
const params = [
encodeAbiParameters(SWAP_PARAMS, [{ poolKey, zeroForOne: true, amountIn, amountOutMinimum,
minHopPriceX36: 0n, hookData: account.address }]),
encodeAbiParameters(parseAbiParameters("address, uint256"), [zeroAddress, amountIn]),
encodeAbiParameters(parseAbiParameters("address, uint256"), [TOKEN, amountOutMinimum]),
];
const input = encodeAbiParameters(parseAbiParameters("bytes, bytes[]"), [actions, params]);
const deadline = BigInt(Math.floor(Date.now() / 1000) + 300);
await wallet.writeContract({
address: ROUTER, abi: routerAbi, functionName: "execute",
args: ["0x10", [input], deadline],
value: amountIn, // native USDC is paid as msg.value
});hookData: say who the trader is
The hook reads the first 20 bytes of hookData as the trader, and falls back to tx.origin when there are none. Behind a smart wallet, a relayer or an aggregator, tx.origin is not the trader, so pass the trader's address. It is what the CurveBuy and CurveSell events, and every feed built on them, attribute the trade to.
Selling, and ERC-20 bases
- A sell is the same call with
zeroForOne = falsefor a native-base coin:SETTLE_ALLnames the coin,TAKE_ALLthe base, and novalueis sent. - An ERC-20 input is pulled through Permit2. The token must
approve(Permit2)once, and Permit2 must allow the router: sign aPermitSingleand put aPERMIT2_PERMITcommand (0x0a) in front ofV4_SWAPin the sameexecute, or callPermit2.approveon chain. - A coin priced in one base can be bought with another by routing through the pool between them:
SWAP_EXACT_IN(0x07) with a path. Its parameters are(currencyIn, path, minHopPriceX36[], amountIn, amountOutMinimum), in that order; put the hop prices anywhere else and the router reverts with no data.
