Skip to Content
wstGBP is live on Ethereum mainnet. All addresses in these docs are verifiable on-chain.
Oracles

Oracles

wstGBP has two on-chain price surfaces. The pip is the protocol’s price source: it is updated off-chain on a regular cadence (currently weekly), and everything that quotes mint and redeem reads it. On top of it sits an ownerless Chainlink-compatible aggregator that publishes the redemption price for lending markets and any other AggregatorV3Interface consumer. Both are denominated in tGBP; a USD price is composed with Chainlink GBP/USD, as described in USD pricing. All addresses are Ethereum mainnet (chainId 1) and verifiable on a block explorer.

ContractAddressRole
pip (proxy)0x6A79dCe61A12aa4b75449e0B03746260765D07dFNAV price source and pause authority
pip implementation0x44BFEB1110bA6091034DBaAb450eF1e7469fF072Logic and event ABI (MaseerPrice behind MaseerProxy)
wstGBP/tGBP feed0xF7493C2739c2b1bF5E6bB0e5b16A265Ed0B400B0Chainlink-compatible aggregator (WsgemAggregatorV3), 8 decimals

The NAV oracle (pip)

The pip’s entire price surface is one function: read() returns (uint256), denominated in gem (tGBP) units per 1e18 wstGBP. tGBP is 18-decimal, so for wstGBP the value is WAD; it is the same number the token serves as navprice() (see the Contract Reference). The feed launched at exactly 1e18 and rises as the NAV is published.

The pip stores exactly one value, the current price, and serves it to every quoting path. Update history is carried by the Poke event: every write, including a pause, emits Poke(price, block.timestamp). Round ids and stamped history live on the Chainlink-compatible feed below.

Pause is expressed in the price itself. pause() publishes 0, and the surfaces above the pip translate that into a revert: the token’s quoting functions revert InvalidPrice() and the feed below reverts OraclePaused().

FunctionReturnsNotes
read()uint256The current price, gem units per 1e18 wstGBP. 0 while paused.
paused()boolTrue when the stored price is 0.
name()stringFeed label.
decimals()uint8Informational only; see the note below.

decimals() on the pip is an informational value set by governance, validated only to be at most 18. It is never used to scale the published price, which is always gem units per 1e18 wstGBP. The subgraph mirrors the value as CurrentPrice.priceDecimals.

For quoting, read navprice(), mintcost(), or burncost() on the token, or use the Chainlink-compatible feed below. A direct pip read returns the same NAV with no spread applied, and paused() reports the pause state in a single call.

Governance and updates

The NAV is published by allowlisted poster addresses calling poke(uint256). The allowlist is managed with kiss(address) (grant) and diss(address) (revoke), and is readable via bud(address) returns (uint256) (1 when allowlisted). Governance wards, managed with rely(address) / deny(address) and readable via wards(address) returns (uint256), can pause() the feed and set parameters with file(bytes32 what, bytes32 data) for "price", "name", and "decimals". These are governance functions; every call is observable on-chain through the events below.

Events (pip)

event Poke(uint256 indexed price, uint256 indexed timestamp); event Kiss(address indexed usr); event Diss(address indexed usr); event File(bytes32 indexed what, bytes32 data);

Like the other proxy modules, events are emitted through the proxy address while the event ABI lives on the implementation. The subgraph indexes them as PriceUpdate, FeedReader, and PriceFileEvent entities (see Data & Analytics).

WsgemAggregatorV3 at 0xF7493C2739c2b1bF5E6bB0e5b16A265Ed0B400B0 publishes the wstGBP/tGBP redemption price through the standard Chainlink AggregatorV3Interface, plus the older V2 aggregator surface. It was deployed at block 25677571 (2026-08-03) in transaction 0x8c3dc1fe8dffdb28ad825a72da369e22da80ed607355c79ceacd1fa7a3f96ac8. decimals() returns 8, version() returns 1, and description() returns "wstGBP/tGBP".

The answer is the redemption price of one wstGBP in tGBP at 8 decimals: the token’s burncost() divided by 1e10, floored. It is not the raw NAV. burncost() is NAV net of the 25 bps redemption spread (burncost = ceil(navprice * 9975 / 10000)), so the answer sits slightly below the headline rate and collateral is never valued above what redemption actually pays. The answer would shift if governance changed the spread. As a worked example, an answer of 100486157 means one wstGBP redeems for 1.00486157 tGBP.

The feed is ownerless. It has no owner, no ward, no setter, and no upgrade path; every parameter is immutable. The only mutable state is the round history, written by the permissionless poke() described below. Changing anything would mean deploying a new feed at a new address.

