Skip to main content
Author: Research Date: 2026-06-22 Revised: 2026-09-16 (web research) - primary provider recommendation corrected. Revised again 2026-09-16 (on-chain verification) - the web-research revision’s Reflector and DIA conclusions were themselves wrong in places; superseded by directly calling the deployed contracts. See “2026-09-16 - on-chain verification pass” below, which is the current source of truth. Both superseded revisions are kept in git history, not in this file.

2026-09-16 - on-chain verification pass (current source of truth)

Everything below this point was obtained by running contracts/scripts/verify-oracles.sh against Stellar testnet - not by reading provider docs or blog posts. Docs go stale and get things wrong (see below); a direct contract call doesn’t. Two corrections to the same-day web-research pass:
  1. Reflector does have a plain BTC/USD feed - the earlier “watch, don’t wire yet” verdict was wrong. The mistake: Reflector runs multiple separate oracle contracts per network, not one. Its “Stellar assets” instance (testnet CAVLP5DH2GJPZMVO7IJY4CVOD5MWEFTJFVPD2YY2FQXOQHRGHK4D6HLP) only lists Stellar Soroban token contracts (SolvBTC’s SAC among them) - that’s the instance the earlier web-research pass happened to look at, and it’s the wrong one for this purpose. Its separate “external CEXs & DEXs” instance (testnet CCYOZJCOPG34LLQQ7N24YXBM7LL62R7ONMZ3G6WZAAYPB5OYKOMJRN63) lists real market tickers - BTC, ETH, USDT, XRP, SOL, and 11 others - as plain Other(Symbol) assets, not SAC addresses. Confirmed live 2026-09-16: lastprice({"Other":"BTC"}) returned a price of ~$76,185 (7618536708010298807 at 14 decimals) with a timestamp matching the actual call time to within seconds - this is a real, currently-updating feed, not stale or mocked data. resolution() returned 300 (5 minutes), matching Reflector’s documented “Pulse” free-tier cadence.
  2. DIA’s documented testnet contract does not exist. Its official docs (diadata.org/docs/guides/chain-specific-guide/stellar) list CAEDPEZDRCEJCF73ASC5JGNKCIJDV2QJQSW6DJ6B74MYALBNKCJ5IFP4 as the testnet deployment. Calling it (stellar contract info interface) returns Contract not found, confirmed on two separate attempts. Stellar’s own oracle-providers docs list the same address independently, so this isn’t a copy-paste error on one page - either the contract was never actually deployed at that address, or it existed and was removed/archived. DIA is parked, not secondary, until Writz gets a working contract address directly from DIA (Discord/Telegram - their own docs say custom/production access goes through those channels anyway).
Corrected recommendation: Pyth primary, Reflector (external-prices instance) secondary. This resolves the “2-source median has no tiebreaker” concern the previous revision flagged - Reflector genuinely works today, so Writz doesn’t need to wait on a listing request or a third source to have a real median. DIA remains a candidate third leg if a working address is obtained later. Stellar mainnet contract addresses for reference (not yet called - Writz is testnet-only right now, and mainnet calls weren’t part of this verification pass):
  • Pyth verifier: CACZ3GBAKUPIAFRILUFO27J5RUH5GJ2VSJ46LP6GJYSKGDRTQ5MS3HCH
  • Reflector, Stellar-assets instance: CALI2BYU2JE6WVRUFYTS6MSBNEHGJ35P4AVCZYF3B6QOE3QKOB2PLE6M
  • Reflector, external CEXs & DEXs instance: CAFJZQWSED6YAWZU3GWRTOCNPPCGBN32L7QV43XX5LZLFTK6JLN34DLN
  • Reflector, fiat exchange rates instance: CBKGPWGKSKZF52CFHMTRR23TBWTPMRDIYZ4O2P5VS65BMHYH4DXMCJZC
