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' export const HOOK_HELPER = '0x4F93a2E29B0AA75875Ab922d780B6dc59b415B6A' // 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 export const hookHelperAbi = [ { type: 'function', name: 'wrapAll', stateMutability: 'nonpayable', inputs: [{ name: 'owner', type: 'address' }, { name: 'minAmountOut', type: 'uint256' }], outputs: [{ name: 'amountOut', type: 'uint256' }] }, { type: 'function', name: 'unwrap', stateMutability: 'nonpayable', inputs: [ { name: 'owner', type: 'address' }, { name: 'amountIn', type: 'uint256' }, { name: 'minAmountOut', type: 'uint256' }, ], outputs: [{ name: 'amountOut', type: 'uint256' }] }, { type: 'function', name: 'unwrapAll', stateMutability: 'nonpayable', inputs: [{ name: 'owner', type: 'address' }, { name: 'minAmountOut', type: 'uint256' }], outputs: [{ name: 'amountOut', 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.

Build CoW Swap order hooks

WsgemHookHelper is the wrap/unwrap target for CoW Swap hooks , user pre/post-interactions carried in the order’s appData as { target, callData, gasLimit }. Approve the helper for exactly the amount the order should convert: the allowance is the owner’s cap. See the venue overview for the full trust model.

import { encodeFunctionData } from 'viem' import { hookHelperAbi } from './swapAbis' import { HOOK_HELPER } from './swapAddresses' // POST-hook: the order buys tGBP; after settlement, wrap the proceeds into wstGBP. // wrapAll sweeps min(balance, allowance): the exact amount is unknown at signing // (surplus), so the owner's exact-amount approval is the cap. const postHook = { target: HOOK_HELPER, callData: encodeFunctionData({ abi: hookHelperAbi, functionName: 'wrapAll', args: [owner, minWstgbpOut], // quote from wstGBP.mintcost(), minus your tolerance }), gasLimit: '300000', } // PRE-hook: the order sells tGBP; before settlement, redeem wstGBP to fund the sell. const preHook = { target: HOOK_HELPER, callData: encodeFunctionData({ abi: hookHelperAbi, functionName: 'unwrap', args: [owner, wstgbpAmountIn, minTgbpOut], // quote from wstGBP.burncost() }), gasLimit: '300000', }

Attach the hook under metadata.hooks.pre / metadata.hooks.post in the order’s appData document . The approval itself cannot be a hook: the owner grants it on-chain (or via Permit) before the order executes.

Pre-hook unwraps require wstGBP.cooldown() == 0 and sufficient tGBP in the wrapper; check previewSwap (sell direction) before signing. Hook execution is a weak guarantee; the skipped-hook outcomes are covered in the venue overview.

Monitor the venue in one multicall

Batch the swap-gating reads so your UI can explain why a swap is blocked rather than failing without a reason (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

What each read means is covered in Uniswap v4 & Aggregators. For direct mint/redeem, the cheapest route for your own funds at the identical price, see Mint & Redeem.

Last updated on