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.
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
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 constQuote 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 redemptionsWhat 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.