Re-run contracts/scripts/verify-oracles.sh before mainnet integration - none of this is guaranteed to still hold by the time Phase 2 oracle work actually starts. Contract addresses can be redeployed, Reflector’s asset list can change, and DIA may or may not have a working address by then.

2026-09-16 revision (web research only) - superseded above, kept for the RedStone/SolvBTC finding

The original version of this document (still in git history) recommended RedStone as primary oracle, on the claim that “Blend is already integrating RedStone for price feeds.” That claim does not hold up:
  • Blend uses Reflector, not RedStone. Reflector is listed as a Blend integration on Stellar’s own oracle-providers documentation; no independent source found confirms Blend adopting RedStone.
  • RedStone does not list Stellar among its 110+ supported chains on its own official docs (docs.redstone.finance). Its Stellar-side announcements (blog posts, March/June/August 2026) describe SEP-40 feeds for RWA assets - USDC, EURC, XLM, PYUSD, tokenized sovereign debt (Etherfuse), USDY (Ondo), Centrifuge tokens - not a plain BTC/USD spot pair.
  • This part still matters even though the Reflector conclusion above was corrected: RedStone’s and Reflector’s Stellar-assets-instance asset lists both surface SolvBTC (and, for Reflector, BTCLN) as their closest thing to “Bitcoin.” Both are wrapped/custodied representations, not the L1 asset. Pricing Writz’s collateral off a wrapped-BTC market price would make the liquidation threshold depend on Solv’s (or a bridge’s) minting integrity - the exact counterparty risk Writz’s pitch (“no wrapped tokens, no custodian”) exists to avoid. Do not wire a SolvBTC or BTCLN feed into get_btc_price_stroops and call it the BTC price - use Reflector’s external-prices instance’s plain BTC asset instead, confirmed above.

Overview

Writz Lend requires a reliable, manipulation-resistant BTC/USD price feed to:
  1. Calculate collateral ratios (how much USDC a user can borrow against their BTC)
  2. Determine when a position is liquidatable (BTC value dropped below liquidation threshold)
  3. Compute fair liquidation prices
Oracle manipulation is the #1 attack vector in DeFi lending protocols. This document evaluates the oracle landscape on Stellar and recommends a strategy.

Available Oracles on Stellar (2026)

Testnet verifier contract (confirmed live 2026-09-16): CAYFT5JE3UQTKT4Q6ZOZK4FXVYVT6RE3MFC7STA4UB6WAEGBT65MRU52 Mainnet verifier contract: CACZ3GBAKUPIAFRILUFO27J5RUH5GJ2VSJ46LP6GJYSKGDRTQ5MS3HCH Architecture is structurally different from Reflector/DIA - this matters for the verification script and for integration effort. Pyth’s Stellar contract is a signature verifier, not a price reader: its interface is verify_update(data: Bytes) -> Bytes, plus admin functions for managing trusted signers. There is no lastprice()-style call that returns a currently-stored value for free. To read a Pyth price, the relayer/keeper must first fetch a signed update from Pyth’s off-chain Hermes service, then submit those bytes to verify_update on-chain (paying the pull fee) as part of the same transaction that uses the price. This means Pyth’s BTC/USD availability could not be confirmed the same way Reflector’s was - verify_update needs a live signed payload as input, not just a read call. Confirming it requires either fetching a real Hermes update for the BTC/USD feed ID and test-submitting it, or checking Pyth’s feed-ID registry (canonical across all chains, so if Hermes carries BTC/USD at all - which it does, it’s one of Pyth’s oldest and most liquid feeds - it’s available on Stellar too, since Stellar’s verifier doesn’t whitelist feed IDs itself). Cost: Per-pull fee, not a subscription. General Pyth pricing across chains runs roughly $0.0001-0.001 per update. No confirmed Stellar-specific fee schedule. Known dependency to track: Pyth’s Hermes service has moved toward requiring an API key for some tiers of off-chain consumption. Whether the free public Hermes endpoint remains usable at Writz’s expected call volume needs to be confirmed directly before committing - an operational risk, not a blocker to starting integration work. Testnet contract (confirmed live 2026-09-16): CCYOZJCOPG34LLQQ7N24YXBM7LL62R7ONMZ3G6WZAAYPB5OYKOMJRN63 Mainnet contract: CAFJZQWSED6YAWZU3GWRTOCNPPCGBN32L7QV43XX5LZLFTK6JLN34DLN Confirmed by direct call, not docs. assets() returns 16 tickers including a plain {"Other":"BTC"}. lastprice({"Other":"BTC"}) returned a live price (~$76,185 at the time of the call, 14 decimals). resolution() is 300 seconds (5-minute cadence - this is Reflector’s free “Pulse” tier, no XRF fee). decimals() is 14 - note this for the Rust integration, it’s not the 7 decimals Stellar native assets use, nor a round number like 8 - get_btc_price_stroops’s scaling math needs to read this from the contract, not assume it. Do not confuse this contract with Reflector’s other Stellar instance (CAVLP5DH2GJPZMVO7IJY4CVOD5MWEFTJFVPD2YY2FQXOQHRGHK4D6HLP testnet / CALI2BYU2JE6WVRUFYTS6MSBNEHGJ35P4AVCZYF3B6QOE3QKOB2PLE6M mainnet) - that one only prices Stellar Soroban tokens against each other (including SolvBTC’s SAC, which is not the BTC feed Writz wants) and has no plain BTC ticker. Audit status: A Code4rena competitive audit of Reflector V3 ran Oct 27-Nov 11, 2025 ($20K USDC pool); final report/findings weren’t confirmed as published in this research pass - check before citing this as “audited” externally.