Internally the feed makes two separate reads on every quote: the pip first, as the pause authority, then burncost() on the token, which resolves through the market gate behind its own upgradeable proxy.

Feed surface

FunctionReturnsNotes
latestRoundData()(uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)answer is live; the timestamps come from the last stamped round. Reverts while paused.
getRoundData(uint80 roundId)(uint80, int256, uint256, uint256, uint80)Pure history, never live. Reverts InvalidRound() for round 0 or beyond the latest.
latestAnswer()int256Live answer; equals latestRoundData().answer.
currentAnswer()int256Alias for latestAnswer().
latestTimestamp()uint256Timestamp of the latest stamped round; the live answer can move before the next poke() stamps it. Does not read the feed, so it stays readable through a pause.
latestRound()uint256Highest stamped round id.
getAnswer(uint256 roundId)int256Historical answer by round.
getTimestamp(uint256 roundId)uint256Historical timestamp by round.
paused()boolTrue while the pip reads 0. Never reverts.
pokeable()boolWhether poke() would stamp a new round right now.
poke()uint80 roundIdPermissionless; stamps the current answer into history. Reverts NoChange() if the 8-decimal answer is unchanged.
WSGEM() / PIP() / GEM()addressThe wired token, pip, and underlying.
SRC_DECIMALS()uint8Decimals of the source quote (18 for wstGBP).

Round semantics

Rounds are dense from 1; round 0 never exists and history is unbounded. Rounds stamp on change: a new round is written when the 8-decimal answer moves, not on a fixed interval. startedAt always equals updatedAt, and answeredInRound always equals roundId.

latestRoundData().answer is read live, while getRoundData(latestRound()).answer is the stored historical record. Between a rate move and the next poke(), the two can disagree. Anyone can call poke() to re-sync them, and pokeable() says whether doing so would change anything.

Staleness

updatedAt can legitimately be a week or more old while the feed is healthy. The NAV publishes on a regular cadence (currently weekly) and rounds only stamp on a change, so a stale updatedAt does not imply a stale answer: the answer is read live on every call. Do not enforce a heartbeat tighter than about two weeks. Frameworks that hard-code a 1-24 hour heartbeat preset will trip during normal operation.

For monitoring, useful alarms are: pokeable() staying true for more than a day (a poke is due but nobody has called it), paused() returning true (the pip is paused), and latestRoundData() reverting while paused() returns false (the feed reverting InvalidPrice() or AnswerOverflow() rather than OraclePaused()).

Reverts, not zeros

The feed never serves an answer of 0. Where a price is unavailable, it reverts:

ErrorThrown when
OraclePaused()The pip is paused (read() is 0). Treat as feed down.
InvalidPrice()The pip is live but the quote is zero or rescales to nothing.
AnswerOverflow()The 8-decimal quote exceeds 2**191 - 1.
InvalidRound()getRoundData, getAnswer, or getTimestamp for round 0 or beyond the latest.
NoChange()poke() while the 8-decimal answer has not moved.

During a pause, latestRoundData(), latestAnswer(), and currentAnswer() revert, while getRoundData(), getAnswer(), getTimestamp(), latestTimestamp(), latestRound(), and paused() stay readable. This InvalidPrice() is the feed’s own error; the token throws an error with the same name (see the Contract Reference) under its own conditions.

Events (feed)

event AnswerUpdated(int256 indexed current, uint256 indexed roundId, uint256 updatedAt); event NewRound(uint256 indexed roundId, address indexed startedBy, uint256 startedAt);

The signatures, including indexed parameters, are identical to Chainlink’s AnswerUpdated and NewRound, so Chainlink ABIs decode them without modification.

Reading the feed

readFeed.ts
import { createPublicClient, http } from 'viem' import { mainnet } from 'viem/chains' const feedAbi = [ { type: 'function', name: 'latestRoundData', stateMutability: 'view', inputs: [], outputs: [ { name: 'roundId', type: 'uint80' }, { name: 'answer', type: 'int256' }, { name: 'startedAt', type: 'uint256' }, { name: 'updatedAt', type: 'uint256' }, { name: 'answeredInRound', type: 'uint80' }, ] }, { type: 'function', name: 'decimals', stateMutability: 'view', inputs: [], outputs: [{ type: 'uint8' }] }, ] as const const client = createPublicClient({ chain: mainnet, transport: http() }) const [roundId, answer, startedAt, updatedAt] = await client.readContract({ address: '0xF7493C2739c2b1bF5E6bB0e5b16A265Ed0B400B0', abi: feedAbi, functionName: 'latestRoundData', }) // The feed reverts rather than serving 0, so this branch is unreachable against the wired feed. if (answer <= 0n) throw new Error('invalid answer') // Size the staleness bound to the NAV cadence (weekly), not to hours. const age = BigInt(Math.floor(Date.now() / 1000)) - updatedAt if (age > 14n * 24n * 60n * 60n) throw new Error('stale feed')

