Skip to Content
wstGBP is live on Ethereum mainnet · all addresses in these docs are verifiable on-chain
GuidesSwap Recipes (Uniswap v4)

Swap Recipes (Uniswap v4)

Practical snippets for swapping through the tGBP/wstGBP v4 backstop pool with viem . Quotes come from the on-chain WsgemQuoter (or the adapter’s own quote views), so no WAD math is required — but the same preview math works off-chain if you prefer.

swapAddresses.ts
export const TGBP = '0x27f6c8289550fCE67f6B50BeD1F519966aFE5287' export const WSTGBP = '0x57C3571f10767E49C9d7b60feb6c67804783B7aE' export const HOOK = '0xfE36B48c9c0240991E4CEf006a2445F2ff524888' export const SWAP_ROUTER = '0x21734507fDca48A3b4e8C496280b63a37D3bD0C8' export const QUOTER = '0x9B409f87aeaADBE912632b1E4de855B6aFCc71Ee' export const DIRECT_ADAPTER = '0xBE402d34f31133B1Dc00277f24F8ce2d975CBe23' // The canonical PoolKey — hardcode it; never accept a route-supplied key. export const POOL_KEY = { currency0: TGBP, currency1: WSTGBP, fee: 0, tickSpacing: 1, hooks: HOOK, } as const export const POOL_ID = '0xdb21c31f461611ebeeab8af1280c77a82bb81725e1bf9d6093fbbc207a375ce5'

Curated ABIs

swapAbis.ts
const poolKey = { type: 'tuple', components: [ { name: 'currency0', type: 'address' }, { name: 'currency1', type: 'address' }, { name: 'fee', type: 'uint24' }, { name: 'tickSpacing', type: 'int24' }, { name: 'hooks', type: 'address' }, ], } as const export const quoterAbi = [ { type: 'function', name: 'quoteExactInput', stateMutability: 'view', inputs: [{ name: 'zeroForOne', type: 'bool' }, { name: 'amountIn', type: 'uint256' }], outputs: [{ name: 'amountOut', type: 'uint256' }] }, { type: 'function', name: 'quoteExactOutput', stateMutability: 'view', inputs: [{ name: 'zeroForOne', type: 'bool' }, { name: 'amountOut', type: 'uint256' }], outputs: [{ name: 'amountIn', type: 'uint256' }] }, { type: 'function', name: 'previewSwap', stateMutability: 'view', inputs: [{ name: 'zeroForOne', type: 'bool' }, { name: 'amountSpecified', type: 'int256' }], outputs: [ { name: 'amountIn', type: 'uint256' }, { name: 'amountOut', type: 'uint256' }, { name: 'executable', type: 'bool' }, { name: 'reason', type: 'string' }, ] }, ] as const export const swapRouterAbi = [ { type: 'function', name: 'swapExactInput', stateMutability: 'nonpayable', inputs: [ { ...poolKey, name: 'key' }, { name: 'zeroForOne', type: 'bool' }, { name: 'amountIn', type: 'uint256' }, { name: 'minAmountOut', type: 'uint256' }, { name: 'recipient', type: 'address' }, { name: 'deadline', type: 'uint256' }, ], outputs: [{ name: 'amountOut', type: 'uint256' }] }, { type: 'function', name: 'swapExactOutput', stateMutability: 'nonpayable', inputs: [ { ...poolKey, name: 'key' }, { name: 'zeroForOne', type: 'bool' }, { name: 'amountOut', type: 'uint256' }, { name: 'maxAmountIn', type: 'uint256' }, { name: 'recipient', type: 'address' }, { name: 'deadline', type: 'uint256' }, ], outputs: [{ name: 'amountIn', type: 'uint256' }] }, ] as const export const directAdapterAbi = [ { type: 'function', name: 'swapExactInput', stateMutability: 'nonpayable', inputs: [ { name: 'tokenIn', type: 'address' }, { name: 'amountIn', type: 'uint256' }, { name: 'minAmountOut', type: 'uint256' }, { name: 'recipient', type: 'address' }, { name: 'deadline', type: 'uint256' }, ], outputs: [{ name: 'amountOut', type: 'uint256' }] }, { type: 'function', name: 'swapExactOutput', stateMutability: 'nonpayable', inputs: [ { name: 'tokenIn', type: 'address' }, { name: 'amountOut', type: 'uint256' }, { name: 'maxAmountIn', type: 'uint256' }, { name: 'recipient', type: 'address' }, { name: 'deadline', type: 'uint256' }, ], outputs: [{ name: 'amountIn', type: 'uint256' }] }, { type: 'function', name: 'quoteExactInput', stateMutability: 'view', inputs: [{ name: 'tokenIn', type: 'address' }, { name: 'amountIn', type: 'uint256' }], outputs: [{ name: 'amountOut', type: 'uint256' }] }, { type: 'function', name: 'quoteExactOutput', stateMutability: 'view', inputs: [{ name: 'tokenIn', type: 'address' }, { name: 'amountOut', type: 'uint256' }], outputs: [{ name: 'amountIn', type: 'uint256' }] }, ] as const

Quote and check executability

previewSwap returns the quote and whether it would execute right now — market closed, dust threshold, capacity, wrapper underfunded, redeem cooldown, and a paused oracle are reported as a reason string instead of a revert. amountSpecified follows the PoolManager.swap convention: negative = exact-input, positive = exact-output.