3. DIA - parked, not currently usable

Documented testnet contract: CAEDPEZDRCEJCF73ASC5JGNKCIJDV2QJQSW6DJ6B74MYALBNKCJ5IFP4 - confirmed NOT live (Contract not found, checked twice via stellar contract info interface). This is the address both DIA’s own docs and Stellar’s official oracle-providers docs list, so it’s not a copy-paste error specific to one source. Action needed before DIA can be a candidate again: get a working contract address directly from DIA (Discord/Telegram per their own docs) and re-run the verification script against it.

4. RedStone - removed as a candidate

Stellar isn’t in RedStone’s own list of 110+ supported chains; its Stellar-side SEP-40 feeds (per its own blog posts) target RWA assets (USDC, EURC, XLM, PYUSD, tokenized debt, Centrifuge tokens), not BTC/USD. Never had a testnet address to verify in the first place. Kept here only because earlier project docs (this file’s pre-2026-09-16 versions, docs/roadmap/phases.md, docs/security/security-model.md, docs/how-it-works/stellar-side.md, docs/architecture/technical-overview.md, docs/developers/contribution-guide.md, docs/security/stride-threat-model.md, docs/research/security-audit-strategy.md, docs/research/blend-usdc-integration.md, and Rust doc comments in contracts/contracts/writz-lend/src/{types,lib}.rs) all named it as primary - those are now corrected to Pyth + Reflector.

Oracle Manipulation Attack Vectors

(Unchanged from the original analysis - still applies regardless of which providers are chosen.)

1. Flash loan oracle manipulation

An attacker takes a large flash loan, moves the price of BTC on a DEX (if the oracle reads from a DEX), liquidates an undercollateralized position at the manipulated price, repays the flash loan. Writz exposure: LOW - Pyth and Reflector’s external-prices instance both use off-chain institutional/exchange price aggregation, not on-chain DEX prices. Off-chain feeds cannot be manipulated by on-chain flash loans.

2. Single oracle failure / manipulation

A single oracle source fails, is manipulated, or goes stale. All lending decisions are made on bad data. Mitigation: Use multiple independent oracles and take the median price. With 2 confirmed-working sources (Pyth + Reflector), a median of 2 doesn’t have a tiebreaker if they disagree - see “Combined-oracle design” below for how the deviation check covers this gap in practice, and treat DIA (or a third source) as worth adding once available, not as blocking launch.