Verify against chain

The invariant to check: the feed’s answer equals the token’s burncost() divided by 1e10, floored.

AGG=0xF7493C2739c2b1bF5E6bB0e5b16A265Ed0B400B0 WSGEM=0x57C3571f10767E49C9d7b60feb6c67804783B7aE PIP=0x6A79dCe61A12aa4b75449e0B03746260765D07dF cast call $AGG "latestRoundData()(uint80,int256,uint256,uint256,uint80)" --rpc-url $ETH_RPC_URL cast call $WSGEM "burncost()(uint256)" --rpc-url $ETH_RPC_URL cast call $PIP "read()(uint256)" --rpc-url $ETH_RPC_URL

USD pricing

There is no standalone wstGBP/USD aggregator. Both surfaces above are denominated in tGBP, and there is no on-chain tGBP/USD feed. A USD price is composed by multiplying the wstGBP/tGBP feed by Chainlink’s GBP/USD feed, on the assumption that one tGBP is worth one GBP:

wstGBP/USD = (wstGBP/tGBP feed answer) * (Chainlink GBP/USD answer) / 1e8
LegAddressDecimalsUpdate rule
wstGBP/tGBP (WsgemAggregatorV3)0xF7493C2739c2b1bF5E6bB0e5b16A265Ed0B400B08Live answer; rounds stamp on change, NAV publishes weekly
Chainlink GBP/USD0x5c0Ab2d9b5a7ed9f470386e82BB36A3613cDd4b5824 hour heartbeat, 0.15% deviation trigger

The two legs have different staleness profiles, so check each leg on its own terms: a heartbeat of about two weeks for the wstGBP/tGBP leg (see Staleness) and Chainlink’s 24 hour heartbeat, plus a margin, for GBP/USD. A single bound applied to the composed price will either trip on the weekly leg or miss a stale FX leg.

The composition prices tGBP at GBP parity. A tGBP depeg from GBP is not reflected in the result.

As a worked example, at mainnet block 26056542 (2026-09-25) the wstGBP/tGBP answer was 101021989 and GBP/USD was 132485000, so one wstGBP was worth about 1.01022 * 1.32485 = 1.33839 USD.

The same composition, with both legs, their staleness flags, and the venue oracles below, is served live by the REST API at /api/tokens/wstgbp/prices and by the get_prices tool on the MCP server.

Deployed USD-leg oracles

Three deployed oracles already perform this composition on-chain, each shaped for one lending venue and each quoting in that venue’s dollar stablecoin rather than in USD. All three read the wstGBP leg at the redemption price (burncost()), directly or through the feed above, and GBP/USD from the Chainlink feed above. None has an owner or setter.

OracleAddressQuote tokenReadOutput
MorphoChainlinkOracleV20xafF848eCA1C9f3A517dA868d4aF5C305a422f0b1USDCprice() returns (uint256)USDC per wstGBP, scaled by 1e36 and adjusted for token decimals (6 vs 18)
WsgemFxLlamalendOracle0xdf4A3577066e5ee89249629AA4f27538D77A2a37crvUSDprice() returns (uint256)crvUSD per wstGBP, WAD
WsgemFraxlendDualOracle0xA15A2aF6CaA24d0057b5EEFAcc2046E5161Da407frxUSDgetPrices() returns (bool isBadData, uint256 priceLow, uint256 priceHigh)wstGBP per frxUSD, 1e18 scale (inverted)

The Morpho adapter divides by Chainlink USDC/USD (0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6) and was deployed through Morpho’s MorphoChainlinkOracleV2Factory (0x3A7bB36Ee3f3eE32A60e9f2b33c1e5f2E83ad766). It prices the wstGBP/USDC Morpho Blue market 0xd31bb0f9fa7440852e9053be853ae1f82cc6c36ea0ab71f3e2e665e6bc2787ec. Its BASE_FEED_1(), BASE_FEED_2(), and QUOTE_FEED_1() getters return the three feeds it reads.

The Llamalend oracle divides by Curve’s crvUSD aggregator (0x18672b1b0c623a30089A280Ed9256379fb0E4E62), an EMA over crvUSD stable pools. Its price() is rate-limited: upward moves of the NAV leg are capped by the oracle’s MAX_UPSIDE_SPEED, so a weekly NAV step reaches it over several hours, while GBP/USD moves and downward moves pass through at once. spotPrice() returns the composition with no rate limit applied and quotePrice() returns burncost().