import { createPublicClient, http } from 'viem' import { mainnet } from 'viem/chains' import { quoterAbi } from './swapAbis' import { QUOTER } from './swapAddresses' const pub = createPublicClient({ chain: mainnet, transport: http() }) // Buy wstGBP with exactly 1,000 tGBP (zeroForOne = true, negative = exact input) const [amountIn, amountOut, executable, reason] = await pub.readContract({ address: QUOTER, abi: quoterAbi, functionName: 'previewSwap', args: [true, -1_000n * 10n ** 18n], }) if (!executable) throw new Error(`swap blocked: ${reason}`)

Buy wstGBP through the v4 router

Approve the router for the input token, then swap with the quoted output as the slippage floor. Quoter and execution match exactly within a block; if your transaction may land later, subtract your own tolerance from minAmountOut — prices only ratchet up, so a buy’s output can only shrink between quote and execution. Never send minAmountOut = 0.

import { erc20Abi } from 'viem' import { swapRouterAbi, quoterAbi } from './swapAbis' import { POOL_KEY, SWAP_ROUTER, QUOTER, TGBP } from './swapAddresses' const amountIn = 1_000n * 10n ** 18n // 1,000 tGBP // 1. approve tGBP to the router await wallet.writeContract({ address: TGBP, abi: erc20Abi, functionName: 'approve', args: [SWAP_ROUTER, amountIn], }) // 2. quote, then swap with the quote as the floor const [, amountOut, executable, reason] = await pub.readContract({ address: QUOTER, abi: quoterAbi, functionName: 'previewSwap', args: [true, -amountIn], }) if (!executable) throw new Error(reason) const { request } = await pub.simulateContract({ address: SWAP_ROUTER, abi: swapRouterAbi, functionName: 'swapExactInput', args: [ POOL_KEY, true, // zeroForOne: true = buy wstGBP amountIn, (amountOut * 9_990n) / 10_000n, // minAmountOut: quote - 10bps tolerance '0x0000000000000000000000000000000000000000', // recipient 0 => msg.sender BigInt(Math.floor(Date.now() / 1000) + 300), // deadline: now + 5 min ], account, }) const hash = await wallet.writeContract(request)

Selling is the same flow mirrored: approve wstGBP to the router, pass zeroForOne = false, and quote with previewSwap(false, -amountIn). Sells require wstGBP.cooldown() == 0 and enough tGBP in the wrapper — both reported by previewSwap.

Swap through the direct adapter (aggregators & solvers)

The adapter needs no PoolKey and no settle-first plumbing — approve it for the input token and call it like any swap contract. tokenIn picks the direction: tGBP buys, wstGBP sells.

import { erc20Abi } from 'viem' import { directAdapterAbi } from './swapAbis' import { DIRECT_ADAPTER, TGBP } from './swapAddresses' const amountIn = 1_000n * 10n ** 18n await wallet.writeContract({ address: TGBP, abi: erc20Abi, functionName: 'approve', args: [DIRECT_ADAPTER, amountIn], }) const amountOut = await pub.readContract({ address: DIRECT_ADAPTER, abi: directAdapterAbi, functionName: 'quoteExactInput', args: [TGBP, amountIn], }) const { request } = await pub.simulateContract({ address: DIRECT_ADAPTER, abi: directAdapterAbi, functionName: 'swapExactInput', args: [ TGBP, // tokenIn: tGBP = buy wstGBP amountIn, (amountOut * 9_990n) / 10_000n, // minAmountOut: quote - 10bps tolerance '0x0000000000000000000000000000000000000000', BigInt(Math.floor(Date.now() / 1000) + 300), ], account, }) const hash = await wallet.writeContract(request)

Exact-output works the same way on both venues: call swapExactOutput with maxAmountIn as the ceiling (quote it first via quoteExactOutput, then add your tolerance up). The router refunds surplus input; the adapter pulls only the computed exact input. Both also offer Permit2 variants (swapExactInputPermit2 / swapExactOutputPermit2) that replace the approval with a signed PermitTransferFrom.

Monitor the venue in one multicall

Batch the swap-gating reads so your UI can explain why a swap is blocked rather than just failing (for a size-specific check, previewSwap already returns the reason):

import { erc20Abi } from 'viem' import { wstgbpAbi } from './wstgbpAbi' // from the Contract Reference import { WSTGBP, TGBP } from './swapAddresses' const wst = { address: WSTGBP, abi: wstgbpAbi } as const const [mintcost, burncost, mintable, burnable, cooldown, capacity, totalSupply, depth] = await pub.multicall({ allowFailure: false, contracts: [ { ...wst, functionName: 'mintcost' }, { ...wst, functionName: 'burncost' }, { ...wst, functionName: 'mintable' }, { ...wst, functionName: 'burnable' }, { ...wst, functionName: 'cooldown' }, { ...wst, functionName: 'capacity' }, { ...wst, functionName: 'totalSupply' }, // sell-side depth: tGBP held by the wrapper { address: TGBP, abi: erc20Abi, functionName: 'balanceOf', args: [WSTGBP] }, ], }) const buyHeadroom = capacity - totalSupply // wstGBP still mintable const sellDepth = depth // tGBP available for redemptions

For what each read means (and the full venue overview — pool id, canonical PoolKey, trust notes), see Uniswap v4 & Aggregators. For direct mint/redeem — the cheapest route for your own funds at the identical price — see Mint & Redeem.

Last updated on