3. Oracle front-running

An attacker observes a large price update in the mempool (or before it’s applied) and liquidates positions milliseconds before the update executes. Writz exposure: MEDIUM - On Stellar, front-running is harder than on Ethereum (no public mempool in the same way), but not impossible. Mitigation: require a price confirmation delay of 1-2 ledgers (~5-10 seconds) before acting on a new price for liquidations.

4. Stale price data

The oracle hasn’t updated in a long time. The contract uses an outdated price that doesn’t reflect current market conditions. Mitigation: Reject any price older than X minutes (e.g., 60 minutes for BTC/USD). If no fresh price is available, the protocol enters a “price paused” state - no new borrows or liquidations until a fresh price is available. Existing positions are safe. Pyth is pull-only (price only as fresh as the last submitted update); Reflector’s external instance updates on a fixed ~5-minute cadence (confirmed via resolution()) - the staleness threshold must account for each provider’s actual cadence, not a single assumed one.

Combined-oracle design

Combining oracles is still the right call - confirmed, not just recommended, now that both sources are known to work:

Implementation path

Two different SDK call shapes are needed, not one shared get_btc_usd_price helper as originally assumed - Pyth and Reflector don’t share an interface:

Liquidation price safety

For liquidations specifically, use a conservative price (slightly lower than current) to avoid liquidating based on a momentary price spike:
This prevents attackers from temporarily spiking the price to trigger unfair liquidations.

Before wiring this in

Updated against the on-chain verification pass above:
  1. Call assets()/lastprice() on Pyth’s, DIA’s, and Reflector’s live testnet contracts - done for Reflector and attempted for DIA (see contracts/scripts/verify-oracles.sh). Pyth couldn’t be fully verified this way since it needs a live Hermes payload, not just a read call - that’s still open.
  2. Fetch a real Hermes BTC/USD update and test-submit it to Pyth’s testnet verifier (CAYFT5JE3UQTKT4Q6ZOZK4FXVYVT6RE3MFC7STA4UB6WAEGBT65MRU52) to close the remaining gap in Pyth verification. Needs the Hermes client/SDK, not just stellar contract invoke.
  3. Get a working DIA contract address directly from DIA (their docs’ listed address doesn’t resolve) if DIA is still wanted as a third median leg.
  4. Confirm Pyth’s Hermes access policy (API key requirement, rate limits) at whatever call volume Writz’s relayer/keeper will actually generate.
  5. Re-run contracts/scripts/verify-oracles.sh against mainnet addresses before any mainnet deployment - everything confirmed above is testnet-only.
  6. Done for writz-lend (2026-09-17): get_btc_price_stroops now calls Reflector’s external-prices instance live, replacing STUB_PRICE_STROOPS_PER_BTC - see contracts/contracts/writz-lend/src/oracle.rs. Reads decimals() live rather than assuming 14, per the note above. commitment-tree is unchanged and still stubbed - see that crate’s oracle.rs and spv-types’s doc comment for why (the ZK circuit’s exact-match price signal has no tolerance window, so a live price would make honest borrows/repays fail intermittently until the circuit itself changes).
  7. Still open: Pyth is not wired anywhere yet. writz-lend is single-source (Reflector only) until it is - see docs/security/security-model.md for what that means for manipulation resistance in the interim.

Key Decisions


Last updated: 2026-09-16 (on-chain verification pass) Sources: direct stellar contract invoke / stellar contract info interface calls against Stellar testnet (2026-09-16, via contracts/scripts/verify-oracles.sh) for the claims in the “on-chain verification pass” section; web research (official docs, blogs, GitHub, Stellar’s oracle-providers documentation) for everything else, explicitly flagged as unverified where it hasn’t been re-checked against a live contract. Re-run the verification script rather than trusting any number in this file that predates its listed “Last updated” date.