price() never returns zero, so read its status alongside it. While the wstGBP feed is paused or the GBP/USD or crvUSD leg is unreadable, stale, or out of range, price() holds its last good value and frozen() returns true; fxFrozen() is true when the FX legs are the cause. If the redemption quote itself is zero, price() reports 1 wei and quoteIsZero() returns true. spotPrice() returns 0 in every one of these states, and quotePrice() returns 0 while the feed is paused or the quote is zero, so a zero from either is a failure sentinel rather than a price.

The FraxLend oracle divides by Chainlink frxUSD/USD (0x9B4a96210bc8D9D55b1908B465D8B0de68B7fF83). It follows FraxLend’s collateral-per-asset convention, so its prices fall as wstGBP gains value. priceHigh is quoted at burncost() and priceLow at mintcost(), so the gap between them is the wrapper’s 25 bps spread. When a Chainlink leg is older than 86,700 seconds it returns the last answer with isBadData = true rather than reverting.

To get USD rather than a stablecoin, multiply a venue price by that stablecoin’s USD price, or compose the two feeds in the table at the top of this section directly. At block 26056542 the Morpho adapter read 1338558024467854630612882 (1.33856 USDC per wstGBP) and Chainlink USDC/USD read 99987434, and 1.33856 * 0.99987 recovers the 1.33839 USD computed above.

Composing the USD price

wstgbpUsd.ts
import { createPublicClient, http } from 'viem' import { mainnet } from 'viem/chains' const roundAbi = [ { type: 'function', name: 'latestRoundData', stateMutability: 'view', inputs: [], outputs: [ { name: 'roundId', type: 'uint80' }, { name: 'answer', type: 'int256' }, { name: 'startedAt', type: 'uint256' }, { name: 'updatedAt', type: 'uint256' }, { name: 'answeredInRound', type: 'uint80' }, ] }, ] as const const WSTGBP_TGBP = '0xF7493C2739c2b1bF5E6bB0e5b16A265Ed0B400B0' const GBP_USD = '0x5c0Ab2d9b5a7ed9f470386e82BB36A3613cDd4b5' const client = createPublicClient({ chain: mainnet, transport: http() }) const read = (address: `0x${string}`) => client.readContract({ address, abi: roundAbi, functionName: 'latestRoundData' }) const [[, navAnswer, , navUpdatedAt], [, fxAnswer, , fxUpdatedAt]] = await Promise.all([ read(WSTGBP_TGBP), read(GBP_USD), ]) // Check each leg against its own cadence, not one shared bound. const now = BigInt(Math.floor(Date.now() / 1000)) if (now - navUpdatedAt > 14n * 24n * 60n * 60n) throw new Error('stale wstGBP/tGBP') if (now - fxUpdatedAt > 25n * 60n * 60n) throw new Error('stale GBP/USD') if (fxAnswer <= 0n) throw new Error('invalid GBP/USD') // Both legs are 8 decimals, so the product is USD per wstGBP at 8 decimals. const wstgbpUsd = (navAnswer * fxAnswer) / 10n ** 8n
cast call 0xF7493C2739c2b1bF5E6bB0e5b16A265Ed0B400B0 "latestRoundData()(uint80,int256,uint256,uint256,uint80)" --rpc-url $ETH_RPC_URL cast call 0x5c0Ab2d9b5a7ed9f470386e82BB36A3613cDd4b5 "latestRoundData()(uint80,int256,uint256,uint256,uint80)" --rpc-url $ETH_RPC_URL cast call 0xafF848eCA1C9f3A517dA868d4aF5C305a422f0b1 "price()(uint256)" --rpc-url $ETH_RPC_URL cast call 0xdf4A3577066e5ee89249629AA4f27538D77A2a37 "price()(uint256)" --rpc-url $ETH_RPC_URL cast call 0xA15A2aF6CaA24d0057b5EEFAcc2046E5161Da407 "getPrices()(bool,uint256,uint256)" --rpc-url $ETH_RPC_URL

Source

The aggregator is WsgemAggregatorV3 in Arb-Capital/wsgem-oracles  (MIT). The pip implementation is MaseerPrice in maseer-finance/maseer-one . Both are verified on Etherscan at the addresses above.

Of the USD-leg oracles, WsgemFxLlamalendOracle is in Arb-Capital/wsgem-llamalend  (MIT) and WsgemFraxlendDualOracle is in Arb-Capital/wsgem-fraxlend  (MIT). The Morpho adapter is an instance of Morpho’s own MorphoChainlinkOracleV2, created by its factory.

Last updated on