Uniswap v4 & Aggregators
wstGBP trades on-chain through a tGBP/wstGBP Uniswap v4 pool whose hook
(WsgemBackstopHook) routes every swap through the wrapper’s atomic mint/redeem:
buys execute at wstGBP.mintcost(), sells at wstGBP.burncost(). Depth inside the
wrapper’s own ~25bps bid/ask band (the spread is the wrapper’s redeem fee) is bounded by
mint capacity and the wrapper’s tGBP balance rather than by pool liquidity, and both
prices ratchet up as NAV accrues.
The design has two consequences:
- There is no LP.
beforeAddLiquidityreverts, so every swap is a wrappermintorredeem, never AMM liquidity. - There is no capital and no owner. The hook is ownerless, holds no funds, and adds no fee, admin, or pause of its own.
Swaps are subject to the same on-chain governance surface as direct mint/redeem: the oracle price, market open/close, capacity, cooldown, and ban-list screening described in the Contract Reference. Quotes are point-in-time; always pass real slippage bounds.
Deployed contracts on Ethereum mainnet (chainId 1)
All five contracts are ownerless and hold no capital.
| Contract | Purpose | Address |
|---|---|---|
WsgemBackstopHook | The v4 hook | 0xfE36B48c9c0240991E4CEf006a2445F2ff524888 |
WsgemSwapRouter | Settle-first v4 swap router | 0x21734507fDca48A3b4e8C496280b63a37D3bD0C8 |
WsgemQuoter | On-chain quotes & executability preview | 0x9B409f87aeaADBE912632b1E4de855B6aFCc71Ee |
WsgemDirectAdapter | Aggregator / solver adapter (no pool) | 0xBE402d34f31133B1Dc00277f24F8ce2d975CBe23 |
WsgemHookHelper | CoW Swap order-hook wrap/unwrap target | 0x4F93a2E29B0AA75875Ab922d780B6dc59b415B6A |
Uniswap v4 PoolManager | Canonical v4 singleton | 0x000000000004444c5dc75cB358380D2e3dE08A90 |
Solidity source for the backstop hook and its periphery (the router, quoter, adapter, and hook helper), along with their tests and deployment scripts, is public at Arb-Capital/wstgbp-univ4-hook .
Pool id & canonical PoolKey
Uniswap v4 pools have no address. The pool lives inside the PoolManager singleton,
keyed by poolId = keccak256(abi.encode(PoolKey)):
| Field | Value |
|---|---|
currency0 | tGBP (0x27f6c8289550fCE67f6B50BeD1F519966aFE5287) |
currency1 | wstGBP (0x57C3571f10767E49C9d7b60feb6c67804783B7aE) |
fee | 0 |
tickSpacing | 1 |
hooks | 0xfE36B48c9c0240991E4CEf006a2445F2ff524888 |
| poolId | 0xdb21c31f461611ebeeab8af1280c77a82bb81725e1bf9d6093fbbc207a375ce5 |
Pin the canonical PoolKey. The router is intentionally generic over PoolKey, and
the hook validates only the two currencies, not the fee, tick spacing, or hook
address. Integrators, bots, and frontends must hardcode or validate the exact key
above and never route through a user- or route-supplied key. (The quoter needs no key;
it is bound to the wrapper at construction.)
Swap direction
tGBP < wstGBP numerically, so currency0 = tGBP, currency1 = wstGBP:
zeroForOne == truebuys wstGBP: pay tGBP, executed as a wrappermintatmintcost().zeroForOne == falsesells wstGBP: receive tGBP, executed as a wrapperredeematburncost().
Both tokens are 18 decimals; all prices are WAD (1e18) tGBP-per-wstGBP.
Swapping on v4 with WsgemSwapRouter
v4 swaps against this hook must be settle-first (pay the input before the swap
executes). Route through WsgemSwapRouter or any settle-first solver; a plain
swap-then-settle v4 router will revert.
function swapExactInput(
PoolKey calldata key,
bool zeroForOne,
uint256 amountIn,
uint256 minAmountOut,
address recipient, // address(0) => msg.sender
uint256 deadline
) external returns (uint256 amountOut);
function swapExactOutput(
PoolKey calldata key,
bool zeroForOne,
uint256 amountOut,
uint256 maxAmountIn, // surplus is refunded
address recipient,
uint256 deadline
) external returns (uint256 amountIn);Both have Permit2 variants (swapExactInputPermit2 / swapExactOutputPermit2) that fund
the swap from a Permit2 SignatureTransfer instead of a router approval; the permit’s
token must be the input currency and its deadline is the swap deadline.
The router enforces minAmountOut (exact-input), maxAmountIn (exact-output), and full
delivery of the exact output: a swap reverts rather than silently delivering less than
agreed. Quotes are point-in-time and the oracle ratchets between quote and execution, so
never send minAmountOut = 0.
Quoting with WsgemQuoter
Pre-flight swaps with the quoter rather than simulating reverts:
function quoteExactInput(bool zeroForOne, uint256 amountIn) external view returns (uint256 amountOut);
function quoteExactOutput(bool zeroForOne, uint256 amountOut) external view returns (uint256 amountIn);
// amountSpecified: negative = exact-input, positive = exact-output (PoolManager convention)
function previewSwap(bool zeroForOne, int256 amountSpecified)
external view
returns (uint256 amountIn, uint256 amountOut, bool executable, string memory reason);previewSwap reports the live blockers instead of reverting: market closed, dust
threshold, capacity exceeded, wrapper underfunded, redeem cooldown active, or oracle
paused. Quoter output matches execution exactly.
Off-chain, quote directly from wstGBP.mintcost() / burncost() with the same WAD math
as direct mint/redeem (see Mint & Redeem).
WsgemDirectAdapter for aggregators and solvers
WsgemDirectAdapter is a standalone, ownerless approve-then-swap contract that calls
wstGBP.mint/redeem directly: no pool, no v4 callback, ordinary
swap-then-settle semantics. DEX aggregators (Odos, LI.FI, Paraswap) and CoW Protocol
solvers can call it like any swap contract; no settle-first router is needed. Prices and
guards are identical to the hook’s.
// tokenIn == tGBP buys wstGBP (mint); tokenIn == wstGBP sells (redeem)
function swapExactInput(
address tokenIn,
uint256 amountIn,
uint256 minAmountOut,
address recipient, // address(0) => msg.sender
uint256 deadline
) external returns (uint256 amountOut);
function swapExactOutput(
address tokenIn,
uint256 amountOut,
uint256 maxAmountIn, // only the computed exact input is pulled
address recipient,
uint256 deadline
) external returns (uint256 amountIn);
function quoteExactInput(address tokenIn, uint256 amountIn) external view returns (uint256 amountOut);
function quoteExactOutput(address tokenIn, uint256 amountOut) external view returns (uint256 amountIn);Permit2 variants (swapExactInputPermit2 / swapExactOutputPermit2) are available here
too. The adapter is a pure price-taker with no price bounds of its own, so pass real
slippage bounds.
CoW Hooks are user
pre/post-interactions, not a liquidity source. Giving CoW solvers access to this
venue means
route integration
of the adapter; attaching a wrap/unwrap action to a user’s order is what
WsgemHookHelper (below) is for.
CoW Swap order hooks with WsgemHookHelper
CoW Swap hooks are
{target, callData, gasLimit} entries in an order’s appData, executed by the public
HooksTrampoline, which holds no funds and is callable by anyone. The adapter can’t be
a hook target (it pulls from msg.sender), so WsgemHookHelper fills that gap: an
ownerless wrap/unwrap target whose proceeds are hard-wired back to the user, at the same
mintcost()/burncost() oracle prices as every other venue.
// POST-hook, after an order that buys tGBP: wrap the proceeds into wstGBP.
// Sweeps min(balance, allowance) of the owner's tGBP; all wstGBP goes to owner.
function wrapAll(address owner, uint256 minAmountOut) external returns (uint256 amountOut);
// PRE-hook, before an order that sells tGBP: redeem the owner's wstGBP back to tGBP.
function unwrap(address owner, uint256 amountIn, uint256 minAmountOut) external returns (uint256 amountOut);
function unwrapAll(address owner, uint256 minAmountOut) external returns (uint256 amountOut);The security model is owner-bound: anyone may call every function, but funds only
ever move from the owner back to the owner, capped by the owner’s ERC-20 allowance to
the helper. The worst an arbitrary caller can do is trigger a conversion of the approved
amount at the same mintcost()/burncost() oracle price as every other venue, delivered
to the owner. That is bounded griefing (the wrapper’s ~25bps bid/ask spread on a forced
round-trip), never extraction.
Integration notes:
- Grant exact-amount approvals to the helper, per order.
wrapAllsweepsmin(balance, allowance)because post-hook proceeds vary with order surplus. minAmountOutprotects the signed hook against oracle movement between order signing and execution; set it from a quote, as with any swap.- Hook execution is a weak guarantee (solver social consensus): an order can settle
even if its hook was skipped. A skipped
wrapAllleaves the owner holding tGBP plus a revocable approval; a skippedunwrapleaves the order unable to settle for lack of the sell token. Either way, nothing is lost. - Unwraps face the same sell-side gates as every venue: they revert if
wstGBP.cooldown() != 0(RedeemCooldownActive) or the wrapper lacks tGBP (WrapperUnderfunded). There are no partial fills and no deferred payouts. - Each conversion emits
Wrap(owner, caller, amountIn, amountOut)orUnwrap(owner, caller, amountIn, amountOut), wherecalleris the executor, never the payer.
Hook payload snippets live in Swap Recipes.
What integrators should monitor
All of these are public on-chain reads on wstGBP (0x57C3...B7aE); they gate the pool,
the router, the adapter, and the hook helper alike:
| Read | Why it matters |
|---|---|
mintcost() / burncost() | The live execution prices (ratchet up as NAV accrues). |
mintable() / burnable() | Market open/close. Closed mint reverts buys; closed burn reverts sells. |
cooldown() | Must be 0 for sells; non-zero makes sells revert (RedeemCooldownActive) rather than queue a deferred payout. Buys are unaffected. |
capacity() vs totalSupply() | Remaining buy headroom. A buy past capacity reverts (ExceedsCap). |
tGBP.balanceOf(wstGBP) | Sell-side funding depth. Sells past it revert (WrapperUnderfunded), never partially fill. |
Ban-list screening applies to swaps as it does to direct mint/redeem: the swap recipient
must not be banned on tGBP (see Contract Reference). Buys settle
wstGBP through the PoolManager to the recipient, and every leg is screened.
Choosing a venue
| You are | Use |
|---|---|
| Routing v4-native flow | WsgemSwapRouter against the canonical PoolKey |
| A DEX aggregator or CoW solver | WsgemDirectAdapter (approve + swap) |
| Attaching wrap/unwrap hooks to a CoW Swap order | WsgemHookHelper (exact approval + hook in appData) |
| Quoting | WsgemQuoter, the adapter’s quote views, or off-chain mintcost()/burncost() math |
| Minting/redeeming your own funds | Direct mint/redeem (cheapest gas for the identical price) |
Copy-paste viem snippets for all of the above (quoting, router swaps, adapter swaps, and venue monitoring) live in Swap Recipes.