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

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.

Last updated on