Skip to main content

Price Verifier

Zenex prices markets with Pyth Lazer. The PriceVerifier contract turns a signed binary Lazer update into verified, feed-scoped PriceData for the trading contract. Signature checking is delegated to a separate Lazer verification contract. The price verifier parses the payload and enforces feed, exponent, confidence, spread, and staleness bounds on top.

PriceData Structure

A verified update produces PriceData values carrying the two sides of the quote. The aggregate Pyth price is validated (positivity, confidence bound) but not returned. Consumers mark off bid and ask:

pub struct PriceData {
pub feed_id: u32, // Pyth Lazer feed identifier
pub exponent: i32, // decimal exponent (e.g. -8)
pub bid: i128, // best bid (native precision)
pub ask: i128, // best ask (native precision)
pub publish_time: u64, // oracle publish timestamp (seconds)
}

The trading contract uses bid and ask for direction-aware entry and exit (see Pricing). The price scalar for downstream math is price_scalar = 10^(-exponent).

Verification Flow

The verifier runs the following on every call before returning data.

Signature delegation. The verifier holds a lazer contract address and calls verify_update on it, passing the raw update bytes. The Lazer contract checks the update's signature against its trusted-signer set and returns the inner payload bytes. This keeps the signing-key trust and the ECDSA verification in one dedicated contract that the price verifier depends on.

Payload parsing. The verifier parses the returned payload (its own magic number, a microsecond timestamp, a channel byte, a feed count, and per-feed properties: price, best bid, best ask, exponent, confidence, and feed update timestamp). An empty feed set is rejected.

Feed selection. verify_price(update_data, feed_id, exponent) extracts the one requested feed, raising FeedNotFound (790) if the update does not contain it and WrongExponent (791) if the feed's exponent differs from the caller's anchor. It validates only the requested feed, so an unrelated malformed feed in the same payload does not fail a scoped read. verify_prices(update_data) returns every feed in the update, validating and staleness-checking each one (a single malformed feed fails the whole call) and applying no exponent check.

Per-feed validation. For each returned feed the verifier requires the price, best bid, best ask, and feed update timestamp to be present, and enforces:

  • price > 0, bid > 0, ask > 0: a non-positive wire value is malformed.
  • bid <= ask: a crossed market is malformed.
  • confidence present and non-negative, with confidence * 10_000 <= |price| * max_confidence_bps: a spread wider than the configured tolerance is rejected.
  • feed update timestamp present: a feed lacking it is rejected, and publish_time is derived from it (t / 1_000_000).

Any violation raises InvalidPrice (781).

Staleness. The publish time must not be in the future (publish_time <= now) and must be no older than max_staleness seconds, else PriceStale (782). A future publish time indicates a malformed payload (the oracle publishes before transaction inclusion), and rejecting it also keeps the age subtraction from underflowing.

Upstream Signer Management

The Lazer contract the verifier delegates to is Pyth's own Stellar deployment from pyth-lazer-public. Its trusted-signer set is managed by Pyth governance, not by Zenex. Wormhole guardians sign a governance VAA, a Wormhole executor contract on Stellar verifies the VAA's guardian signatures and parses the governance payload, and the executor then calls into the Lazer contract (update_trusted_signer, and upgrade for the contract itself) by cross-contract call. Signer entries carry an expiry, so a key lapses on schedule without an explicit removal.

verify_update checks an update's ECDSA envelope against that signer set and returns the verified inner payload bytes to the caller. The one Zenex-controlled lever in this chain is the verifier's owner-only update_lazer, which repoints the delegation target to a different Lazer contract address.

Access Control

The price verifier implements OZ Ownable. For standard Ownable behavior, refer to OpenZeppelin Stellar Contracts.

FunctionAuth
verify_pricePermissionless
verify_pricesPermissionless
lazer / max_confidence_bps / max_stalenessPermissionless (views)
update_lazerOwner only (#[only_owner])
update_max_confidence_bpsOwner only
update_max_stalenessOwner only

Staleness and Confidence Bounds

max_staleness is a single configurable threshold applied to every price. It is set at deployment and updatable by the owner, but both the constructor and update_max_staleness enforce a hard cap of MAX_STALENESS_SECONDS = 15. A larger value reverts with InvalidStaleness (783). The cap keeps the verifier from vending a price older than the short window the trading layer is willing to act on.

max_confidence_bps bounds the acceptable confidence interval relative to price (for example 100 = 1%). It is likewise owner-updatable, with no code-level cap, and drives the confidence check above.

Error Codes

CodeNameMeaning
780InvalidDataVerified payload contains no feeds
781InvalidPriceMissing, non-positive, crossed, or over-confidence feed data
782PriceStaleUpdate is future-dated or older than max_staleness
783InvalidStalenessConfigured max_staleness exceeds MAX_STALENESS_SECONDS
784TruncatedDataPayload ended before a field could be read
785InvalidPayloadLengthTrailing bytes remain after the last feed is parsed
786InvalidPayloadMagicPayload magic number did not match
787InvalidChannelUnknown channel byte
788InvalidPropertyUnknown feed property id (the parser cannot skip a property of unknown length)
789InvalidMarketSessionMarket session field is invalid
790FeedNotFoundRequested feed_id absent from the update
791WrongExponentFeed exponent differs from the caller's scale anchor