# Architecture Diagrams Source: https://docs.writz.xyz/architecture/diagrams Visual reference for Writz Protocol v1 - from Bitcoin locking to SPV verification and lending on Soroban. ## System Architecture End-to-end view of how Bitcoin, the relayer, the Writz v1 contracts (`bitcoin-spv`, `writz-lend`) and the browser interact. The privacy edition's contracts are drawn as a separate group: `writz-lend` never calls them. ![Writz System Architecture](https://mintcdn.com/writz-protocol/H_sPi9-nz-OQpN2-/diagrams/output/01-system-architecture.png?fit=max\&auto=format\&n=H_sPi9-nz-OQpN2-\&q=85\&s=90a746955763cafd483252d008de5afe) **Key layers:** * **Bitcoin** - P2WSH locks collateral; two spending paths (cooperative co-sign or CLTV timelock) * **Relayer** - submits Bitcoin headers to `bitcoin-spv`, serves SPV proofs, and co-signs the release PSBT after a full repayment * **Soroban contracts** - `bitcoin-spv` verifies Bitcoin transactions; `writz-lend` holds plaintext positions, the USDC pool and liquidation, priced by the Reflector oracle * **Browser** - the Bitcoin wallet signs the key-control proof and the release; the Stellar wallet signs Soroban transactions *** ## Deposit Flow Step-by-step path from connecting a Bitcoin wallet to having an active position on Stellar. ![Deposit Flow](https://mintcdn.com/writz-protocol/H_sPi9-nz-OQpN2-/diagrams/output/02-deposit-flow.png?fit=max\&auto=format\&n=H_sPi9-nz-OQpN2-\&q=85\&s=9070ae6e89a95ec763061e305168152b) 1. Connect Bitcoin wallet (Xverse) and Stellar wallet (Freighter) 2. Derive unique P2WSH address from protocol key + user key + timelock height 3. Send BTC to that address on-chain 4. Relayer serves the SPV proof (block hash + Merkle path + raw transaction) 5. The Bitcoin wallet signs the key-control message (BIP-137) for this contract, depositor and txid 6. `writz-lend.deposit()` rebuilds the P2WSH, calls `bitcoin-spv.verify_transaction`, checks the timelock window and the signature, consumes the txid and creates the position *** ## Borrow & Repay Flow How `writz-lend` handles a borrow or a repayment. ![Borrow and Repay Flow](https://mintcdn.com/writz-protocol/H_sPi9-nz-OQpN2-/diagrams/output/03-borrow-repay-flow.png?fit=max\&auto=format\&n=H_sPi9-nz-OQpN2-\&q=85\&s=474ec33dd921ecbc57cfbf699c5d0987) * **Borrow:** only the depositor; interest accrues first; the post-borrow ratio at the Reflector price must stay at or above 150%, within pool liquidity and the 50,000 USDC total cap * **Repay:** partial or full; a full repayment closes the position and emits `repay_full` *** ## BTC Release Flow (Path A) Cooperative co-signed release after full repayment. ![BTC Release Flow](https://mintcdn.com/writz-protocol/nYtWJyg5bCBf8TvX/diagrams/output/04-btc-release-flow.png?fit=max\&auto=format\&n=nYtWJyg5bCBf8TvX\&q=85\&s=0fa74e0c0e3b8d4d7382e253a90e9c4a) * The relayer's repay watcher reacts to `repay_full`, builds the PSBT and co-signs it with the protocol key * The half-signed PSBT is published on `writz-lend` (`publish_release_psbt`) and can be read with `get_release_psbt` even if Writz is down * Final witness: `[user_sig, protocol_sig, 0x01, redeemScript]` *** ## Soroban Contract Interactions How the Writz v1 testnet contracts call each other. ![Contract Interactions](https://mintcdn.com/writz-protocol/ob-rXdGsZ5IiQwOx/diagrams/output/07-contract-interactions.png?fit=max\&auto=format\&n=ob-rXdGsZ5IiQwOx\&q=85\&s=544fdc9c126f2a544d00dfe1432140e3) | Contract | Role | | - | - | | `bitcoin-spv` | Verifies Bitcoin transactions against its stored header chain; shared deposit txid registry | | `writz-lend` | Deposits, USDC pool, interest, keeper liquidation, release PSBT storage | | `commitment-tree`, `zk-verifier` | Privacy edition (in progress), testnet only; not part of v1 and never called by `writz-lend` | *** ## Privacy edition (in progress): ZK Circuits > These circuits belong to the separate, opt-in privacy edition, in progress and on testnet only. They are not part of Writz v1 and are not used by `writz-lend`. See [Privacy edition (in progress)](/how-it-works/privacy-edition). The main Groth16 circuits (Circom / BN254), their private inputs, public inputs, and public outputs. ![ZK Circuits](https://mintcdn.com/writz-protocol/5nk-qtD5SjakN9Bx/diagrams/output/05-zk-circuits.png?fit=max\&auto=format\&n=5nk-qtD5SjakN9Bx\&q=85\&s=20f8953bd7bfc25c6fabfff9d32779d4) | Circuit | Purpose | | - | - | | `deposit.circom` | Binds a Poseidon commitment to a Bitcoin txid | | `borrow_repay.circom` | Proves Merkle inclusion + valid collateral ratio after state change | | `liquidation.circom` | Proves position is under-collateralized | *** ## Privacy edition (in progress): Commitment State Machine > Privacy edition only. Writz Lend positions on `writz-lend` have no commitments: they are plaintext, keyed by the Bitcoin txid, with the statuses Active, Closed and Liquidated. Complete lifecycle of a privacy edition position from pending to BTC release (or liquidation). ![Commitment State Machine](https://mintcdn.com/writz-protocol/H_sPi9-nz-OQpN2-/diagrams/output/06-commitment-state-machine.png?fit=max\&auto=format\&n=H_sPi9-nz-OQpN2-\&q=85\&s=9ca0614c08ba6e95c712b3bed90209e8) | State | Description | | - | - | | **Pending** | `deposit()` verified, waiting for `insert_commitment()` | | **Active · debt=0** | Commitment in Merkle tree, no outstanding debt | | **Active · debt>0** | USDC borrowed; nonce rotates on every partial repay | | **Closed** | Full repayment done; old nullifier spent | | **Released** | Path A PSBT finalized; BTC returned on Bitcoin | | **Liquidated** | Ratio below 120%; keeper repaid debt | *** ## Regenerating Diagrams ```bash theme={null} pip3 install graphviz python3 scripts/diagrams/render-all.py ``` Output: `docs/diagrams/output/*.svg` and `*.png` # Technical Architecture Overview Source: https://docs.writz.xyz/architecture/technical-overview **Version:** 0.1 (draft - pending Phase 0 validation) **Last updated:** 2026-06-22 **Status:** Preliminary - subject to change after Phase 0 benchmarks > **Superseded values notice:** This document predates several Phase 0/1 decisions and is kept for historical architecture context only. Where it conflicts with `docs/research/circom-circuit-design.md`, `docs/research/interest-rate-model.md`, `docs/research/oracle-design.md`, or the contract source (`contracts/contracts/writz-lend/src/`), those are the sources of truth, not this file. Known stale values below have been corrected inline; treat any other specific number in this document as unverified until cross-checked against the docs above. *** ## System Architecture Writz Protocol operates across two blockchains: Bitcoin and Stellar. The architecture is designed to be trustless on the verification layer. Positions are plaintext: amounts, collateral and addresses are public on chain. Privacy is not part of the protocol; it is being designed as a separate, opt-in edition, in progress (see `docs/how-it-works/privacy-edition.md`). ``` ┌─────────────────────────────────────────────────────────────────┐ │ BITCOIN NETWORK │ │ │ │ User BTC Wallet (Xverse) │ │ │ │ │ │ Send BTC to P2WSH address │ │ ▼ │ │ P2WSH Locking Script │ │ ┌─────────────────────────────────────────┐ │ │ │ Spending condition A: │ │ │ │ Protocol co-signature + user sig │ ← loan repaid │ │ │ Spending condition B: │ │ │ │ Timelock expiry (safety fallback) │ ← emergency │ │ └─────────────────────────────────────────┘ │ │ │ │ │ │ Bitcoin transaction confirmed (6 blocks) │ └────────┼────────────────────────────────────────────────────────┘ │ │ SPV Proof Package │ (block headers + Merkle proof + raw tx) │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ STELLAR / SOROBAN │ │ │ │ ┌────────────────────────────────────────┐ │ │ │ Bitcoin SPV Contract │ │ │ │ │ │ │ │ verify_transaction( │ │ │ │ headers: Vec, │ │ │ │ merkle_proof: MerkleProof, │ │ │ │ raw_tx: Transaction │ │ │ │ ) → bool │ │ │ │ │ │ │ │ Operations: │ │ │ │ • Validate header PoW (SHA256d) │ │ │ │ • Validate header chain continuity │ │ │ │ • Verify Merkle inclusion proof │ │ │ │ • Extract output address + amount │ │ │ └─────────────────┬──────────────────────┘ │ │ │ Verification result │ │ ▼ │ │ ┌────────────────────────────────────────┐ │ │ │ Writz Lend Contract │ │ │ │ │ │ │ │ deposit(spv_proof, key_sig) → txid │ │ │ │ borrow(txid, amount) → USDC │ │ │ │ repay(txid, usdc_amount) │ │ │ │ liquidate(txid) (keeper) │ │ │ │ publish_release_psbt(txid, psbt) │ │ │ │ │ │ │ │ Plaintext positions keyed by txid │ │ │ └─────────────────┬──────────────────────┘ │ │ │ │ │ ▼ │ │ ┌────────────────────────────────────────┐ │ │ │ USDC Pool (Stellar native) │ │ │ │ │ │ │ │ Lenders supply USDC → earn yield │ │ │ │ Borrowers receive USDC │ │ │ │ Protocol captures spread │ │ │ └────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ ``` *** ## Component Breakdown ### 1. P2WSH Locking Script (Bitcoin) The BTC locking mechanism lives entirely on Bitcoin. It is a Pay-to-Witness-Script-Hash (P2WSH) address whose redeem script encodes two spending conditions using Bitcoin Script. **Script design (simplified):** ``` OP_IF OP_CHECKSIGVERIFY OP_CHECKSIG OP_ELSE OP_CHECKLOCKTIMEVERIFY OP_DROP OP_CHECKSIG OP_ENDIF ``` **Spending path A (normal - loan repaid):** * Both the protocol and the user sign the release transaction * This is triggered when the user repays their USDC loan * The Soroban contract generates the co-signature upon verifying repayment **Spending path B (emergency - timelock expiry):** * After a predefined locktime (e.g., loan maturity + 30 days), the user can spend without the protocol's signature * Protects users if the protocol becomes unavailable **Script type:** Writz stays on P2WSH with an absolute block-height time-lock for mainnet v1. An earlier plan to move to Taproot (P2TR) is withdrawn: the planned 2-of-3 protocol side does not fit a MuSig2 key path, Taproot signing is not broadly supported by the launch wallets, and Soroban has no host functions for a Taproot output check. See "Script Roadmap: P2WSH, not Taproot" in [The Bitcoin Side](/how-it-works/bitcoin-side). *** ### 2. Bitcoin SPV Contract (Soroban) The core of Writz's technical innovation. A Soroban smart contract that verifies Bitcoin transactions cryptographically. **Approach: an on-chain header light client** `bitcoin-spv` keeps a compact chain of Bitcoin block headers on Stellar, anchored at a checkpoint block that is set once and never changed. Relayers submit new headers with `submit_headers`; each one must descend from a stored header, carry valid proof of work at exactly the difficulty Bitcoin's retarget rules require, and accumulate chainwork, and the contract tracks the most-work chain. A transaction is then proven against a header the contract already stored, so a caller cannot present a header chain it mined privately. An earlier stateless design, in which the caller supplied the headers at verification time, was replaced for that reason. See [How SPV Verification Works](/how-it-works/spv-verification) and [SPV vs Wrapped Bitcoin](/security/spv-vs-wrapped). **Core functions:** ```rust theme={null} // Add contiguous block headers to the light client (80-byte raw headers). // Permissionless unless a submitter is configured. Returns the best tip height. pub fn submit_headers(env: Env, headers: Vec>) -> Result // Prove that a transaction is in a stored block on the most-work chain, // buried under at least `min_confirmations` blocks. pub fn verify_transaction( env: Env, block_hash: BytesN<32>, // a header the contract already stored merkle_proof: Vec>, // sibling hashes from tx to block root tx_index: u32, // the transaction index in the block raw_tx: Bytes, // the raw transaction, without witness data min_confirmations: u32, ) -> Result // { txid, block_hash, block_height, confirmations } ``` Output checks are done by the consumer: `writz-lend::deposit` rebuilds the expected Writz P2WSH script from the protocol key, the depositor's key and the timelock, and requires the deposit output to match it. **Cryptographic operations required:** * `SHA256d` (double-SHA256): Bitcoin's primary hash function - used for block hashes, txids, Merkle tree nodes * Merkle proof verification: O(log n) SHA256d operations for n transactions in the block * Block header PoW verification: check that `SHA256d(header) < target` * Header chain continuity: verify each header's `prev_block_hash` matches a stored header * Difficulty retargeting and cumulative chainwork, to follow the most-work chain *** ### 3. Privacy (not part of the protocol) Writz v1 has no privacy layer. Positions, amounts and collateral are public on chain. An opt-in privacy edition with its own contracts (`commitment-tree`, `zk-verifier`, Circom circuits verified with Groth16 on Soroban), its own pool, audit and trusted-setup ceremony is in progress on testnet and planned for after v1 is live on mainnet, with no date. See `docs/how-it-works/privacy-edition.md`. *** ### 4. Writz Lend Contract (Soroban) The lending logic that sits above the SPV layer. **Key parameters (to be finalized in Phase 0):** * Collateralization ratio: 150% minimum (BTC value must be 1.5x the USDC borrowed) * Liquidation threshold: 120% (position liquidated if BTC/USDC ratio drops below this) * Liquidation bonus: 10% of debt, paid in USDC from the protocol reserve (funded by 15% of interest), capped at the reserve balance * Borrow rate: variable, determined by utilization ratio (similar to Blend/Aave) * Supply rate: borrow rate × utilization × (1 - protocol fee percentage) * Protocol fee: 15% of interest spread (finalized; see `PROTOCOL_FEE_BP` in `contracts/contracts/writz-lend/src/rates.rs` and `docs/research/interest-rate-model.md`) **Interest rate model:** ``` utilization = borrowed_usdc / total_usdc_supplied if utilization <= optimal_utilization (75%): borrow_rate = base_rate + (utilization / optimal) × slope1 else: borrow_rate = base_rate + slope1 + ((utilization - optimal) / (1 - optimal)) × slope2 ``` (Corrected - this section originally showed 80% optimal utilization; the finalized value is 75%, see `docs/research/interest-rate-model.md`.) **Liquidation flow:** * A designated keeper monitors positions, which are public on chain * When a position drops below 120% collateralization at the oracle price, the keeper calls `liquidate`; the contract re-checks the ratio itself * If the keeper has been inactive for 24 hours (no liquidation and no heartbeat), any caller may liquidate an undercollateralized position * Liquidator pays USDC and receives a USDC bonus from the protocol reserve; no BTC is released (GHSA-5rxp-7f9g-r66x) *** ### 5. USDC Liquidity Pools (Stellar native) The supply side of Writz Lend, and what Earn means on Writz. USDC lenders supply the pool with `supply_usdc` and earn the interest borrowers pay. This is standard Stellar asset mechanics - no novel engineering required. * Lenders supply USDC → the contract records a scaled balance that grows with the supply index (no receipt token) * Borrowers draw from the pool against their BTC collateral * Interest accrues continuously; `withdraw_supply` returns principal plus interest at any time, up to the USDC not lent out *** ### 6. Open SDK A Rust crate that wraps the SPV verification logic, making it consumable by any other Soroban developer. ```rust theme={null} // Third-party Soroban contract using Writz SPV use writz_spv::StellarSpvClient; let spv = StellarSpvClient::new(env, WRITZ_SPV_CONTRACT_ADDRESS); let result = spv.verify_payment( raw_tx, merkle_proof, block_headers, expected_address, min_confirmations: 6, ); ``` *** ## Data Flow - Full User Journey ### Deposit + Borrow ``` 1. User connects Xverse (BTC) + Freighter (Stellar) in Writz UI 2. Writz generates a unique P2WSH address for this deposit 3. User sends BTC to the P2WSH address on Bitcoin 4. After 6 confirmations: a. Writz header service provides block headers + Merkle proof b. User (or Writz UI) submits SPV proof to Soroban SPV contract c. SPV contract verifies and returns true 5. Writz Lend contract recognizes the deposit a. Rebuilds the Writz P2WSH, checks the user's key-control signature, consumes the txid b. Creates a plaintext position keyed by the Bitcoin txid c. User can now borrow up to 66% of BTC value in USDC 6. User requests USDC loan a. Contract checks the 150% collateral ratio at the oracle price b. USDC transferred to user's Stellar wallet ``` ### Repay + Withdraw ``` 1. User repays USDC loan + accrued interest 2. Writz Lend contract confirms repayment 3. Relayer co-signs the P2WSH spending path A PSBT and publishes it on writz-lend 4. User signs and broadcasts the BTC release transaction on Bitcoin 5. BTC arrives in user's original wallet ``` ### Liquidation ``` 1. Keeper detects an undercollateralized position from public chain state 2. Keeper calls liquidate(txid); the contract recomputes the health ratio from the oracle price 3. Liquidator pays USDC to cover the loan 4. Contract pays the liquidator a USDC bonus from the reserve (capped at its balance) and records the liquidation. The protocol never moves BTC. ``` *** ## Security Considerations ### Bitcoin-side risks | Risk | Mitigation | | - | - | | Bitcoin reorg (chain reorganization) | Require 6 confirmations. Reorganizations that deep are very rare: the deepest on mainnet are the 2010 value-overflow fix and the 2013 BIP-50 fork | | P2WSH script bug | Formal verification of locking script; multiple independent reviews | | Protocol key compromise | Planned: a 2-of-3 protocol side in the P2WSH script, three keys held separately, so no single key can co-sign a release; the signing policy will be published. The user's time-lock exit never depends on these keys | ### Soroban-side risks | Risk | Mitigation | | - | - | | SPV contract bug | Small header light client with no upgrade path; independent audit planned before mainnet; exposure caps bound what a verification failure could cost lenders. See [SPV vs Wrapped Bitcoin](/security/spv-vs-wrapped) | | Oracle/price feed manipulation | Reflector SEP-40 price feed today, rejected when older than one hour; a second source (Pyth, pull model) with a deviation guard is planned - see `docs/research/oracle-design.md` | | Interest rate model edge cases | Stress-test model against historical volatility; cap max borrow rate | ### Economic risks | Risk | Mitigation | | - | - | | BTC price crash → mass liquidations | Conservative 150% collateral ratio; liquidation discount creates keeper incentive | | USDC supply drought | No minimum borrow guarantee; utilization-based rates auto-adjust | | Keeper failure (no liquidations) | Stale-keeper fallback: after 24 hours of keeper inactivity, anyone with USDC can liquidate an eligible position | *** ## Technology Stack | Component | Technology | | - | - | | Stellar smart contracts | Soroban (Rust) | | Bitcoin SPV library | Writz's own header light client (`bitcoin-spv`, Rust, no external SPV library) | | Bitcoin scripting | P2WSH with an absolute CLTV time-lock; a 2-of-3 miniscript form planned; Taproot withdrawn | | Frontend | React/Next.js | | Bitcoin wallet | Xverse (via PSBT standard) | | Stellar wallet | Stellar Wallets Kit (Freighter, Lobstr, others) | | Price oracles | Reflector (SEP-40) today; Pyth planned as a second source with a deviation guard - see `docs/research/oracle-design.md` | | Header service | Custom Node.js service connecting to Bitcoin full node | *** *Last updated: 2026-06-22 - This document is preliminary. Architecture will be updated after Phase 0 benchmarks confirm feasibility of SPV in Soroban.* # Contract Reference Source: https://docs.writz.xyz/developers/contract-reference The public interface of the four Soroban contracts in this repository, as defined on `main`. Writz v1 is `bitcoin-spv` and `writz-lend`; `zk-verifier` and `commitment-tree` belong to the separate [privacy edition (in progress)](/how-it-works/privacy-edition), are deployed on testnet only, and are never called by `writz-lend`. Every function below is copied from the contract source, with the first paragraph of its doc comment; `env: Env` is omitted. The source is the authority: `contracts/contracts//src/lib.rs`, and the generated TypeScript bindings in `packages/` and `frontend/app/src/lib/contracts/`. Every contract is configured by its `__constructor`, which runs inside the deploy transaction. There is no separate `initialize` call. To deploy and wire up all four, use `scripts/deploy/deploy_stack.mjs` (see #177 for the order and arguments). **Testnet addresses:** | Contract | Address | | - | - | | `bitcoin-spv` | `CBX23YKCCJMAMBMSCNP6YVKEXUIYK2CTKSR4TQ3RVBPAFYZFVCW7W4EO` | | `zk-verifier` | `CB5XAP6MF2GVVFATFTVCKVEXUODWWRQP4B3QFDMRTM55REP7N2YMXEUD` | | `commitment-tree` | `CAQBLCAPGXXCZTJDAJLY7E4PTJYGR6CY4YWH4JSJKFXYIHW3GANP6RFM` | | `writz-lend` | `CABF7VK7F25ZGDLV6EWSAU3NYQYKDQNDSJIISDMDZAUASMSXUADSHZGG` | `bitcoin-spv` and `writz-lend` are the instances from the 2026-10-25 redeploy; `zk-verifier` and `commitment-tree` are from the coordinated redeploy (#177, 2026-10-11). All four run the interfaces documented here. See `contracts/deployments/testnet.md`. Testnet `bitcoin-spv` and `writz-lend` were redeployed on 2026-10-25 because testnet rent rose sharply: testnet's live state is above its target size, and the previous contracts gave every entry they created the long mainnet storage windows, so a deposit was charged about 428 XLM in fees. The new contracts take those windows as deployment parameters and run testnet-length ones (a deposit was charged about 89 XLM, measured 2026-10-25). Positions on the previous contracts were closed and their BTC released; their IDs are recorded as superseded in `contracts/deployments/testnet.md`. The windows are the `header_ttl_ledgers` and `consumed_ttl_ledgers` arguments of `bitcoin-spv`'s `__constructor` and the `entry_ttl_ledgers` argument of `writz-lend`'s (both below); what each entry keeps and when it is extended is in [Storage and TTL management](/how-it-works/stellar-side#storage-and-ttl-management). *** ## bitcoin-spv ### `__constructor` Runs exactly once, atomically, as part of deployment (`__constructor`): no separate initialize transaction exists for anyone to front-run (GHSA-422m-f73x-fh58). ```rust theme={null} pub fn __constructor(admin: Address, pow_limit_bits: u32, header_ttl_ledgers: u32, consumed_ttl_ledgers: u32) ``` ### `set_checkpoint` Sets the trust-root checkpoint. Admin-gated and callable exactly once: after this the chain only grows through `submit_headers`, so the admin cannot later rewrite which chain is trusted. ```rust theme={null} pub fn set_checkpoint(caller: Address, height: u32, block_hash: BytesN<32>, bits: u32, time: u32, period_start_time: u32) -> Result<(), SPVError> ``` ### `set_consumer` Registers (or removes) a lending contract allowed to consume deposit txids. Admin-only. Both lending contracts share one txid registry so the same Bitcoin output cannot back loans from both pools (GHSA-2975-ggwh-pxw5). ```rust theme={null} pub fn set_consumer(caller: Address, consumer: Address, allowed: bool) -> Result<(), SPVError> ``` ### `consume_deposit` Marks a deposit txid as consumed by a lending contract. Fails if the caller is not a registered consumer or the txid was already consumed. Called by each lending contract's `deposit` after its own checks. ```rust theme={null} pub fn consume_deposit(consumer: Address, txid: BytesN<32>) -> Result<(), SPVError> ``` ### `is_deposit_consumed` Returns whether a deposit txid has already been consumed. ```rust theme={null} pub fn is_deposit_consumed(txid: BytesN<32>) -> bool ``` ### `set_submitter` Restricts `submit_headers` to `submitter`, or reopens it to everyone with `None`. Admin-gated. ```rust theme={null} pub fn set_submitter(caller: Address, submitter: Option
) -> Result<(), SPVError> ``` ### `set_ttls` Sets how long stored headers (with their height index) and consumed deposit markers are kept live, in ledgers. Admin-gated, same bounds as the constructor (17,280 to 3,110,400, else `InvalidEntryTtl`). Applies to entries written or touched after the change only; emits `ttls_set`. ```rust theme={null} pub fn set_ttls(caller: Address, header_ttl_ledgers: u32, consumed_ttl_ledgers: u32) -> Result<(), SPVError> ``` ### `set_admin` Rotates the admin address. Admin-gated. ```rust theme={null} pub fn set_admin(caller: Address, new_admin: Address) -> Result<(), SPVError> ``` ### `get_checkpoint` Returns the checkpoint, or `None` if never set. ```rust theme={null} pub fn get_checkpoint() -> Option ``` ### `get_best_tip` Returns the tip of the most-work chain, or `None` before the checkpoint is set. ```rust theme={null} pub fn get_best_tip() -> Option ``` ### `get_header` Returns the stored header for `block_hash`, if any. ```rust theme={null} pub fn get_header(block_hash: BytesN<32>) -> Option ``` ### `get_canonical_hash` Returns the most-work chain's block hash at `height`, if known. ```rust theme={null} pub fn get_canonical_hash(height: u32) -> Option> ``` ### `refresh_ttl` Extends the TTL of the Config, Checkpoint and best-tip entries. Permissionless - anyone can call this to keep an inactive deployment from expiring. ```rust theme={null} pub fn refresh_ttl() ``` ### `submit_headers` Adds a run of contiguous block headers to the light client. ```rust theme={null} pub fn submit_headers(headers: Vec>) -> Result ``` ### `verify_transaction` Verify that a Bitcoin transaction is included in a confirmed block of the most-work chain this contract tracks. ```rust theme={null} pub fn verify_transaction(block_hash: BytesN<32>, merkle_proof: Vec>, tx_index: u32, raw_tx: Bytes, min_confirmations: u32) -> Result ``` *** ## zk-verifier > Privacy edition (in progress), testnet only. Not part of Writz v1. ### `__constructor` Runs exactly once, atomically, as part of deployment (`__constructor`). The admin is the only account that can call `set_verification_key`. ```rust theme={null} pub fn __constructor(admin: Address) ``` ### `set_verification_key` Store or replace the verification key for a circuit. ```rust theme={null} pub fn set_verification_key(caller: Address, circuit: CircuitId, vk: VerificationKey) -> Result<(), ZkVerifierError> ``` ### `get_verification_key` Returns the stored verification key for a circuit, or None. ```rust theme={null} pub fn get_verification_key(circuit: CircuitId) -> Option ``` ### `refresh_ttl` Extends the TTL of the admin entry and every set verification key. Permissionless - call on a schedule so an infrequently-used deployment doesn't silently expire and start failing proof verification with `VerificationKeyNotSet`. ```rust theme={null} pub fn refresh_ttl() ``` ### `verify_deposit` Verify a Groth16 proof for the deposit circuit. ```rust theme={null} pub fn verify_deposit(proof: Proof, public_signals: Vec>) -> Result ``` ### `verify_borrow_repay` Verify a Groth16 proof for the borrow/repay circuit. ```rust theme={null} pub fn verify_borrow_repay(proof: Proof, public_signals: Vec>) -> Result ``` ### `verify_zero_debt` Verify a Groth16 proof for the zero-debt release circuit. ```rust theme={null} pub fn verify_zero_debt(proof: Proof, public_signals: Vec>) -> Result ``` ### `verify_liquidation` Verify a Groth16 proof for the liquidation circuit. ```rust theme={null} pub fn verify_liquidation(proof: Proof, public_signals: Vec>) -> Result ``` ### `verify_insert` Verify a Groth16 proof for the Merkle insertion circuit. ```rust theme={null} pub fn verify_insert(proof: Proof, public_signals: Vec>) -> Result ``` ### Events Topic fields are in **bold**. | Event | Topic | Fields | | - | - | - | | `VkRotatedEvent` | `vk_rotated` | **new\_vk\_hash**, circuit, old\_vk\_hash | *** ## commitment-tree > Privacy edition (in progress), testnet only. Not part of Writz v1; `writz-lend` never calls it. ### `__constructor` Runs exactly once, atomically, as part of deployment (`__constructor`) - see GHSA-422m-f73x-fh58. ```rust theme={null} pub fn __constructor(admin: Address, spv_contract: Address, zk_verifier: Address, usdc_token: Address, oracle: Address, min_confirmations: u32, protocol_pubkey: BytesN<33>, keeper_pub_x: BytesN<32>, keeper_pub_y: BytesN<32>) ``` ### `deposit` Register a BTC deposit with a ZK commitment. ```rust theme={null} pub fn deposit(depositor: Address, block_hash: BytesN<32>, merkle_proof_btc: Vec>, tx_index: u32, raw_tx: Bytes, user_pubkey: BytesN<33>, timelock_height: u32, key_sig: BytesN<65>, zk_proof: Proof, public_signals: Vec>, enc_note: Bytes) -> Result, CommitmentTreeError> ``` ### `insert_commitment` Insert a pending commitment into the Merkle tree and advance the root. ```rust theme={null} pub fn insert_commitment(caller: Address, zk_proof: Proof, public_signals: Vec>) -> Result<(), CommitmentTreeError> ``` ### `borrow` Borrow USDC against a BTC position using a ZK proof. ```rust theme={null} pub fn borrow(borrower: Address, zk_proof: Proof, public_signals: Vec>, enc_note: Bytes) -> Result<(), CommitmentTreeError> ``` ### `mark_released` Records that a fully repaid position's BTC has been released, by spending the zero-debt leaf's nullifier (GHSA-w4rp-v54x-2cv3, GHSA-hcjf-8vjc-2hfv). After this, `borrow` on that leaf fails its nullifier check. ```rust theme={null} pub fn mark_released(zk_proof: Proof, public_signals: Vec>) -> Result<(), CommitmentTreeError> ``` ### `repay` Repay USDC debt on a ZK position. ```rust theme={null} pub fn repay(repayer: Address, zk_proof: Proof, public_signals: Vec>, enc_note: Bytes) -> Result<(), CommitmentTreeError> ``` ### `liquidate` Liquidate an undercollateralized position using a ZK proof. ```rust theme={null} pub fn liquidate(keeper: Address, zk_proof: Proof, public_signals: Vec>) -> Result<(), CommitmentTreeError> ``` ### `supply_usdc` Lender supplies USDC to the pool to earn yield from borrower interest. ```rust theme={null} pub fn supply_usdc(supplier: Address, amount: i128) -> Result<(), CommitmentTreeError> ``` ### `withdraw_supply` Lender withdraws USDC from the pool. ```rust theme={null} pub fn withdraw_supply(supplier: Address, amount: i128) -> Result<(), CommitmentTreeError> ``` ### `set_oracle` Updates the oracle contract address used for BTC/USD pricing. Admin only. ```rust theme={null} pub fn set_oracle(caller: Address, new_oracle: Address) -> Result<(), CommitmentTreeError> ``` ### `set_spv_contract` Updates the bitcoin-spv contract address used for deposit verification. Admin only. Changing this mid-flight affects only deposits submitted after the change - an in-flight deposit's SPV proof was already verified against the previous contract by the time this would run. ```rust theme={null} pub fn set_spv_contract(caller: Address, new_spv_contract: Address) -> Result<(), CommitmentTreeError> ``` ### `set_zk_verifier` Updates the zk-verifier contract address used for ZK proof verification. Admin only. Changing this mid-flight affects only proofs submitted after the change. ```rust theme={null} pub fn set_zk_verifier(caller: Address, new_zk_verifier: Address) -> Result<(), CommitmentTreeError> ``` ### `set_max_total_borrowed` Sets the ceiling on total outstanding borrows, in USDC stroops. Admin only. Lowering it below the current total blocks new borrows without touching existing positions. ```rust theme={null} pub fn set_max_total_borrowed(caller: Address, max: i128) -> Result<(), CommitmentTreeError> ``` ### `set_keeper_pubkey` Rotates the keeper's BabyJubJub key (#212). Admin only. Positions whose last note went to the old key stay liquidatable by whoever holds it; their next borrow or repay re-encrypts to the new one. ```rust theme={null} pub fn set_keeper_pubkey(caller: Address, keeper_pub_x: BytesN<32>, keeper_pub_y: BytesN<32>) -> Result<(), CommitmentTreeError> ``` ### `set_borrow_timelock_margin` Sets how many blocks a borrowing position must have left before its timelock (#212). Admin only; 144 (\~1 day) up to the deposit window's own maximum. ```rust theme={null} pub fn set_borrow_timelock_margin(caller: Address, blocks: u32) -> Result<(), CommitmentTreeError> ``` ### `set_max_tip_age` Sets how old `bitcoin-spv`'s tip may be for a borrow to rely on it (#212). Admin only; 1 to 48 hours. ```rust theme={null} pub fn set_max_tip_age(caller: Address, secs: u64) -> Result<(), CommitmentTreeError> ``` ### `set_paused` Pauses or unpauses new deposits/borrows/USDC supply. Admin only. ```rust theme={null} pub fn set_paused(caller: Address, paused: bool) -> Result<(), CommitmentTreeError> ``` ### `refresh_nullifier_ttl` Extend the TTL of a spent-nullifier entry to another 180-day window. ```rust theme={null} pub fn refresh_nullifier_ttl(nullifier: BytesN<32>) -> bool ``` ### `refresh_commitment_ttl` Extend the TTL of the Bitcoin txid → commitment dedup record. ```rust theme={null} pub fn refresh_commitment_ttl(txid: BytesN<32>) -> bool ``` ### `refresh_merkle_root_ttl` Extend the TTL of the on-chain Merkle root to another 180-day window. ```rust theme={null} pub fn refresh_merkle_root_ttl() ``` ### `refresh_pool_ttl` Extend the TTL of the USDC pool accounting entry. ```rust theme={null} pub fn refresh_pool_ttl() ``` ### `refresh_supply_balance_ttl` Extend the TTL of a lender's supply balance entry. ```rust theme={null} pub fn refresh_supply_balance_ttl(lender: Address) -> bool ``` ### `refresh_instance_ttl` Extend the instance storage TTL to another 90-day window. ```rust theme={null} pub fn refresh_instance_ttl() ``` ### `get_keeper_pubkey` The keeper's BabyJubJub public key (x, y) every borrow and repay proof must encrypt its keeper note to (#212). ```rust theme={null} pub fn get_keeper_pubkey() -> Result<(BytesN<32>, BytesN<32>), CommitmentTreeError> ``` ### `get_borrow_timelock_margin` Blocks a borrowing position must have left before its timelock (#212). ```rust theme={null} pub fn get_borrow_timelock_margin() -> Result ``` ### `get_merkle_root` Returns the current Poseidon Merkle root of the position commitment tree. ```rust theme={null} pub fn get_merkle_root() -> BytesN<32> ``` ### `is_nullifier_spent` Returns true if the nullifier has already been spent. ```rust theme={null} pub fn is_nullifier_spent(nullifier: BytesN<32>) -> bool ``` ### `get_commitment` Returns the commitment for a Bitcoin txid, or None if not deposited. ```rust theme={null} pub fn get_commitment(txid: BytesN<32>) -> Option> ``` ### `get_next_leaf_index` Returns the index of the next empty Merkle leaf - the `leaf_index` the next insertion proof must target. ```rust theme={null} pub fn get_next_leaf_index() -> u32 ``` ### `is_commitment_pending` Returns true if a commitment is pending Merkle tree insertion. ```rust theme={null} pub fn is_commitment_pending(commitment: BytesN<32>) -> bool ``` ### `get_pool_state` Returns `(total_supplied, total_borrowed)` in USDC stroops. ```rust theme={null} pub fn get_pool_state() -> (i128, i128) ``` ### `get_supply_balance` Returns the USDC supply balance (in stroops) for a lender. ```rust theme={null} pub fn get_supply_balance(lender: Address) -> i128 ``` ### Events Topic fields are in **bold**. | Event | Topic | Fields | | - | - | - | | `DepositEvent` | `deposit` | **commitment**, depositor, txid, nullifier, user\_pubkey, timelock\_height, enc\_note | | `InsertLeafEvent` | `insert_leaf` | **new\_root**, commitment, leaf\_index | | `BorrowEvent` | `borrow` | **new\_root**, borrower, usdc\_amount, old\_nullifier, new\_commitment, enc\_note, keeper\_note | | `RepayEvent` | `repay` | **new\_root**, repayer, usdc\_amount, old\_nullifier, new\_commitment, enc\_note, keeper\_note | | `LiquidateEvent` | `liquidate` | **nullifier**, keeper, usdc\_debt | | `SupplyEvent` | `supply` | **supplier**, usdc\_amount, total\_supplied | | `WithdrawEvent` | `withdraw` | **supplier**, usdc\_amount, total\_supplied | | `PausedSetEvent` | `paused_set` | **admin**, paused | *** ## writz-lend ### `__constructor` Runs exactly once, atomically, as part of deployment (`__constructor`): no separate initialize transaction exists for anyone to front-run (GHSA-422m-f73x-fh58). ```rust theme={null} pub fn __constructor(admin: Address, spv_contract: Address, usdc_token: Address, oracle: Address, keeper: Address, relayer: Address, protocol_pubkey: BytesN<33>, min_confirmations: u32, min_deposit_satoshis: u64, entry_ttl_ledgers: u32) ``` ### `deposit` Register a BTC deposit by submitting an SPV proof. `user_pubkey` must differ from the protocol key, else `UserKeyIsProtocolKey`. ```rust theme={null} pub fn deposit(depositor: Address, block_hash: BytesN<32>, merkle_proof: Vec>, tx_index: u32, raw_tx: Bytes, p2wsh_script_pubkey: Bytes, timelock_height: u32, user_pubkey: BytesN<33>, key_sig: BytesN<65>) -> Result, WritzLendError> ``` ### `supply_usdc` Lender supplies USDC to the pool, making it available for borrowing. Lenders earn `supply_rate_bp()` APR on their supplied amount. ```rust theme={null} pub fn supply_usdc(supplier: Address, amount: i128) -> Result<(), WritzLendError> ``` ### `withdraw_supply` Lender withdraws previously supplied USDC from the pool. ```rust theme={null} pub fn withdraw_supply(supplier: Address, amount: i128) -> Result<(), WritzLendError> ``` ### `borrow` Borrow USDC against an existing BTC deposit. Every borrow, including a top-up, requires the position's timelock height H to be at least `borrow_timelock_margin_blocks` (4,320 by default) above the `bitcoin-spv` tip, read from a tip header no older than `max_tip_age_secs` (6 hours by default); else `TimelockTooSoon`, or `StaleBitcoinTip` when the tip is missing or too old. ```rust theme={null} pub fn borrow(borrower: Address, txid: BytesN<32>, usdc_amount: i128) -> Result<(), WritzLendError> ``` ### `repay` Repay some or all of the USDC debt on a position. ```rust theme={null} pub fn repay(repayer: Address, txid: BytesN<32>, usdc_amount: i128) -> Result<(), WritzLendError> ``` ### `liquidate` Liquidate an undercollateralized position. Accepted only while H is at least `LIQUIDATION_CUTOFF_BLOCKS` (288) above the `bitcoin-spv` tip, read from a fresh tip as for `borrow`; else `LiquidationWindowClosed`, or `StaleBitcoinTip`. ```rust theme={null} pub fn liquidate(keeper: Address, txid: BytesN<32>) -> Result<(), WritzLendError> ``` ### `set_max_total_borrowed` Update the keeper address. Admin only. ```rust theme={null} pub fn set_max_total_borrowed(caller: Address, max: i128) -> Result<(), WritzLendError> ``` ### `set_keeper` ```rust theme={null} pub fn set_keeper(caller: Address, new_keeper: Address) -> Result<(), WritzLendError> ``` ### `keeper_heartbeat` Explicit liveness signal from the designated keeper. ```rust theme={null} pub fn keeper_heartbeat(keeper: Address) -> Result<(), WritzLendError> ``` ### `set_keeper_stale_window` Updates how many seconds of keeper inactivity before liquidation opens to any caller. Admin only. ```rust theme={null} pub fn set_keeper_stale_window(caller: Address, secs: u64) -> Result<(), WritzLendError> ``` ### `set_borrow_timelock_margin` Sets how many Bitcoin blocks a borrow must leave between the `bitcoin-spv` tip and the position's timelock height. Admin only; 4,320 to 105,000, else `InvalidTimelockParams`: the margin can be raised, never lowered below the default. ```rust theme={null} pub fn set_borrow_timelock_margin(caller: Address, blocks: u32) -> Result<(), WritzLendError> ``` ### `set_max_tip_age` Sets how old the `bitcoin-spv` tip header may be, in seconds, for `borrow` and `liquidate` to rely on it. Admin only; 1 to 48 hours, else `InvalidTimelockParams`. ```rust theme={null} pub fn set_max_tip_age(caller: Address, secs: u64) -> Result<(), WritzLendError> ``` ### `set_relayer` Updates the relayer address authorized to call `publish_release_psbt`. Admin only. ```rust theme={null} pub fn set_relayer(caller: Address, new_relayer: Address) -> Result<(), WritzLendError> ``` ### `set_oracle` Updates the oracle contract address used for BTC/USD pricing. Admin only. ```rust theme={null} pub fn set_oracle(caller: Address, new_oracle: Address) -> Result<(), WritzLendError> ``` ### `set_entry_ttl` Sets how long per-user entries (positions, supply balances, release PSBTs) are kept live, in ledgers. Admin only, same bounds as the constructor's `entry_ttl_ledgers` (17,280 to 3,110,400, else `InvalidEntryTtl`). Applies to entries written or touched after the change only; emits `entry_ttl_set`. ```rust theme={null} pub fn set_entry_ttl(caller: Address, entry_ttl_ledgers: u32) -> Result<(), WritzLendError> ``` ### `set_spv_contract` Updates the bitcoin-spv contract address used for deposit verification. Admin only. Changing this mid-flight affects only deposits submitted after the change - an in-flight deposit's SPV proof was already verified against the previous contract by the time this would run. ```rust theme={null} pub fn set_spv_contract(caller: Address, new_spv_contract: Address) -> Result<(), WritzLendError> ``` ### `set_paused` Pauses or unpauses new deposits/borrows/USDC supply. Admin only. ```rust theme={null} pub fn set_paused(caller: Address, paused: bool) -> Result<(), WritzLendError> ``` ### `publish_release_psbt` Publishes a co-signed Path A release PSBT for a repaid position. ```rust theme={null} pub fn publish_release_psbt(relayer: Address, txid: BytesN<32>, psbt: Bytes) -> Result<(), WritzLendError> ``` ### `get_release_psbt` Returns the relayer-published release PSBT for a position, or `None` if the relayer hasn't published one yet (or the position was never fully repaid). ```rust theme={null} pub fn get_release_psbt(txid: BytesN<32>) -> Option ``` ### `get_position` Returns the position for the given Bitcoin txid, or `None`. ```rust theme={null} pub fn get_position(txid: BytesN<32>) -> Option ``` ### `get_health_ratio_bp` Returns the health ratio (in basis points) for a position. ```rust theme={null} pub fn get_health_ratio_bp(txid: BytesN<32>) -> Result ``` ### `get_borrow_timelock_margin` Blocks a borrow must leave between the `bitcoin-spv` tip and the position's timelock height. ```rust theme={null} pub fn get_borrow_timelock_margin() -> Result ``` ### `get_max_tip_age` Oldest the `bitcoin-spv` tip header may be, in seconds, for `borrow` and `liquidate` to rely on it. ```rust theme={null} pub fn get_max_tip_age() -> Result ``` ### `get_borrow_rate_bp` Current annual borrow rate in basis points (e.g. 800 = 8%). ```rust theme={null} pub fn get_borrow_rate_bp() -> i128 ``` ### `get_supply_rate_bp` Current annual supply rate in basis points. ```rust theme={null} pub fn get_supply_rate_bp() -> i128 ``` ### `get_protocol_state` Returns a snapshot of the global protocol state. ```rust theme={null} pub fn get_protocol_state() -> ProtocolState ``` ### `get_supply_balance` Returns the USDC supply balance (in stroops) for a lender. ```rust theme={null} pub fn get_supply_balance(lender: Address) -> i128 ``` ### `refresh_position_ttl` Extend the TTL of a position entry to the full `entry_ttl_ledgers` window (180 days on mainnet). ```rust theme={null} pub fn refresh_position_ttl(txid: BytesN<32>) -> bool ``` ### `refresh_supply_balance_ttl` Extend the TTL of a lender's supply balance entry to the full `entry_ttl_ledgers` window. ```rust theme={null} pub fn refresh_supply_balance_ttl(lender: Address) -> bool ``` ### `refresh_protocol_ttl` Extend the TTL of the global protocol accounting entry. ```rust theme={null} pub fn refresh_protocol_ttl() ``` ### `refresh_release_psbt_ttl` Extend the TTL of a published release PSBT to another window. ```rust theme={null} pub fn refresh_release_psbt_ttl(txid: BytesN<32>) -> bool ``` ### Events Topic fields are in **bold**. | Event | Topic | Fields | | - | - | - | | `DepositEvent` | `deposit` | **txid**, btc\_satoshis, timelock\_height | | `BorrowEvent` | `borrow` | **txid**, borrower, usdc\_amount | | `RepayFullEvent` | `repay_full` | **txid**, repayer, p2wsh\_script\_pubkey | | `RepayEvent` | `repay` | **txid**, usdc\_amount | | `LiquidateEvent` | `liquidate` | **txid**, keeper, bonus\_paid | | `SupplyEvent` | `supply` | **supplier**, usdc\_amount, total\_supplied | | `WithdrawEvent` | `withdraw` | **supplier**, usdc\_amount, total\_supplied | | `PausedSetEvent` | `paused_set` | **admin**, paused | | `OracleSetEvent` | `oracle_set` | **admin**, new\_oracle | # Contributing to Writz Source: https://docs.writz.xyz/developers/contribution-guide Writz is open source. Contributions - bug reports, code improvements, documentation, and security research - are welcome. This page explains how to contribute effectively. *** ## Ways to Contribute **Bug reports:** If you find a bug in any of the contracts, circuits, relayer, or bitcoin-script toolkit, open a GitHub issue with a minimal reproduction. Include the test case if possible. **Code contributions:** See the open issues tagged `good first issue` or `help wanted` on GitHub. All contributions require tests. The CI must pass before merge. **Documentation:** Spotted something unclear, incorrect, or missing? Open a PR directly against the `docs/` directory. **Security research:** See the [Bug Bounty](/security/bug-bounty) page for responsible disclosure guidelines and rewards. **ZK ceremony participation:** The privacy edition (in progress, not part of v1) will need a Groth16 trusted setup ceremony with independent participants; it has no date yet. If you're interested in participating, [open an issue](https://github.com/WritzProtocol/writz/issues) (`writz.xyz` has no mailbox yet). No technical background required - participants just need to run a script and discard their randomness. *** ## Development Setup ```bash theme={null} git clone https://github.com/WritzProtocol/writz.git cd writz # Install all dependencies. Note the package manager differs per module: # Bun for bitcoin-script / relayer / frontend / packages/*, npm for circuits. cd contracts && cargo fetch cd ../privacy/contracts && cargo fetch cd ../../bitcoin-script && bun install && bun run build cd ../privacy/packages/commitment-tree && bun install cd ../../../relayer && bun install cd ../privacy/circuits && npm install ``` All 623 tests should pass on a clean checkout. *** ## Secrets `relayer/.env.example` and `frontend/app/.env.example` are tracked deliberately, so no `.gitignore` rule protects them, and a directory-wide `git add` stages whatever they happen to contain. A pre-commit hook scans staged additions for live-looking credentials, `sk_` keys, Stellar `S...` seeds, Bitcoin WIF keys, PEM private key blocks and AWS access key ids. Install it once per clone: ```bash theme={null} git config core.hooksPath scripts/hooks ``` It reads staged content only, so it costs nothing on an unrelated commit and a clean working tree cannot hide anything from it. Documentation placeholders like `sk_...` or `sk_your_api_key_here` do not trip it. Two habits the hook does not replace: * Review the staged diff, not the list of filenames. A file showing as modified when you did not expect to touch it is the signal worth stopping on. * Treat a pushed secret as published. Deleting the commit does not un-publish it, and an orphaned commit stays reachable by its SHA. Revoke first. ## Keeping the docs in sync Contract addresses and test counts are quoted in a lot of places (README, SCF docs, community posts) and drift easily after a redeploy or when tests are added. Two scripts guard against that: * After a redeploy, update `contracts/deployments/testnet.md` - it's the source of truth every other doc is checked against. * After adding/removing tests, run `node scripts/update-test-counts.mjs` to regenerate `docs/_data/facts.json` from the real test suites. * Before opening a PR that touches deployments or docs, run `node scripts/check-docs-sync.mjs` - it scans every `.md` file for contract addresses and "N tests" mentions that don't match the current truth, and CI runs it on every PR regardless. *** ## Code Standards ### Rust / Soroban Contracts * Run `cargo fmt` and `cargo clippy` before committing * Every public function must have a test * Use `#[contractevent]` for all events - no deprecated `Events` API * Per-entry persistent storage only - no growing `Vec` or `Map` in instance storage * Manage TTL: every write to persistent storage must set TTL thresholds * No `unwrap()` on user inputs - return errors via `Result` or panic with descriptive messages ### TypeScript (Relayer + Bitcoin Script) * Run `bun run lint` before committing * Every function must have a test covering the happy path and at least one error case * No `any` types - explicit type annotations required * Use the `Result` pattern for operations that can fail ### Circom Circuits * Every constraint must be intentional - document why it exists * All public inputs must be explicitly marked `<== signal (public)` * Test proof generation AND proof verification - not just compilation * Test with both valid and invalid inputs - verify that invalid inputs are rejected *** ## Pull Request Process 1. Fork the repository and create a feature branch (`feature/your-feature`) 2. Make your changes with tests 3. Run `cargo test` (contracts), `bun test` (bitcoin-script), `bun run test` (relayer), `npm test` (circuits) - all must pass 4. Open a PR with a clear description of what changed and why 5. A maintainer will review within 5 business days *** ## Open Items The following areas are actively looking for contributors: | Area | Description | Skill needed | | - | - | - | | Frontend (app.writz.xyz) | React/Next.js frontend for Writz Lend | React, TypeScript, Stellar Wallets Kit | | WASM prover integration | Integrate circom WASM prover in browser | TypeScript, snarkjs | | Phase 2 ceremony tooling | Scripts for Powers of Tau Phase 2 ceremony | snarkjs, cryptography | | Taproot migration | Upgrade P2WSH to P2TR for Phase 2 | Bitcoin Script, bitcoinjs-lib | | SDK npm package | `writz-sdk` TypeScript package for SPV proof assembly | TypeScript, npm | | Oracle integration | Integrate Pyth + Reflector SEP-40 adapters | Rust, Soroban | If you want to work on any of these, open an issue first to discuss the approach before writing code. # For AI Agents Source: https://docs.writz.xyz/developers/for-ai-agents Compact, exact integration facts for AI coding agents: Writz testnet contract IDs, network passphrase, RPC URL, function signatures, the relayer proof route, verified examples, and what does not exist. This page is written for AI coding agents (and the people who prompt them). Everything here is exact and was checked against Stellar testnet on 2026-10-23. If something you need is not on this page or in [Build on Writz](/developers/integrate), it probably does not exist: do not invent it. Machine-readable entry points: [docs.writz.xyz/llms.txt](https://docs.writz.xyz/llms.txt) (index), [docs.writz.xyz/llms-full.txt](https://docs.writz.xyz/llms-full.txt) (all pages), and any page as Markdown by adding `.md` to its URL, for example [docs.writz.xyz/developers/for-ai-agents.md](https://docs.writz.xyz/developers/for-ai-agents.md). **Prompt to give your agent:** ```text theme={null} Integrate Writz on Stellar testnet. Read https://docs.writz.xyz/developers/for-ai-agents.md first and use only the contract IDs, signatures and endpoints it lists. Use @writz/sdk from npm for TypeScript and writz-spv-client from crates.io for Soroban contracts. The Bitcoin network is signet. Simulate reads, never sign them. ``` *** ## Facts ```text theme={null} STELLAR_NETWORK testnet NETWORK_PASSPHRASE Test SDF Network ; September 2015 SOROBAN_RPC_URL https://soroban-testnet.stellar.org BITCOIN_NETWORK signet RELAYER_URL https://api.testnet.writz.xyz BITCOIN_SPV_ID CBX23YKCCJMAMBMSCNP6YVKEXUIYK2CTKSR4TQ3RVBPAFYZFVCW7W4EO WRITZ_LEND_ID CABF7VK7F25ZGDLV6EWSAU3NYQYKDQNDSJIISDMDZAUASMSXUADSHZGG TEST_USDC_SAC CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA (USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5, 7 decimals) SPV_CHECKPOINT_HEIGHT 327925 (signet; only later blocks can be proven) SOROBAN_SDK soroban-sdk = "26" ``` Source of truth for IDs: [`contracts/deployments/testnet.md`](https://github.com/WritzProtocol/writz/blob/main/contracts/deployments/testnet.md), machine-readable in [`contracts/deployments/testnet.json`](https://github.com/WritzProtocol/writz/blob/main/contracts/deployments/testnet.json). The contracts are immutable; a new version gets a new ID there. Testnet `bitcoin-spv` and `writz-lend` were redeployed on 2026-10-25 because testnet rent rose sharply: testnet's live state is above its target size, and the previous contracts gave every entry they created the long mainnet storage windows, so a deposit was charged about 428 XLM in fees. The new contracts take those windows as deployment parameters and run testnet-length ones (a deposit was charged about 89 XLM, measured 2026-10-25). Positions on the previous contracts were closed and their BTC released; their IDs are recorded as superseded in `contracts/deployments/testnet.md`. Any other `bitcoin-spv` or `writz-lend` ID is superseded; do not use it. How the storage windows work: [Contract reference](/developers/contract-reference) and [Storage and TTL management](/how-it-works/stellar-side#storage-and-ttl-management). ## Packages ```text theme={null} NPM_PACKAGE @writz/sdk source packages/sdk PUBLISHED, npm install @writz/sdk CRATE writz-spv-client source contracts/contracts/writz-spv-client PUBLISHED, cargo add writz-spv-client ``` `npm install @writz/sdk` and `cargo add writz-spv-client` both work. The surface: * `@writz/sdk`: `createWritzClient()` (testnet preset), `writz.verifyBitcoinTx(txid)`, `writz.relayer.getSpvProof(txid)` (throws `RelayerError` with `code`, `status`, `retryAfterSeconds`; retries 409 `light_client_behind`, 429 and 503 after `Retry-After`), `writz.spv.verifyTransaction(sorobanArgs)` (throws `ContractError` with `code`, `errorName`), `writz.lend.getProtocolState()`, `writz.lend.getSupplyRateBp()`, `writz.lend.getSupplyBalance(address)`, `writz.lend.buildSupplyUsdc(address, stroops)` and `writz.lend.buildWithdrawSupply(address, stroops)` (unsigned XDR, the SDK never signs). * `writz-spv-client`: `BitcoinSpvClient`, `SpvVerificationResult`, `SPVError`, `find_output_value(&raw_tx, &script_pubkey) -> Option`, `txid(&env, &raw_tx)`. ## Does not exist (do not generate code for it) * Writz publishes exactly two packages: `@writz/sdk` on npm and `writz-spv-client` on crates.io. There is no `writz-sdk` and no other `@writz/...` package. * `verify_transaction` has **no** `headers` argument and its result has **no** `outputs` field. * No Bitcoin mainnet, testnet3 or testnet4 verification. Signet only. * No Writz contract on Stellar mainnet. * No relayer integration route other than those listed below (the rest serve the Writz app); no API keys; no webhooks. * External contracts cannot call `bitcoin-spv.consume_deposit` (admin-registered consumers only). *** ## `bitcoin-spv` interface ```rust theme={null} // Main primitive. Read-only, no auth. fn verify_transaction(block_hash: BytesN<32>, merkle_proof: Vec>, tx_index: u32, raw_tx: Bytes, min_confirmations: u32) -> Result; fn get_best_tip() -> Option; // { hash: BytesN<32>, height: u32, chainwork: U256 } fn get_header(block_hash: BytesN<32>) -> Option; // { prev: BytesN<32>, merkle_root: BytesN<32>, height: u32, bits: u32, time: u32, period_start_time: u32, chainwork: U256 } fn get_canonical_hash(height: u32) -> Option>; fn get_checkpoint() -> Option; fn is_deposit_consumed(txid: BytesN<32>) -> bool; pub struct SpvVerificationResult { pub txid: BytesN<32>, // SHA256d(raw_tx), INTERNAL byte order pub block_hash: BytesN<32>, // INTERNAL byte order pub block_height: u32, pub confirmations: u32, // depth below the light client's tip, tip = 1 } ``` Rules: `block_hash` and every `merkle_proof` item in internal byte order (reverse of explorers); `raw_tx` without witness data, never exactly 64 bytes; `min_confirmations >= 1`. Inclusion only: to check amount and recipient, parse `raw_tx`. Store accepted txids yourself (replay protection). Errors (`Error(Contract, #N)`): 2 InsufficientConfirmations, 3 ZeroMinConfirmations, 5 MerkleProofInvalid, 7 EmptyTransaction, 15 AmbiguousTransactionLength, 22 HeaderNotFound, 23 NotOnBestChain, 24 BlockNotAfterCheckpoint. ## `writz-lend` reads and the lender side ```rust theme={null} fn get_protocol_state() -> ProtocolState; // { total_scaled_supply: i128, total_borrowed: i128, reserve_usdc: i128, supply_index: i128, // borrow_index: i128, index_ledger: u32, last_keeper_heartbeat: u64 } fn get_borrow_rate_bp() -> i128; // annual, basis points fn get_supply_rate_bp() -> i128; // annual, basis points fn get_supply_balance(lender: Address) -> i128; // USDC stroops (7 decimals) fn get_position(txid: BytesN<32>) -> Option; // txid of the BTC deposit, internal order fn get_health_ratio_bp(txid: BytesN<32>) -> Result; // 15000 = 150%, liquidation at 12000 ``` ```rust theme={null} // Lender side: permissionless writes, the supplier signs. Amounts in TEST_USDC_SAC stroops. fn supply_usdc(supplier: Address, amount: i128) -> Result<(), WritzLendError>; fn withdraw_supply(supplier: Address, amount: i128) -> Result<(), WritzLendError>; ``` Indexes are scaled by 10^18. Lender yield is the interest Bitcoin borrowers pay: `get_supply_rate_bp` is variable and reads `0` while nothing, or very little, is borrowed, so read it live and never present a fixed APY. `get_supply_balance` includes accrued interest. `withdraw_supply` fails with #13 InsufficientSupply above the lender's balance and #11 InsufficientLiquidity above what is not lent out; `supply_usdc` fails with #17 Paused while the pool is paused, and the supplier needs a trustline to the test USDC (without one, simulation fails inside the token contract). Generate a typed client from the deployed ID for anything else; the full list is in [Contract Reference](/developers/contract-reference). ## Relayer ```text theme={null} GET / service index (JSON) GET /health status GET /spv-proof/:txid?confirmations=N proof for a confirmed signet tx; txid in DISPLAY order; N in 1..20, default 6 GET /deposit-status/:txid writz-lend deposit progress: stage (unseen|mempool|confirming|ready|verified), confirmations, required, stellarTxHash ?stellarTx= the app's deposit submission: adds stages submitted|failed and `submission` {status, reason}; 400 stellar_tx_mismatch if it is not this deposit GET /timeline/:btcTxid full loan cycle: { btcTxid, positionId, owner, status, complete, gaps[], events[] }, each event { kind, chain, hash, at, detail?, url } POST /cctp/relay {"burnHash":"<64 hex>"} mints on Base Sepolia a USDC burn sent from Stellar with CCTP V2; returns { kind, ... } GET /cctp/relay/:burnHash transfer status: kind (sent|confirming|confirmed|arrived|failed), mintHash, explorerUrl, reason, retrying, nextRetryAt ``` `/spv-proof` 200 body: `{ txid, rawTxNoWitness, blockHash, txIndex, merkleProof[], headers[], blockHeight, confirmations, sorobanArgs: { block_hash, merkle_proof[], tx_index, raw_tx, min_confirmations } }`. Hex strings. `sorobanArgs` is already in internal byte order and maps one to one onto `verify_transaction`. Errors: 400 `invalid_txid` or `invalid_confirmations`, 404 `unconfirmed`, 404 `tx_not_found` (txid not on signet), 409 `insufficient_confirmations` (`requested`, `available`), 409 `light_client_behind` (block not yet in `bitcoin-spv`; calling the contract now fails with #22 or #2), 429 `rate_limited` (60 per minute per IP), 502 `upstream_error`, 503 `upstream_unavailable`. On 409, 429 and 503, wait `retryAfterSeconds` (body) or `Retry-After` (header) before retrying; never retry in a tight loop. CORS is open. Signet coins for an integration come from a public Signet faucet (for example [signetfaucet.com](https://signetfaucet.com) or [bitcoinsignetfaucet.com](https://bitcoinsignetfaucet.com)); claim them by hand and never automate around a faucet's captcha. The relayer's `/faucet/signet` route is not for agents or builders: it only funds accounts created through the Writz testnet app's social login, needs that login's session token, and refuses every other request. `/timeline` lists one event per transaction of a writz-lend position, in chain order: `btc_lock` and `released` on `bitcoin`, `verified`, `borrowed`, `received` and `repaid` on `stellar`, and a `received` mint on `base-sepolia`. `at` is Unix ms or null; `url` is the mempool.space, stellar.expert or Basescan page. A `received` event (a swap to a stablecoin or XLM, or a CCTP burn) is linked to the loan by a rule, not by the chain: it belongs to the owner's most recent borrow before it, with no borrow or repay by the owner in between. `complete: false` with `gaps` means a source could not be read in full; the events listed are still real. Errors: 400 `invalid_txid`, 404 `position_not_found` (use `/deposit-status` before the deposit is verified), 429 `rate_limited` (60 per minute per IP), 503 `stellar_unavailable` or `lending_not_configured`. `/cctp/relay` completes a Stellar to Base Sepolia CCTP V2 transfer so the recipient needs no wallet or gas on Base Sepolia: call `POST` once with the Stellar hash of the `deposit_for_burn`, then poll `GET /cctp/relay/:burnHash` every few seconds until `kind` is `arrived` (`mintHash` is the Base Sepolia transaction, `null` only while it is still being looked up) or `failed` (`reason`). Transient failures (an RPC outage, an empty gas account, the daily cap, Circle slow to confirm) are retried by the relay itself: the status then stays `confirming` or `confirmed` with `retrying: true`, `reason` and `nextRetryAt`; `failed` is final (a refused or invalid message, or 24 hours of retrying). Asking again never mints twice. Only burns from Stellar (domain 27) to Base Sepolia (domain 6) through Circle's TokenMessengerV2, with no destination caller and no hook, are relayed. Errors: 400 `invalid_burn_hash`, 404 `not_found` (on GET, before any POST), 429 `rate_limited` (30 per IP per 10 minutes), 503 `cctp_relay_disabled` or `relay_busy`. The other routes the index lists (`/merkle-path`, `/notes`, `/insert-commitment`, `/update-leaf`, `/metrics/*`) serve the Writz app and are not an integration surface. Yield on USDC is the `writz-lend` lender side, called on the contract directly (above), not through the relayer. *** ## Verified examples **CLI** (simulation; the source account is any valid G address and is not charged): ```bash theme={null} SPV=CBX23YKCCJMAMBMSCNP6YVKEXUIYK2CTKSR4TQ3RVBPAFYZFVCW7W4EO READER=GB2BSYQS3FRJ5LZSSIDF3ZCSG5MKWJT5SZ3OZO4QRCAMCR357YAVPTWT TXID=eac0f95c531b4bd5307597f74bedd7379928539056b9a1467ea6d1159507e269 curl -s "https://api.testnet.writz.xyz/spv-proof/$TXID" > proof.json stellar contract invoke --id $SPV --network testnet --source-account $READER --send=no -- \ verify_transaction \ --block_hash "$(jq -r .sorobanArgs.block_hash proof.json)" \ --merkle_proof "$(jq -c .sorobanArgs.merkle_proof proof.json)" \ --tx_index "$(jq -r .sorobanArgs.tx_index proof.json)" \ --raw_tx "$(jq -r .sorobanArgs.raw_tx proof.json)" \ --min_confirmations 6 # {"block_hash":"fe324a44...02000000","block_height":327928,"confirmations":7,"txid":"69e20795...5cf9c0ea"} ``` **TypeScript** (`@stellar/stellar-sdk` 16.x, read-only): ```ts theme={null} import { Account, BASE_FEE, Contract, Keypair, TransactionBuilder, nativeToScVal, rpc, scValToNative, xdr } from "@stellar/stellar-sdk"; const server = new rpc.Server("https://soroban-testnet.stellar.org"); const spv = new Contract("CBX23YKCCJMAMBMSCNP6YVKEXUIYK2CTKSR4TQ3RVBPAFYZFVCW7W4EO"); const hex = (s: string) => Buffer.from(s, "hex"); const txid = "eac0f95c531b4bd5307597f74bedd7379928539056b9a1467ea6d1159507e269"; const a = (await (await fetch(`https://api.testnet.writz.xyz/spv-proof/${txid}`)).json()).sorobanArgs; const tx = new TransactionBuilder(new Account(Keypair.random().publicKey(), "0"), { fee: BASE_FEE, networkPassphrase: "Test SDF Network ; September 2015", }) .addOperation(spv.call("verify_transaction", nativeToScVal(hex(a.block_hash)), xdr.ScVal.scvVec(a.merkle_proof.map((h: string) => nativeToScVal(hex(h)))), nativeToScVal(a.tx_index, { type: "u32" }), nativeToScVal(hex(a.raw_tx)), nativeToScVal(6, { type: "u32" }))) .setTimeout(30) .build(); const sim = await server.simulateTransaction(tx); if (rpc.Api.isSimulationError(sim)) throw new Error(sim.error); // "...Error(Contract, #N)..." console.log(scValToNative(sim.result!.retval)); // { txid, block_hash, block_height, confirmations } ``` Runnable: [`examples/verify-bitcoin-tx`](https://github.com/WritzProtocol/writz/tree/main/examples/verify-bitcoin-tx). **Rust, cross-contract** (soroban-sdk 26): ```rust theme={null} #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub struct SpvVerificationResult { pub txid: BytesN<32>, pub block_hash: BytesN<32>, pub block_height: u32, pub confirmations: u32 } #[contractclient(name = "BitcoinSpvClient")] pub trait BitcoinSpv { fn verify_transaction(env: Env, block_hash: BytesN<32>, merkle_proof: Vec>, tx_index: u32, raw_tx: Bytes, min_confirmations: u32) -> SpvVerificationResult; } let proof = match BitcoinSpvClient::new(&env, &spv_id) .try_verify_transaction(&block_hash, &merkle_proof, &tx_index, &raw_tx, &6) { Ok(Ok(r)) => r, _ => return Err(Error::ProofRejected) }; // Output check, with spv-types as a git dependency: // spv-types = { git = "https://github.com/WritzProtocol/writz", rev = "0a1065991ee0498ad2c54367e0ffa69ca4a59a9a" } let sats: Option = spv_types::btc_parser::find_p2wsh_output(&raw_tx, &expected_script_pubkey); // any script type ``` Complete contract with tests, deployed and run against testnet: [`examples/spv-payment-check`](https://github.com/WritzProtocol/writz/tree/main/examples/spv-payment-check). *** ## Checklist before you answer "it works" 1. `get_best_tip` returns a height near the signet tip on mempool.space. 2. The txid is a **signet** transaction with enough confirmations, in a block above 327,925. 3. You passed `sorobanArgs` through unchanged (no extra byte reversal). 4. You simulated reads (`--send=no`, `simulateTransaction`) and only signed real writes. 5. Your contract checks the output (amount and scriptPubKey) and stores the txid. # Build on Writz Source: https://docs.writz.xyz/developers/integrate Integrate Writz in 15 minutes: prove a Bitcoin transaction from your Soroban contract or your app, with live testnet contract IDs and copy-paste snippets that were run against testnet. **Prove a Bitcoin transaction on Stellar in 15 minutes.** This page is for builders who want to use Writz from their own project. If you want to build and test this repository instead, see [Quick Start](/developers/quick-start), the contributor guide. If you are an AI coding agent, [For AI Agents](/developers/for-ai-agents) has the same facts in a compact form. Every command and response on this page was run against Stellar testnet and Bitcoin signet on 2026-10-23. Values that change over time (the Bitcoin tip, confirmation counts) will differ when you run them. *** ## What you can build on today | Surface | What it gives you | Status | | - | - | - | | `bitcoin-spv` contract, `verify_transaction` | Proof, inside Soroban, that a Bitcoin signet transaction is in a block of the most-work chain with at least N confirmations | Live on testnet, verified below | | Relayer `GET /spv-proof/:txid` | A ready-made proof for any confirmed signet transaction, already in the argument format `verify_transaction` takes | Live, verified below | | `bitcoin-spv` reads (`get_best_tip`, `get_header`, `get_canonical_hash`, `get_checkpoint`) | The light client's view of Bitcoin: tip height, block hashes, header timestamps | Live, verified below | | `writz-lend` reads (`get_protocol_state`, rates, `get_position`, `get_supply_balance`) | Pool size, rates and positions of the Writz lending pool | Live on testnet, reads verified below | | `writz-lend` lender side (`supply_usdc`, `withdraw_supply`, `get_supply_balance`, `get_supply_rate_bp`) | Yield on USDC from the interest Bitcoin borrowers pay, without a contract of your own | Live on testnet, see [section 7](#7-earn-yield-on-usdc-as-a-lender) | | The testnet app, [testnet.writz.xyz](https://testnet.writz.xyz) | Deposit signet BTC, borrow test USDC | Link to it; there is no embed or widget API | Ideas that fit in a weekend: a Soroban contract that releases something (an NFT, access, an escrowed token) once a Bitcoin payment confirms; a Bitcoin payment checker for a store or invoice app; a dashboard of the Writz pool; a bot that watches a Bitcoin address and proves its payments on Stellar. **Not available yet:** Bitcoin mainnet or testnet3/testnet4 verification on Stellar (the testnet light client tracks **signet** only), and any Writz contract on Stellar mainnet. See [Bitcoin SPV SDK](/developers/spv-sdk#not-yet-available). *** ## Packages | Package | Registry | For | Status | | - | - | - | - | | [`@writz/sdk`](https://github.com/WritzProtocol/writz/tree/main/packages/sdk) | npm | TypeScript in the browser, Node or Bun: testnet preset, relayer client with typed errors and `Retry-After`, `bitcoin-spv` and `writz-lend` clients | Published: [`npm install @writz/sdk`](https://www.npmjs.com/package/@writz/sdk) | | [`writz-spv-client`](https://github.com/WritzProtocol/writz/tree/main/contracts/contracts/writz-spv-client) | crates.io | Soroban contracts: `BitcoinSpvClient`, the result and error types, the on-chain output parser `find_output_value` | Published: [`cargo add writz-spv-client`](https://crates.io/crates/writz-spv-client) | Both are published: `npm install @writz/sdk` and `cargo add writz-spv-client`. The snippets on this page also work without them, with only `@stellar/stellar-sdk` or `soroban-sdk`. The package code and its tests are in the repository either way. *** ## Network and contract IDs | | Value | | - | - | | Stellar network | Testnet | | Network passphrase | `Test SDF Network ; September 2015` | | Soroban RPC | `https://soroban-testnet.stellar.org` | | Bitcoin network | Signet | | Writz relayer | `https://api.testnet.writz.xyz` | | `bitcoin-spv` | `CBX23YKCCJMAMBMSCNP6YVKEXUIYK2CTKSR4TQ3RVBPAFYZFVCW7W4EO` | | `writz-lend` | `CABF7VK7F25ZGDLV6EWSAU3NYQYKDQNDSJIISDMDZAUASMSXUADSHZGG` | | Test USDC (Stellar Asset Contract for `USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5`) | `CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA` | | BTC price oracle read by `writz-lend` (Reflector) | `CCYOZJCOPG34LLQQ7N24YXBM7LL62R7ONMZ3G6WZAAYPB5OYKOMJRN63` | The source of truth for every ID, with deploy transactions and wasm hashes, is [`contracts/deployments/testnet.md`](https://github.com/WritzProtocol/writz/blob/main/contracts/deployments/testnet.md). The same values, machine-readable, are in [`contracts/deployments/testnet.json`](https://github.com/WritzProtocol/writz/blob/main/contracts/deployments/testnet.json) and in the `@writz/sdk` network preset, both generated from that file. Writz contracts are never upgraded in place: a new version gets a new ID, recorded there. The `bitcoin-spv` checkpoint is signet block **327,925**. Only blocks after it can be proven. Testnet `bitcoin-spv` and `writz-lend` were redeployed on 2026-10-25 because testnet rent rose sharply: testnet's live state is above its target size, and the previous contracts gave every entry they created the long mainnet storage windows, so a deposit was charged about 428 XLM in fees. The new contracts take those windows as deployment parameters and run testnet-length ones (a deposit was charged about 89 XLM, measured 2026-10-25). Positions on the previous contracts were closed and their BTC released; their IDs are recorded as superseded in `contracts/deployments/testnet.md`. A transaction in a block at or below the new checkpoint can no longer be proven; pick one above it. How the storage windows work: [Contract reference](/developers/contract-reference) and [Storage and TTL management](/how-it-works/stellar-side#storage-and-ttl-management). *** ## 1. Check the light client (1 minute) Install the [Stellar CLI](https://developers.stellar.org/docs/tools/cli) (`brew install stellar-cli`). Read calls are simulated, never submitted, so the source account only has to be a valid address; it is not charged and does not sign. ```bash theme={null} SPV=CBX23YKCCJMAMBMSCNP6YVKEXUIYK2CTKSR4TQ3RVBPAFYZFVCW7W4EO READER=GB2BSYQS3FRJ5LZSSIDF3ZCSG5MKWJT5SZ3OZO4QRCAMCR357YAVPTWT # any G... address stellar contract invoke --id $SPV --network testnet --source-account $READER --send=no -- get_best_tip ``` ```json theme={null} {"chainwork":"1813841685","hash":"64460cd64bcb2b7b89262a51c5a913520670b55c00a590760848bdde12000000","height":327934} ``` `height` should be at or near the signet tip on [mempool.space/signet](https://mempool.space/signet). The Writz relayer submits new signet headers to the contract as they are mined. ## 2. Get a proof from the relayer Pick any confirmed signet transaction (here one in block 327,928) and ask the relayer for its proof: ```bash theme={null} TXID=eac0f95c531b4bd5307597f74bedd7379928539056b9a1467ea6d1159507e269 curl -s "https://api.testnet.writz.xyz/spv-proof/$TXID?confirmations=6" > proof.json jq '.sorobanArgs | {block_hash, tx_index, proof_len: (.merkle_proof|length), min_confirmations}' proof.json ``` ```json theme={null} { "block_hash": "fe324a4450cea25959ed180d312172c8484e8e08837b75835d3ec48f02000000", "tx_index": 842, "proof_len": 10, "min_confirmations": 6 } ``` The full response has `txid`, `rawTxNoWitness`, `blockHash`, `txIndex`, `merkleProof`, `headers`, `blockHeight`, `confirmations` and `sorobanArgs`. Use `sorobanArgs`: its five fields are exactly the five arguments of `verify_transaction`, as hex strings and integers, already in the byte order the contract expects. `confirmations` (query parameter, 1 to 20, default 6) is the minimum you ask for, not the block's actual depth. The relayer builds proofs from public signet data (mempool.space). It holds no keys for you and you do not have to trust it: a wrong proof simply fails on chain. ## 3. Verify it on Stellar ```bash theme={null} stellar contract invoke --id $SPV --network testnet --source-account $READER --send=no -- \ verify_transaction \ --block_hash "$(jq -r .sorobanArgs.block_hash proof.json)" \ --merkle_proof "$(jq -c .sorobanArgs.merkle_proof proof.json)" \ --tx_index "$(jq -r .sorobanArgs.tx_index proof.json)" \ --raw_tx "$(jq -r .sorobanArgs.raw_tx proof.json)" \ --min_confirmations 6 ``` ```json theme={null} {"block_hash":"fe324a4450cea25959ed180d312172c8484e8e08837b75835d3ec48f02000000","block_height":327928,"confirmations":7,"txid":"69e2079515d1a67e46a1b9569053289937d7ed4bf7977530d54b1b535cf9c0ea"} ``` That is the whole primitive. `confirmations` is the block's real depth below the light client's tip. Note that `txid` comes back reversed compared to the `TXID` you passed in: see [Byte order](#byte-order). ## 4. From TypeScript Runnable example: [`examples/verify-bitcoin-tx`](https://github.com/WritzProtocol/writz/tree/main/examples/verify-bitcoin-tx) (Bun and `@stellar/stellar-sdk`, read-only, no keys). ```bash theme={null} cd examples/verify-bitcoin-tx && bun install && bun run start ``` With `@writz/sdk` (`npm install @writz/sdk`) the whole flow is three lines, and relayer errors arrive typed, with the `Retry-After` wait already honoured: ```ts theme={null} import { createWritzClient } from "@writz/sdk"; const writz = createWritzClient(); // testnet preset const { result } = await writz.verifyBitcoinTx("eac0f95c531b4bd5307597f74bedd7379928539056b9a1467ea6d1159507e269"); // { txid, txidDisplay, blockHash, blockHeight: 327928, confirmations } ``` Without it, the quickest typed client is generated from the deployed contract: ```bash theme={null} stellar contract bindings typescript --network testnet \ --contract-id CBX23YKCCJMAMBMSCNP6YVKEXUIYK2CTKSR4TQ3RVBPAFYZFVCW7W4EO \ --output-dir bitcoin-spv-client cd bitcoin-spv-client && bun install && bun run build ``` ```ts theme={null} import { Client, networks } from "./bitcoin-spv-client/dist/index.js"; const spv = new Client({ ...networks.testnet, rpcUrl: "https://soroban-testnet.stellar.org" }); const hex = (s: string) => Buffer.from(s, "hex"); const txid = "eac0f95c531b4bd5307597f74bedd7379928539056b9a1467ea6d1159507e269"; const { sorobanArgs: a } = await ( await fetch(`https://api.testnet.writz.xyz/spv-proof/${txid}`) ).json(); const { result } = await spv.verify_transaction({ block_hash: hex(a.block_hash), merkle_proof: a.merkle_proof.map(hex), tx_index: a.tx_index, raw_tx: hex(a.raw_tx), min_confirmations: 6, }); console.log(result.isOk() ? result.unwrap() : result.unwrapErr()); // { block_hash: , block_height: 327928, confirmations: 7, txid: } ``` The same command generates a client for `writz-lend` (`--contract-id CABF7VK7F25ZGDLV6EWSAU3NYQYKDQNDSJIISDMDZAUASMSXUADSHZGG`). Generate bindings from the deployed contract ID, not from the source on `main`: the source can be ahead of what is deployed. ## 5. From your Soroban contract Your contract calls `bitcoin-spv` with a cross-contract client. The crate `writz-spv-client` contains the client, the types and the output parser: add `writz-spv-client = "0.1"` to `Cargo.toml` and `use writz_spv_client::{find_output_value, BitcoinSpvClient};`. Without the crate, declare the result type yourself with these exact field names (they are decoded by name): ```rust theme={null} use soroban_sdk::{contractclient, contracttype, Bytes, BytesN, Env, Vec}; #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub struct SpvVerificationResult { pub txid: BytesN<32>, // SHA256d(raw_tx), internal byte order pub block_hash: BytesN<32>, // internal byte order pub block_height: u32, pub confirmations: u32, // depth below the light client's tip, tip = 1 } #[contractclient(name = "BitcoinSpvClient")] pub trait BitcoinSpv { fn verify_transaction( env: Env, block_hash: BytesN<32>, merkle_proof: Vec>, tx_index: u32, raw_tx: Bytes, min_confirmations: u32, ) -> SpvVerificationResult; } // Inside a contract function (spv: Address is the bitcoin-spv contract ID): let proof = match BitcoinSpvClient::new(&env, &spv) .try_verify_transaction(&block_hash, &merkle_proof, &tx_index, &raw_tx, &6) { Ok(Ok(result)) => result, _ => return Err(Error::ProofRejected), // your own contract error }; ``` `verify_transaction` proves **inclusion and depth only**. It does not tell you what the transaction paid or to whom. To check that, parse `raw_tx` in your contract. The shared Writz crate `spv-types` has an on-chain parser you can use as a git dependency: ```toml theme={null} [dependencies] soroban-sdk = "26" spv-types = { git = "https://github.com/WritzProtocol/writz", rev = "0a1065991ee0498ad2c54367e0ffa69ca4a59a9a" } ``` ```rust theme={null} // Value in sats of the first output whose scriptPubKey equals `expected_spk` // (works for any script type: P2WPKH, P2WSH, P2TR, ...). None if there is none. let sats: Option = spv_types::btc_parser::find_p2wsh_output(&raw_tx, &expected_spk); ``` And store the txids you accept: `verify_transaction` is a pure check and will verify the same transaction again for anyone, so replay protection is yours. The complete contract, with tests, is [`examples/spv-payment-check`](https://github.com/WritzProtocol/writz/tree/main/examples/spv-payment-check): it accepts a Bitcoin payment of at least N sats to a given scriptPubKey, once per txid. It was deployed to testnet and run against the real `bitcoin-spv` with the proof above; its README has the commands. ## 6. Read the Writz lending pool ```bash theme={null} LEND=CABF7VK7F25ZGDLV6EWSAU3NYQYKDQNDSJIISDMDZAUASMSXUADSHZGG stellar contract invoke --id $LEND --network testnet --source-account $READER --send=no -- get_protocol_state stellar contract invoke --id $LEND --network testnet --source-account $READER --send=no -- get_borrow_rate_bp stellar contract invoke --id $LEND --network testnet --source-account $READER --send=no -- get_supply_balance --lender $READER ``` ```json theme={null} {"borrow_index":"1000000000000000000","index_ledger":5352956,"last_keeper_heartbeat":1792788367,"reserve_usdc":"0","supply_index":"1000000000000000000","total_borrowed":"0","total_scaled_supply":"10000000000"} "0" "10000000000" ``` USDC amounts are in stroops (7 decimals: `10000000000` is 1,000 USDC). Rates are annual basis points (`800` is 8%). Indexes are scaled by 10^18. `get_position` and `get_health_ratio_bp` take a deposit txid in internal byte order; `get_health_ratio_bp` returns basis points (`15000` is 150%, liquidation at `12000`). Full interface: [Contract Reference](/developers/contract-reference). `supply_usdc` and `withdraw_supply` are permissionless writes signed by the supplier; [section 7](#7-earn-yield-on-usdc-as-a-lender) covers them. Borrowing needs a real signet deposit to the Writz script, which is what the [testnet app](https://testnet.writz.xyz) does for you. ## 7. Earn yield on USDC as a lender Yield on Writz is the lender side of `writz-lend`. Lenders supply USDC to the pool that Bitcoin borrowers draw from, and the interest those borrowers pay is the lenders' yield. You need no contract of your own: the user's wallet signs `supply_usdc` and `withdraw_supply`, and your app reads the balance and the rate by simulation. The asset is the same test USDC as the rest of this page (`USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5`), and the user's account needs a trustline to it. ```bash theme={null} LEND=CABF7VK7F25ZGDLV6EWSAU3NYQYKDQNDSJIISDMDZAUASMSXUADSHZGG stellar contract invoke --id $LEND --network testnet --source-account me --send=no -- get_supply_rate_bp stellar contract invoke --id $LEND --network testnet --source-account me -- supply_usdc --supplier $(stellar keys address me) --amount 100000000 stellar contract invoke --id $LEND --network testnet --source-account me --send=no -- get_supply_balance --lender $(stellar keys address me) stellar contract invoke --id $LEND --network testnet --source-account me -- withdraw_supply --supplier $(stellar keys address me) --amount 100000000 ``` `100000000` is 10 USDC. `get_supply_balance` includes the interest earned so far. `withdraw_supply` fails with `#13` (`InsufficientSupply`) above the lender's balance and with `#11` (`InsufficientLiquidity`) above the USDC that is not lent out at that moment. The supply rate is variable and is zero while nothing is borrowed; with very little borrowed it still reads `0`, because it is counted in whole basis points (`500` is 5% a year). Read it live and label it as variable. Never show a fixed APY. With `@writz/sdk` these are `lend.getSupplyRateBp`, `lend.getSupplyBalance`, `lend.buildSupplyUsdc` and `lend.buildWithdrawSupply`; the builders return unsigned transactions for the wallet and never sign. A browser flow with `@stellar/stellar-sdk` and product ideas built on it are in [What to build on Writz](/developers/request-for-startups#no-soroban-needed-build-on-writz-yield). *** ## Byte order Bitcoin explorers show txids and block hashes **reversed** (display order). `bitcoin-spv` works in **internal** order, the raw SHA256d output. * The relayer route takes the txid in **display** order (as on mempool.space) and returns `sorobanArgs` already in internal order. Pass them through unchanged. * `verify_transaction` returns `txid` and `block_hash` in internal order. To compare with an explorer, reverse the bytes: `69e20795...5cf9c0ea` is `eac0f95c...9507e269`. * If you build proofs yourself from an Esplora API, reverse the block hash and every Merkle sibling before calling the contract. ## Common errors From the relayer (`GET /spv-proof/:txid`): | HTTP | `error` | Meaning | | - | - | - | | 400 | `invalid_txid` | Not 64 hex characters | | 400 | `invalid_confirmations` | `confirmations` not between 1 and 20 | | 404 | `unconfirmed` | The transaction is in the mempool, not in a block yet | | 409 | `insufficient_confirmations` | Fewer confirmations than requested; the body has `requested` and `available`. Signet mines a block about every 10 minutes | | 404 | `tx_not_found` | The signet indexer does not know this txid. Mainnet and testnet3/4 txids are not found | | 409 | `light_client_behind` | The block is not yet in `bitcoin-spv`, or not deep enough there. Calling the contract now would fail with `#22` or `#2`. Wait `retryAfterSeconds` (about 60 s, one header-sync cycle) | | 429 | `rate_limited` | More than 60 requests per minute from your IP. Wait `retryAfterSeconds` | | 502 | `upstream_error` | An unexpected answer from the Bitcoin indexer | | 503 | `upstream_unavailable` | The Bitcoin indexer and its fallback are both failing. Wait `retryAfterSeconds` | Every 409, 429 and 503 carries `retryAfterSeconds` in the body and a `Retry-After` header. Honour it instead of retrying in a loop. Proofs for deeply confirmed transactions are cached, so asking again for the same txid is cheap. From `bitcoin-spv` (simulation fails with `Error(Contract, #N)`): | Code | Name | Usual cause | | - | - | - | | 2 | `InsufficientConfirmations` | The block is shallower than `min_confirmations` | | 3 | `ZeroMinConfirmations` | `min_confirmations` is 0 | | 5 | `MerkleProofInvalid` | Wrong `tx_index`, siblings in display order, or a `raw_tx` with witness data | | 7 | `EmptyTransaction` | `raw_tx` is empty | | 15 | `AmbiguousTransactionLength` | `raw_tx` is exactly 64 bytes, never accepted | | 22 | `HeaderNotFound` | The block is not in the light client: before the checkpoint (327,925), not a signet block, `block_hash` in display order, or mined in the last minutes and not submitted yet (check `get_best_tip`) | | 23 | `NotOnBestChain` | The block was orphaned | | 24 | `BlockNotAfterCheckpoint` | The block is the checkpoint or older | Other frequent mistakes: * **Witness data in `raw_tx`.** The contract hashes `raw_tx` to get the txid, so it must be the legacy (non-witness) serialization. The relayer's `raw_tx` already is. * **Sending instead of simulating.** Reads need `--send=no` on the CLI (or `simulateTransaction` in the SDK); there is nothing to sign. * **Generating bindings from source.** Use the deployed contract ID so the client matches what is on chain. ## Faucets and wallets * **Stellar testnet XLM:** `stellar keys generate me --network testnet --fund`, or Friendbot in [Stellar Lab](https://lab.stellar.org) or in the Freighter wallet on testnet. * **Test USDC:** add a trustline to `USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5`, then swap testnet XLM for it in Freighter, or use [faucet.circle.com](https://faucet.circle.com) (Stellar Testnet). This is the USDC the Writz pool uses. * **Bitcoin signet coins:** a signet wallet (Xverse and Sparrow support signet) and its `tb1q...` or `tb1p...` address, then a public Signet faucet such as [signetfaucet.com](https://signetfaucet.com), [bitcoinsignetfaucet.com](https://bitcoinsignetfaucet.com) or [signet.dcorral.com](https://signet.dcorral.com). Their amounts, captchas and limits are set by their operators. The relayer's `/faucet/signet` route is not a builder faucet: it only funds accounts created through the Writz testnet app's social login, once each, and refuses any other request. Coins from a faucet are unconfirmed at first; wait for a block (about 10 minutes) before building a proof. * **Explorers:** [mempool.space/signet](https://mempool.space/signet) for Bitcoin, [stellar.expert/explorer/testnet](https://stellar.expert/explorer/testnet) for Stellar. ## Limits to know * **Testnet and signet only.** Nothing here is on mainnet, and the contracts are not audited. Do not use real funds. * **Signet trust.** Signet blocks are authorised by a signature the contract does not check, so on signet only one designated Writz account may submit headers. Proof-of-work and difficulty are still checked per header. * **Shared relayer.** `api.testnet.writz.xyz` is one instance with no uptime guarantee. If it is down, build the proof yourself from any signet Esplora API (see `relayer/src/bitcoin/spv.ts` for the exact steps) and call the contract directly. *** **Next:** [Bitcoin SPV SDK](/developers/spv-sdk) for the full `verify_transaction` reference, or [For AI Agents](/developers/for-ai-agents) to hand this to your coding agent. # Quick Start Source: https://docs.writz.xyz/developers/quick-start > **Building on Writz from your own project?** This page is the contributor guide: it builds and tests this repository. To call the deployed contracts from your app or your Soroban contract, start with [Build on Writz](/developers/integrate). AI coding agents: [For AI Agents](/developers/for-ai-agents). The core lending and verification logic in this repository is working code, not a mockup: the contracts are deployed on Stellar testnet, and the tests pass (see `docs/developers/runbook.md`). Writz v1 is `bitcoin-spv` and `writz-lend`; `zk-verifier`, `commitment-tree` and the circuits, all under `privacy/`, belong to the separate privacy edition, in progress and testnet only. `writz-lend/src/oracle.rs`'s `get_btc_price_stroops` now calls a real Reflector oracle on testnet (see `docs/research/oracle-design.md`) - the privacy edition's `commitment-tree` still returns a hardcoded price; it is not part of v1. Start here and have something running in under 5 minutes. *** ## Prerequisites Install the following before proceeding: ```bash theme={null} # Rust + WebAssembly target curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh rustup target add wasm32v1-none # Stellar CLI (version 27 or later) # macOS brew install stellar-cli # Linux cargo install --locked stellar-cli # Node.js 20+ # https://nodejs.org/ # Bun 1.1+ - bitcoin-script, relayer, frontend and packages/* install with Bun, # not npm. Only privacy/circuits/ is npm-managed. curl -fsSL https://bun.sh/install | bash # circom (for ZK circuit tests only). # circom 2.x is a Rust binary. `npm install -g circom` installs the legacy 1.x # package and CANNOT compile `pragma circom 2.0.0` - use the release binary: curl -fL -o ~/.local/bin/circom \ https://github.com/iden3/circom/releases/download/v2.2.3/circom-linux-amd64 chmod +x ~/.local/bin/circom # macOS: use circom-macos-amd64 circom --version # expect: circom compiler 2.2.3 # snarkjs needs no global install - it is a dependency of privacy/circuits/ ``` *** ## Clone the Repository ```bash theme={null} git clone https://github.com/WritzProtocol/writz.git cd writz ``` *** ## Run All Tests ### Soroban contracts (Rust) ```bash theme={null} cd contracts cargo test ``` Expected output: 263 tests pass across `bitcoin-spv` (68), `zk-verifier` (26), `commitment-tree` (59), `writz-lend` (100), and `spv-types` (10). ### Bitcoin script toolkit (TypeScript, Bun) ```bash theme={null} cd bitcoin-script bun install bun test ``` Expected output: 60 tests pass. ### Relayer service (TypeScript, Bun install + Jest) The relayer installs with Bun but its suite is Jest (ts-jest), so it must run through the package script - plain `bun test` selects Bun's own runner and fails. It also imports the local `@writz/*` packages via their built `dist/` output, so build those first. ```bash theme={null} cd privacy/packages/commitment-tree && bun install cd ../../../bitcoin-script && bun run build cd ../relayer && bun install && bun run test ``` Expected output: 248 tests pass. ### ZK circuits (Circom + snarkjs, npm) ```bash theme={null} cd privacy/circuits npm install npm test ``` Expected output: 52 tests pass (proof generation, commitment correctness, ratio enforcement, nullifiers). If `verify()` assertions fail here while `prove()` succeeds, your local `privacy/circuits/keys/*_final.zkey` (gitignored, regenerated locally) is out of sync with the committed `privacy/circuits/keys/*_vkey.json`. Run `bash scripts/compile_all.sh && bash scripts/setup_dev.sh` to regenerate both together from a fresh dev trusted setup, then re-run `npm test`. ### All together: 623 tests, all passing. *** ## Build the Contracts ```bash theme={null} cd contracts stellar contract build cd ../privacy/contracts stellar contract build ``` This produces Wasm artifacts in `contracts/target/wasm32v1-none/release/`: * `bitcoin_spv.wasm` - 28.4 KB * `writz_lend.wasm` - 23.7 KB and, for the privacy edition, in `privacy/contracts/target/wasm32v1-none/release/`: * `zk_verifier.wasm` - 11.8 KB * `commitment_tree.wasm` - \~38 KB *** ## Use the Testnet Deployments All four contracts are live on Stellar testnet. You can call them directly without deploying: ```bash theme={null} # Check the SPV contract is alive stellar contract invoke \ --id CBX23YKCCJMAMBMSCNP6YVKEXUIYK2CTKSR4TQ3RVBPAFYZFVCW7W4EO \ --network testnet \ -- get_version # Check the Merkle root stellar contract invoke \ --id CAQBLCAPGXXCZTJDAJLY7E4PTJYGR6CY4YWH4JSJKFXYIHW3GANP6RFM \ --network testnet \ -- get_merkle_root # Returns: 0x2134e76ac74b4b8765b6e37992aa15f0... (Poseidon-2 empty tree root) ``` *** ## Run the Privacy Edition End-to-End Flow `scripts/deploy/e2e_local.mjs` deploys all four contracts through their constructors on a local Stellar network, registers the verification keys and runs deposit → borrow → repay → release with real Groth16 proofs. It also replays the closed attacks (a deposit claiming someone else's output, a redirected borrow, a re-borrow after release, the same txid in both lenders) and expects each to fail. CI runs it as the `e2e-local` job. No testnet account or real keys are involved. ```bash theme={null} docker run -d --rm --name writz-local-stellar -p 8000:8000 \ stellar/quickstart:latest --local --enable rpc (cd contracts && cargo build --release --target wasm32v1-none --locked) (cd privacy/contracts && cargo build --release --target wasm32v1-none --locked) (cd privacy/circuits && npm ci && npm run compile && npm run setup:dev) (cd scripts/deploy && bun install) node scripts/deploy/e2e_local.mjs ``` `npm run setup:dev` regenerates the development keys in `privacy/circuits/keys/`; restore the committed ones afterwards with `git checkout privacy/circuits/keys`. *** ## Run the Bitcoin P2WSH End-to-End This script tests the Bitcoin locking and release flow on Bitcoin Signet. No funds required for a dry run. ```bash theme={null} cd bitcoin-script npm run build # Dry run - builds and inspects the P2WSH transaction without broadcasting node scripts/e2e_testnet.mjs --dry-run # Live broadcast (requires Signet BTC - get from a Signet faucet) node scripts/e2e_testnet.mjs ``` The live broadcast will: 1. Generate a unique P2WSH address 2. Send Signet BTC to the address 3. Build the Path A co-signed release transaction 4. Sign with both user and protocol keys (PSBT round-trip) 5. Broadcast to Bitcoin Signet Reference transactions (already executed on Bitcoin Signet): * Funding: [`61deea44`](https://blockstream.info/signet/tx/61deea4439ecd6c325c5b23ecf4b27694ce3cb0474adbbcc6221968ecbd583a4) * Release: [`11932100`](https://blockstream.info/signet/tx/119321009b2f92dac8f25f6bcddb2ed6a3ae778e8748ec52910cce90742e4098) *** ## Deploy Your Own Contracts `scripts/deploy/deploy_stack.mjs` deploys and wires up all four contracts in the order they depend on each other. Without `--execute` it only validates its inputs and prints the plan. Set `STELLAR_NETWORK=local` to rehearse against the local network above. The inputs are listed in `scripts/deploy/.env.example`. ```bash theme={null} (cd contracts && cargo build --release --target wasm32v1-none --locked) (cd privacy/contracts && cargo build --release --target wasm32v1-none --locked) cd scripts/deploy node deploy_stack.mjs # validate and print the plan node deploy_stack.mjs --v1-only # bitcoin-spv and writz-lend only node deploy_stack.mjs --execute # deploy ``` Do not deploy to the shared testnet or to mainnet without the project owner's sign-off. *** ## Repository Layout for Developers ``` contracts/ contracts/ bitcoin-spv/src/ lib.rs - public contract interface header.rs - Bitcoin block header parsing + PoW verification merkle.rs - Merkle proof verification crypto.rs - SHA256d implementation in Soroban Wasm types.rs - Config, Checkpoint (SpvVerificationResult now lives in the shared `spv-types` crate) writz-lend/src/ lib.rs - non-ZK lending skeleton rates.rs - kinked interest rate model privacy/contracts/ zk-verifier/src/ lib.rs - verify_deposit(), verify_borrow_repay(), set_verification_key() commitment-tree/src/ lib.rs - deposit(), borrow(), repay(), liquidate() oracle.rs - SEP-40 oracle interface privacy/circuits/ src/ deposit.circom - Deposit ZK circuit borrow_repay.circom - Borrow/Repay ZK circuit liquidation.circom - Liquidation ZK circuit merkle.circom - Shared Poseidon Merkle components keys/ deposit.vkey.json - Deposit verification key borrow_repay.vkey.json - Borrow/Repay verification key liquidation.vkey.json - Liquidation verification key relayer/src/ index.ts - Express API: GET /spv-proof/:txid spv.ts - SPV proof assembly (Esplora + Merkle computation) bitcoin.ts - Bitcoin types and parsing bitcoin-script/src/ script.ts - P2WSH redeem script builder address.ts - Deposit address derivation spend.ts - Path A/B PSBT signing keys.ts - Key management utilities ``` *** **Next:** [Bitcoin SPV SDK →](/developers/spv-sdk) # What to build on Writz Source: https://docs.writz.xyz/developers/request-for-startups Startup ideas that use the Writz primitives live on Stellar testnet today, scoped for a weekend and for a business after it, plus the showcase of projects built on Writz. Writz lets Stellar know what happened on Bitcoin. A Soroban contract can check that a Bitcoin transaction is buried in the Bitcoin chain, and a borrower can lock native BTC on Bitcoin and take USDC on Stellar against it, with no bridge, no custodian and no wrapped token. This page lists products we would like to see built on top of that. Each idea is meant to work twice: as a demo a small team can ship in a weekend hackathon, and as a business someone would pay for once it is real. Every idea names the Writz piece it uses, and only pieces that are live on Stellar testnet today. Anything that is not live yet is in [Not available yet](#not-available-yet), so you do not plan a weekend around it. > [!TIP] > > Building at the Pura Vida Ideathon (23 to 26 October 2026)? Any idea on this page, or your own, is eligible for the Writz integration bounties. The event page has the rules and the deadline. Not a Soroban developer? Start with [No Soroban needed: build on Writz yield](#no-soroban-needed-build-on-writz-yield). Those ideas lend USDC to Writz Lend, let it earn the interest Bitcoin borrowers pay and take it out, from a web page or a bot, with no contract of your own. *** ## What is live on testnet | Primitive | What it gives you | How to reach it | | - | - | - | | **Bitcoin payment verification** (`bitcoin-spv`) | An on-chain Bitcoin header light client. `verify_transaction` checks that a transaction is in a block on the best Bitcoin chain with at least N confirmations and returns its `txid`, `block_hash`, `block_height` and `confirmations`. Read-only, permissionless, callable from any Soroban contract or by simulation | Contract ID in [`contracts/deployments/testnet.md`](https://github.com/WritzProtocol/writz/blob/main/contracts/deployments/testnet.md); interface in the [Contract Reference](/developers/contract-reference) | | **SPV proof API** | `GET https://api.testnet.writz.xyz/spv-proof/:txid?confirmations=N` returns the raw transaction, its Merkle proof and `sorobanArgs` ready to pass to `verify_transaction` | [api.testnet.writz.xyz](https://api.testnet.writz.xyz) lists every endpoint | | **BTC-collateralized USDC credit** (Writz Lend, `writz-lend`) | A borrower locks BTC in a P2WSH script on Bitcoin, registers it with `deposit`, borrows USDC up to 66.7% of its value (`borrow`), repays (`repay`) and takes the BTC back. Lenders `supply_usdc` and `withdraw_supply`. Read functions: `get_position`, `get_health_ratio_bp`, `get_borrow_rate_bp`, `get_supply_rate_bp`, `get_protocol_state` | [Writz Lend](/products/writz-lend); current contract ID in the deployment record above | | **Bitcoin lock script** (`@writz/bitcoin-script`) | TypeScript: builds the deposit redeem script and address, and the release and emergency (timelock) spends | [`bitcoin-script/`](https://github.com/WritzProtocol/writz/tree/main/bitcoin-script) in the repo (not published to npm; install from the repo) | | **Bitcoin transaction parsing** (`spv-types`) | Rust, `no_std`: `btc_parser::find_p2wsh_output` returns the satoshi value of the first output whose scriptPubKey equals the one you expect (any output type, despite the name); `key_proof` checks a BIP-137 signature from a Bitcoin key | [`contracts/contracts/spv-types/`](https://github.com/WritzProtocol/writz/tree/main/contracts/contracts/spv-types) (depend on it by git path) | | **USDC yield** (Writz Lend, lender side) | Lenders `supply_usdc` to the Writz Lend pool and `withdraw_supply` with interest. `get_supply_balance` returns a lender's balance including interest, `get_supply_rate_bp` the current annual rate. The interest comes from borrowers, so the rate is zero while nothing is borrowed | [Writz Lend](/products/writz-lend); current contract ID in the deployment record above | Things to know before you start: * **Try the app first.** The [Guides](/guides/start-here) walk through wallet setup, test funds and a full loan in the testnet app, click by click, so you see the flow you are building on. * **The Bitcoin side is Signet.** On testnet, `bitcoin-spv` follows Bitcoin Signet, and the Writz relayer keeps it in sync with the Signet tip. Get Signet coins from any public Signet faucet. A transaction that is not on Signet will not verify. * **Confirmations take time.** Six Signet confirmations take about an hour, sometimes longer. Make your Bitcoin transactions on day one, not an hour before the deadline. For a demo you can ask for fewer confirmations; say so in your pitch. * **`verify_transaction` does not remember.** It proves a transaction is in the chain; it does not mark it as used. If your contract pays out against a Bitcoin payment, store the `txid` and refuse it the second time. It also does not read outputs: parse `raw_tx` to check the amount and the recipient (use `spv-types` from Rust). * **A Writz Lend deposit is a real Bitcoin transaction.** It needs Signet BTC sent to the address `@writz/bitcoin-script` derives, the confirmations the deployment requires (1 on testnet), the proof from the SPV proof API, and a BIP-137 signature from the depositor's Bitcoin key over the message the contract rebuilds (reference implementation: `signDeposit` in [`scripts/deploy/e2e_local.mjs`](https://github.com/WritzProtocol/writz/blob/main/scripts/deploy/e2e_local.mjs)). The minimum deposit is 5,000 sats on testnet and 100,000 sats (0.001 BTC) on mainnet. * **Testnet USDC.** Writz Lend uses Circle's testnet USDC (issuer `GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5`). Add the trustline in your wallet and swap testnet XLM for it, or use Circle's faucet. * **Testnet only.** Nothing here is audited, and Writz lending is not on mainnet (target: Q2 to Q3 2027). Do not put real funds through it. *** ## No Soroban needed: build on Writz yield These ideas need a designer and a front-end developer more than a smart contract developer. The integration is the same in all of them: the user lends USDC to the Writz Lend pool, it earns the interest Bitcoin borrowers pay, and the user takes it out when they want. Your product is everything around that: who saves, why, how often, and what they see. | What the user does | Contract call | What the user signs | | - | - | - | | Save | `supply_usdc(supplier, amount)` | One Stellar transaction, built by your page, signed in the user's own wallet | | Take money out | `withdraw_supply(supplier, amount)` | One Stellar transaction, the same way | | See the balance | `get_supply_balance(lender)`, principal plus interest | Nothing: a simulated read | | See the rate | `get_supply_rate_bp()`, annual basis points | Nothing: a simulated read | The yield is not a subsidy. It is the interest that people who lock BTC and borrow USDC pay to the pool, minus the 15% protocol fee; see [Writz Lend, For USDC Lenders](/products/writz-lend#for-usdc-lenders-earn). Before you design around it: * **Test money, not real money.** Writz Lend holds Circle's testnet USDC (issuer `GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5`), which has no value. The user's account needs a trustline to it first; swap testnet XLM for it on the DEX or use Circle's faucet. * **No promised rate.** The rate is variable and comes only from borrowers. While nothing is borrowed it is zero, and with very little borrowed `get_supply_rate_bp` still reads `0` (it counts whole basis points: `500` is 5% a year). On 2026-10-25 it read `0` with 2 USDC borrowed from a 1,000 USDC pool. Read it live, label it as variable, and design the screen for the zero case. Do not put a fixed APY in your pitch or your UI. * **Withdrawals depend on free liquidity.** A lender can always withdraw up to their balance, as long as that USDC is not lent out at that moment. When borrowers hold most of the pool, a large withdrawal fails with `InsufficientLiquidity` (#11) until they repay or new lenders supply. Show that state instead of a generic error. * **The user keeps the keys.** Your page builds the transaction; the user's wallet signs it. Let users sign in their own wallet ([Stellar Wallets Kit](https://stellarwalletskit.dev) covers Freighter, xBull, Lobstr and others). If your app collects users' USDC into an account you control, you have become a custodian, and you should say so in your pitch. * **One balance per Stellar account.** Writz Lend tracks one supply balance per account. Goals, sub-accounts and "money for the trip" versus "money for school" live in your app's own database, not on chain. * **Use your own RPC budget wisely.** Reads are simulations against Soroban RPC, not relayer calls. A bot or a backend that serves many users should cache the rate and avoid polling every user's balance every few seconds. ### Integrate yield in 15 minutes From the command line, with `LEND` set to the current `writz-lend` ID from [`contracts/deployments/testnet.md`](https://github.com/WritzProtocol/writz/blob/main/contracts/deployments/testnet.md) and an account that holds Circle testnet USDC (amounts in stroops, `100000000` is 10 USDC): ```bash theme={null} stellar contract invoke --id $LEND --network testnet --source-account me --send=no -- get_supply_rate_bp stellar contract invoke --id $LEND --network testnet --source-account me -- supply_usdc --supplier $(stellar keys address me) --amount 100000000 stellar contract invoke --id $LEND --network testnet --source-account me --send=no -- get_supply_balance --lender $(stellar keys address me) stellar contract invoke --id $LEND --network testnet --source-account me -- withdraw_supply --supplier $(stellar keys address me) --amount 100000000 ``` From a web page, with `@stellar/stellar-sdk` 16.x. Reads are simulated and need no signature; a save or a withdrawal is prepared by your page, signed in the user's wallet and submitted by your page: ```ts theme={null} import { Address, BASE_FEE, Contract, Networks, TransactionBuilder, nativeToScVal, rpc, scValToNative } from "@stellar/stellar-sdk"; const LEND = "CABF7VK7F25ZGDLV6EWSAU3NYQYKDQNDSJIISDMDZAUASMSXUADSHZGG"; // writz-lend, contracts/deployments/testnet.md const server = new rpc.Server("https://soroban-testnet.stellar.org"); const lend = new Contract(LEND); async function build(address: string, method: string, ...args: any[]) { const account = await server.getAccount(address); return new TransactionBuilder(account, { fee: BASE_FEE, networkPassphrase: Networks.TESTNET }) .addOperation(lend.call(method, ...args)) .setTimeout(300) .build(); } async function read(address: string, method: string, ...args: any[]) { const sim = await server.simulateTransaction(await build(address, method, ...args)); if (rpc.Api.isSimulationError(sim)) throw new Error(sim.error); return scValToNative(sim.result!.retval); } export const rateBp = (address: string) => read(address, "get_supply_rate_bp").then(Number); // 500 is 5% a year; 0 while nothing is borrowed export const balanceStroops = (address: string) => read(address, "get_supply_balance", new Address(address).toScVal()) as Promise; // 7 decimals, interest included async function send(address: string, method: "supply_usdc" | "withdraw_supply", usdc: number) { const amount = nativeToScVal(BigInt(Math.round(usdc * 1e7)), { type: "i128" }); const tx = await server.prepareTransaction(await build(address, method, new Address(address).toScVal(), amount)); const signedXdr = await signWithWallet(tx.toXDR()); // your wallet integration const sent = await server.sendTransaction(TransactionBuilder.fromXDR(signedXdr, Networks.TESTNET)); if (sent.status === "ERROR" || sent.status === "TRY_AGAIN_LATER") throw new Error(sent.status); return server.pollTransaction(sent.hash, { attempts: 30 }); } export const save = (address: string, usdc: number) => send(address, "supply_usdc", usdc); export const takeOut = (address: string, usdc: number) => send(address, "withdraw_supply", usdc); ``` `prepareTransaction` simulates first, so a call that would fail throws before the wallet opens: `Error(Contract, #13)` on `withdraw_supply` means more than the balance, `#11` more than the free liquidity, `#17` that the pool is paused. An account without a trustline to the test USDC fails inside the token contract on `supply_usdc`. To withdraw everything, pass the exact balance from `get_supply_balance`. Prepare a fresh transaction if the user waits long before signing: a built transaction carries the account's sequence number and goes stale. `stellar contract bindings typescript` generates a typed client instead of these helpers. With `@writz/sdk` (`npm install @writz/sdk`, see [Build on Writz](/developers/integrate#packages)) the same calls are `lend.getSupplyRateBp()`, `lend.getSupplyBalance(address)`, `lend.buildSupplyUsdc(address, stroops)` and `lend.buildWithdrawSupply(address, stroops)`, which return the unsigned transaction for the wallet. Checked on 2026-10-25 against Stellar testnet and the live `writz-lend` contract, with the TypeScript above: `get_supply_rate_bp` returned `0` and `get_supply_balance` the supplier's 1,000 USDC; `supply_usdc` and `withdraw_supply` of 10 USDC were prepared by simulation for an account that holds the test USDC, a `withdraw_supply` above its balance failed with `#13`, and `supply_usdc` from an account without a trustline failed inside the token contract. Nothing was submitted, so these checks left no deposits behind. ### A. Dollar savings with goals **Problem.** Students and young workers in Costa Rica and across Latin America save in a currency that loses value, or not at all, because a dollar account takes paperwork and a savings goal has nowhere visible to live. **Who pays.** Free for savers; the business is a small share of the yield, or a cooperative, bank or university that offers it to its members under its own name. **Writz surface.** Writz Lend, lender side. **48h scope.** A digital alcancía, mobile first: create a goal (name, target, date), connect a wallet, save with `supply_usdc` (the saver signs one transaction), and a progress bar from `get_supply_balance` against the target. Show the live rate from `get_supply_rate_bp`, marked as variable, with a clear screen for when it is zero, and a withdraw button. Goals live in your own database; the pool sees one balance per account. **Stretch.** Round-ups and saving streaks, a goal shared with family, sign-in by email with an embedded wallet, cash in and out in colones through a SEP-24 anchor. ### B. A remittance that saves a slice on arrival **Problem.** Families that receive remittances spend all of it on arrival. Saving a fixed share is easy to plan and hard to do by hand every time. **Who pays.** Remittance apps and wallets, as a feature that keeps money and users on their platform; a share of the yield on what is saved. **Writz surface.** Writz Lend, lender side. Remittances on Stellar arrive in Circle USDC, the asset the lending pool holds, so the saved slice goes straight in without a swap. **48h scope.** A receiver app that watches the account for incoming USDC payments (Horizon's payments stream), and on each one proposes "save 10%", built as a `supply_usdc` transaction the receiver signs. A history screen of received versus saved, and the saved balance with interest from `get_supply_balance`. Show the live rate from `get_supply_rate_bp`, and what the screen says when it is zero. **Stretch.** A rule per sender, a SEP-24 anchor for cash out, and [Bitcoin-backed remittances](#3-borrow-do-not-sell-bitcoin-backed-remittances) on the sending side. ### C. A treasury for small businesses **Problem.** A hostel, a surf school or a freelancer paid in USDC keeps money idle between getting paid and paying suppliers. **Who pays.** Small businesses, a monthly plan; accountants and point-of-sale platforms that offer it to their merchants. **Writz surface.** Writz Lend, lender side. **48h scope.** A dashboard with one rule: keep X USDC in the wallet for expenses, lend the rest to the pool. A button that builds the `supply_usdc` or `withdraw_supply` to get back to the rule, signed by the owner. Interest earned since the first deposit, computed as `get_supply_balance` minus net deposits (which your app records). Handle the case where the pool's free liquidity is lower than the withdrawal the rule asks for. **Stretch.** A business account that needs two signatures (Stellar multisig), withdrawals timed for scheduled supplier payments, an export for the accountant. ### D. An endowment that spends only the yield **Problem.** NGOs, school associations and community projects receive donations and want them to last. A fund that pays out only what its money earns keeps the principal intact. **Who pays.** NGOs and foundations, a platform fee; donors, per campaign. **Writz surface.** Writz Lend, lender side. **48h scope.** Donors send USDC to the fund's own Stellar account (a plain payment; if donors supplied to the pool themselves, the balance would be theirs, not the fund's). The fund lends the donations to Writz Lend with `supply_usdc`. A public page shows the principal (the sum of donations, recorded by your app), the current value from `get_supply_balance`, and "available to spend": value minus principal. The treasurer withdraws exactly that amount with `withdraw_supply` and pays a beneficiary, with every transaction linked. Be honest on the page that "available to spend" stays at zero while nothing is borrowed. **Stretch.** A Soroban contract that is itself the supplier and enforces on chain that the principal never leaves, donor receipts, members voting on where the yield goes. ### E. A savings bot in the chat app people already use **Problem.** Many people in Latin America live in WhatsApp and Telegram, not in wallet apps. **Who pays.** Cooperatives, communities and fintechs that want a chat channel to their members; a share of the yield. **Writz surface.** Writz Lend, lender side. **48h scope.** A Telegram bot (its Bot API is free and quick to start with; WhatsApp's business API needs approval, so leave it for later) with `/rate`, `/balance` and `/save 10`. The bot reads `get_supply_rate_bp` and `get_supply_balance` by simulation and, for a deposit, replies with a link to a small page where the user signs the `supply_usdc` in their own wallet. The bot never holds a key. A weekly message with what the user saved. Cache the rate: one bot serving many users should not simulate on every message. **Stretch.** Group savings challenges, reminders on payday, and the [savings circle](#8-savings-circles-that-earn-while-they-wait) from idea 8 run inside a group chat. | Idea | Writz surface | Stellar fit | Weekend | Business | | - | - | - | - | - | | [A. Dollar savings with goals](#a-dollar-savings-with-goals) | Writz Lend, lender side | Medium | High | Medium | | [B. Remittance that saves](#b-a-remittance-that-saves-a-slice-on-arrival) | Writz Lend, lender side | High | High | High | | [C. Business treasury](#c-a-treasury-for-small-businesses) | Writz Lend, lender side | High | High | Medium | | [D. Endowment](#d-an-endowment-that-spends-only-the-yield) | Writz Lend, lender side | Medium | High | Medium | | [E. Savings bot](#e-a-savings-bot-in-the-chat-app-people-already-use) | Writz Lend, lender side | Medium | High | Medium | Scores use the same questions as the table below. Stellar fit is Medium where the same savings app could run on another chain's lending market; it rises when you add Stellar-specific pieces such as a SEP-24 anchor for cash in and out. *** ## How the ideas are scored | Score | Question | | - | - | | **Stellar fit** | Is Stellar essential: USDC settlement, anchors, Soroban contracts, low fees? Would the product fall apart on another chain or off-chain? | | **Weekend** | Can two to four people show it working end to end, on testnet, in about 48 hours? | | **Business** | Is there a clear customer who already has this problem and would pay to have it solved? | High means it clearly passes; Medium means it passes with a caveat named in the idea. | # | Idea | Writz primitive | Stellar fit | Weekend | Business | | - | - | - | - | - | - | | 1 | [P2P Bitcoin-for-USDC escrow](#1-p2p-bitcoin-for-usdc-escrow) | Payment verification | High | High | High | | 2 | [Pay with Bitcoin, settle the books on Stellar](#2-pay-with-bitcoin-settle-the-books-on-stellar) | Payment verification | High | High | Medium | | 3 | [Borrow, do not sell: Bitcoin-backed remittances](#3-borrow-do-not-sell-bitcoin-backed-remittances) | USDC credit | High | Medium | High | | 4 | [Payroll from a Bitcoin treasury](#4-payroll-from-a-bitcoin-treasury) | USDC credit | High | Medium | High | | 5 | [Position guardian](#5-position-guardian) | USDC credit (read) | High | High | Medium | | 6 | [A Bitcoin-backed credit line for AI agents](#6-a-bitcoin-backed-credit-line-for-ai-agents) | USDC credit | High | Medium | Medium | | 7 | [Bitcoin payment checks as a tool for agents](#7-bitcoin-payment-checks-as-a-tool-for-agents) | Payment verification | Medium | High | Medium | | 8 | [Savings circles that earn while they wait](#8-savings-circles-that-earn-while-they-wait) | USDC lending | High | High | Medium | *** ## The ideas ### 1. P2P Bitcoin-for-USDC escrow **Problem.** Across Latin America, people trade Bitcoin for dollars peer to peer in chat groups and P2P marketplaces. One side always has to send first and trust the other, or both trust a platform that holds the funds. Scams and frozen accounts are the cost. **Who pays.** Traders and OTC desks, a small fee per completed trade; P2P marketplaces that want settlement without holding customer funds. **Writz primitive.** Payment verification. The USDC seller locks USDC in a Soroban escrow; the buyer pays BTC on Bitcoin; the escrow calls `verify_transaction`, parses the outputs, and releases the USDC only when the BTC has reached the seller's address with enough confirmations. **48h scope.** A Soroban escrow contract (create offer, lock USDC, claim with an SPV proof, refund after a deadline) with a cross-contract call to `bitcoin-spv`, an output check with `spv-types`, and a used-`txid` set. A small web page that takes a Signet `txid`, fetches the proof from the SPV proof API and submits the claim. One complete trade on testnet, with transaction links. **Stretch.** The reverse direction (BTC seller is paid USDC), partial fills, reputation per Stellar address, a SEP-24 anchor so the USDC seller can be paid out in local currency. ### 2. Pay with Bitcoin, settle the books on Stellar **Problem.** Tourist businesses in Costa Rica (hostels, surf schools, tour operators) are asked to accept Bitcoin, but checking that a payment really arrived, issuing a receipt and reconciling it with dollar accounting is manual and error-prone. **Who pays.** Merchants, per invoice or a monthly plan; point-of-sale and booking platforms that want Bitcoin acceptance without building it. **Writz primitive.** Payment verification. Each invoice is a record on Stellar; it is marked paid when `verify_transaction` proves the matching Bitcoin payment, and a receipt is issued on chain that any accountant or auditor can check. **48h scope.** An invoice page (amount, merchant Bitcoin address, expiry), a Soroban contract that marks an invoice paid against an SPV proof and rejects a reused `txid`, and a merchant dashboard listing paid invoices with their Bitcoin and Stellar transaction links. Demo one Signet payment to one invoice. **Stretch.** Fiat value fixed at invoice time from the Reflector oracle, payout of the dollar amount in USDC through an anchor, a QR flow for a phone at the counter. ### 3. Borrow, do not sell: Bitcoin-backed remittances **Problem.** Many people who hold Bitcoin in Central America also send money home every month. Selling Bitcoin to send dollars means giving up the Bitcoin, and in some places a taxable event. Borrowing against it keeps the Bitcoin and sends dollars today. **Who pays.** The sender, a fee on each remittance on top of the loan interest; wallets and remittance apps that add "send without selling" as a feature. **Writz primitive.** BTC-collateralized USDC credit. The sender borrows USDC on Writz Lend against BTC that never leaves Bitcoin, and the USDC is sent on Stellar to the recipient or to an anchor that pays out locally. **48h scope.** A mobile-first web app: show an existing Writz Lend position and how much can be borrowed safely, borrow, send the USDC to a recipient address, and show the repayment schedule. Use the [Stellar test anchor](https://testanchor.stellar.org) (SEP-24) to show a local cash-out step. Create the Bitcoin deposit on day one; it needs six Signet confirmations. **Stretch.** Recipient onboarding without a wallet (claimable balances or a passkey wallet), reminders before a position gets close to liquidation, a real anchor partner per corridor. ### 4. Payroll from a Bitcoin treasury **Problem.** Startups and small companies that hold part of their treasury in Bitcoin still pay salaries and suppliers in dollars. Every payday they sell Bitcoin, often at a bad moment. **Who pays.** Companies, a monthly subscription per seat or per payroll run. **Writz primitive.** BTC-collateralized USDC credit. On payday the company borrows USDC against its locked BTC and pays everyone in one Stellar transaction; it repays from revenue as it comes in. **48h scope.** A dashboard: treasury position (collateral, debt, health from `get_health_ratio_bp`, current rate from `get_borrow_rate_bp`), a payee list, and a "run payroll" action that borrows the total and pays every payee in one transaction. A guard that refuses a run that would push the position's health below a threshold the company sets. **Stretch.** Scheduled runs, a multisig approval before each run, payslips, payouts in local currency through anchors. ### 5. Position guardian **Problem.** A borrower's position can be liquidated if the price of Bitcoin falls. Most borrowers do not watch prices around the clock, and finding out after the fact is the worst way to learn. **Who pays.** Borrowers, a premium tier with more alerts and channels; wallets and lending front ends that want to offer the alerts to their users. **Writz primitive.** BTC-collateralized USDC credit, read side. `get_position` and `get_health_ratio_bp` give the current state of a position; the Reflector price feed the contract reads gives the market. **48h scope.** A Telegram or WhatsApp bot: register a position by its Bitcoin `txid`, poll its health, alert at thresholds the user picks, and show the Bitcoin price at which it would be liquidated. Include a one-tap link to repay (a SEP-7 payment request or a link into a web page that builds the `repay` call for the user's wallet). **Stretch.** Automatic partial repayment from a USDC balance the user pre-authorizes, a weekly position report, support for every position a Stellar address holds. ### 6. A Bitcoin-backed credit line for AI agents **Problem.** AI agents increasingly pay for APIs, data and compute per request in USDC. Someone has to keep their wallets funded, and the person who owns the agent may hold Bitcoin rather than dollars. **Who pays.** Agent builders and teams that run agents, a fee on credit drawn or a monthly plan. **Writz primitive.** BTC-collateralized USDC credit. The owner keeps a Writz Lend position; the agent's Stellar wallet is topped up with borrowed USDC under limits the owner sets, and the agent spends it on paid APIs (for example over x402 on Stellar). **48h scope.** An agent with its own Stellar account and a spending policy (per-request and daily limits), a top-up flow the owner signs that borrows on Writz Lend and funds the agent, and the agent paying a paid API in USDC. A ledger page showing what the agent spent and what the owner owes. **Stretch.** Automatic repayment from income the agent earns, a policy contract on Soroban so the limits are enforced on chain rather than in the agent's code. ### 7. Bitcoin payment checks as a tool for agents **Problem.** An agent or a service that sells to agents and wants to accept Bitcoin needs a way to know a payment is final before it delivers. Calling a block explorer means trusting it. **Who pays.** Developers of agent services, per verification or a monthly plan; marketplaces of agent tools. **Writz primitive.** Payment verification, packaged as a tool: an MCP server (or a small HTTP service) that takes a `txid`, an expected output and a number of confirmations, fetches the proof, and answers from `verify_transaction` on Stellar rather than from an explorer. **48h scope.** An MCP server with a `verify_bitcoin_payment` tool, an agent that sells a piece of content and releases it only after the tool confirms a Signet payment, and a short demo of the full loop. **Stretch.** Recording the verified `txid` in a Soroban contract so the same payment cannot unlock two deliveries, webhooks, a hosted plan. ### 8. Savings circles that earn while they wait **Problem.** Rotating savings groups (tandas, cundinas) are common across Latin America: members pay in every week and one member takes the pot each round. The pot sits idle, and the organizer has to be trusted with it. **Who pays.** Groups, a small share of the yield; cooperatives and community organizations that run many circles. **Writz primitive.** USDC lending, the Writz Lend lender side. Between payouts, the group's USDC is lent to the Writz Lend pool and earns the interest Bitcoin borrowers pay. **48h scope.** A Soroban contract for one circle (members, contribution, rotation order, payout) that is itself the supplier: it calls `supply_usdc` with each contribution and `withdraw_supply` for the pot when a member's turn comes, plus a simple page for members. A payout can fail if the pool's free liquidity is below the pot at that moment, so the contract needs a retry or a partial payout rule. **Stretch.** Penalties for late payments, invitations by phone number, a group whose members hold Bitcoin and contribute by borrowing against it on Writz Lend. *** ## Not available yet These are on the Writz roadmap or in progress. Do not depend on them this weekend; if your idea needs one, build against the live primitives above and describe the rest as your next step. | Not available | Status | | - | - | | Writz lending on mainnet | Testnet only; mainnet target Q2 to Q3 2027 | | Privacy for positions | A separate, opt-in privacy edition is being designed. It is not part of the protocol, and its testnet contracts are not an integration target | | An SDK on npm or crates.io | Not published; use `@writz/bitcoin-script` and `spv-types` from the repository | | A liquidator receiving Bitcoin | Not how liquidation works today: it clears the debt on Stellar and no BTC moves to the liquidator (see [Writz Lend](/products/writz-lend)). Do not design a product that assumes it | | Proof of Bitcoin reserves, private conversion, automatic yield | Planned features listed in Vision, not built | | Submitting Bitcoin headers yourself | On testnet, only the Writz relayer submits Signet headers to `bitcoin-spv` | *** ## Built on Writz Projects that use a Writz primitive on Stellar. Each card links to the project's repository and its proof that the integration works on chain. > [card] [Your project here](#get-listed) > > Ship something that uses Writz on testnet and open a pull request to add it. ### Get listed 1. Use at least one primitive from [What is live on testnet](#what-is-live-on-testnet) in a way your demo depends on, not just a link. 2. Put your code in a public repository, with a README that says which primitive you use and how. 3. Open a pull request to [WritzProtocol/writz](https://github.com/WritzProtocol/writz) that adds one card to this section: project name, one sentence, an icon, and a link to the repository. In the pull request description, list the testnet contract IDs or transaction hashes that show the integration working. 4. We check the transactions on testnet and merge. A listing is not an endorsement or an audit. Questions while you build: ask in the Writz [Discord](https://discord.gg/zDa4xRN9Xe). # Testnet Runbook Source: https://docs.writz.xyz/developers/runbook **Reproduce the full Writz flow from a clean checkout.** This is the operational companion to [Quick Start](/developers/quick-start). Quick Start gets the test suites green; this runbook gets the protocol *running* against Stellar testnet and Bitcoin Signet. *** ## What this covers, and what it cannot The flow splits into two halves with very different reproducibility: | Half | Covered how | Automated? | | - | - | - | | Soroban + ZK: deploy → supply → deposit → insert commitment → borrow → repay → release | `scripts/deploy/e2e_local.mjs` on a local Stellar network, real Groth16 proofs | Yes - scripted end to end, and run in CI | | Bitcoin: fund a P2WSH address → confirmations → real SPV proof → co-signed release | Manual, through the frontend | No - needs Signet coins and two browser wallets | Be aware of what the scripted half does **not** prove: `e2e_local.mjs` builds a **fabricated** Bitcoin transaction paying the depositor's Writz script and mines easy-difficulty headers on top of a local checkpoint. The `bitcoin-spv` contract genuinely verifies that header chain and Merkle inclusion, but no real Bitcoin transaction, and therefore no real deposit, is involved. It also passes an empty `enc_note`, so the sealed recovery-note round trip (#18) is untested there. Treat the scripted run as proof that the **Soroban and ZK layers** work. The Bitcoin custody path needs the manual walkthrough at the end. *** ## 1. Prerequisites Versions below are the ones CI pins; anything older is untested. ```bash theme={null} rustup target add wasm32v1-none cargo install stellar-cli --locked --version 27 # >= 27: needs crypto::bn254 node --version # >= 20 bun --version # >= 1.1 (CI pins 1.3.14) ``` `circom` **2.x is a Rust binary**. Do not `npm install -g circom` - that installs the legacy 1.x package, which cannot compile `pragma circom 2.0.0`: ```bash theme={null} curl -fL -o ~/.local/bin/circom \ https://github.com/iden3/circom/releases/download/v2.2.3/circom-linux-amd64 chmod +x ~/.local/bin/circom # macOS: circom-macos-amd64 circom --version # expect: circom compiler 2.2.3 ``` `snarkjs` needs no global install - it is a dependency of `privacy/circuits/`. *** ## 2. Build the artifacts a clean checkout lacks These paths the scripts depend on are **gitignored**, so a fresh clone does not have them: | Path | In git? | Produced by | | - | - | - | | `privacy/circuits/build/` (r1cs + prover wasm) | No | `npm run compile` | | `privacy/circuits/ptau/` (Powers of Tau) | No | `bash scripts/setup_dev.sh` | | `privacy/circuits/keys/*.zkey` (proving keys) | No | `bash scripts/setup_dev.sh` | | `privacy/circuits/keys/*_vkey.json` (verification keys) | **Yes** | committed; pushed on-chain by `set_vkeys.js` | | `contracts/target/wasm32v1-none/release/*.wasm` | No | `stellar contract build` | | `privacy/contracts/target/wasm32v1-none/release/*.wasm` | No | `stellar contract build` in `privacy/contracts` | ```bash theme={null} # ZK circuits cd privacy/circuits npm install npm run compile # → build/*.r1cs, build/*_js/*.wasm # Soroban contracts → wasm cd ../contracts && stellar contract build # → privacy/contracts/target/wasm32v1-none/release/commitment_tree.wasm ``` *** ## 3. Trusted setup - read before running `setup_dev.sh` `privacy/circuits/scripts/setup_dev.sh` generates the proving keys. It seeds its entropy with `$(date)`: ```bash theme={null} snarkjs powersoftau contribute ... -e="writz dev entropy $(date)" snarkjs zkey contribute ... -e="writz dev $name entropy $(date)" ``` Two consequences that will otherwise cost you an afternoon: 1. **The setup is not reproducible.** Running it produces a *different* proving/verification key pair every time. 2. **It overwrites the committed `keys/*_vkey.json`.** Your working tree will show those four files as modified. Do not commit them unless you also intend to push the new keys on-chain. The shared testnet `zk-verifier` holds the verification keys from the **original** setup, whose `.zkey` files are not in git. Proofs from a regenerated setup submitted to it fail with `InvalidZkProof` - that is a key mismatch, not a bug in your proof. The scripted flow below avoids this by deploying its own `zk-verifier` and registering whatever keys are in `privacy/circuits/keys/`. *** ## 4. Run the scripted ZK flow ```bash theme={null} docker run -d --rm --name writz-local-stellar -p 8000:8000 \ stellar/quickstart:latest --local --enable rpc (cd scripts/deploy && bun install) node scripts/deploy/e2e_local.mjs ``` It refuses to run against anything but the local standalone network. Each run deploys fresh instances of all four contracts through their constructors, then walks deposit → borrow → repay → release and replays the closed attacks: | Attack | Expected error | | - | - | | Deposit claiming an output locked to another Bitcoin key | `VaultOutputNotFound` (#17) | | Deposit whose timelock opens under 1,008 blocks after confirmation | `InvalidTimelock` (#21) | | Deposit into a lender `bitcoin-spv` has not registered | `NotAConsumer` (#27) | | The same txid deposited into the second lender | `TxidAlreadyConsumed` (#26) | | A borrow proof resubmitted by another account | `RecipientMismatch` (#19) | | Borrowing against a released zero-debt leaf | `NullifierAlreadySpent` (#6) | | The admin inserting a root other than the proven one | `InvalidZkProof` (#4) | To rehearse the real deployment against the same local network, run `scripts/deploy/deploy_stack.mjs` with `STELLAR_NETWORK=local` (inputs in `scripts/deploy/.env.example`). *** ## 5. Testnet assumptions The scripted flow is **not** a faithful mainnet rehearsal. What differs: | Assumption | Value on testnet | Why | | - | - | - | | USDC | **Native asset SAC** | Avoids needing Circle USDC faucet access. The production instance uses the real testnet USDC SAC (`CBIELTK6…`). | | BTC price | Stubbed at **\$60,000** (`600_000_000_000` stroops) | No live oracle wired on testnet; the oracle address is accepted but ignored. | | Bitcoin transaction | **Fabricated** raw tx + synthetic header | Removes the \~10-minute Signet confirmation wait from the loop. | | `min_confirmations` | `6`, against six locally mined headers | The headers are mined at a trivial difficulty, so confirmations cost nothing. | | `min_deposit_satoshis` | `10_000` (0.0001 BTC) | Lowered so Signet faucet amounts are usable. **Hardcoded in the constructor** - the deposit circuit binds it into a public signal, so the script's constant must match or deposit fails with `ProtocolParamMismatch`. | | Lending pool | Pre-funded by the script | No external suppliers on a local network. | | Trusted setup | `pot15` dev ceremony, single contributor | Privacy edition only: a real multi-party ceremony gates that edition, not Writz v1 mainnet. Never use these keys in production. | | `enc_note` | Empty | The script exercises the interface, not the encryption round trip. | *** ## 6. The Bitcoin half (manual) This part cannot be scripted - it needs Signet coins and two browser wallets. **You need:** [Xverse](https://www.xverse.app/) on Signet with ≥ 0.0001 sBTC (from a [Signet faucet](https://signetfaucet.com/)), and [Freighter](https://freighter.app/) on Stellar testnet, funded via Friendbot. 1. Start the relayer, or point at the hosted one: ```bash theme={null} cd relayer && cp .env.example .env # set COMMITMENT_TREE_ID, ADMIN_SECRET bun install && bun start # → http://localhost:3000 ``` Hosted alternative: `https://api.testnet.writz.xyz`. 2. Start the frontend: ```bash theme={null} cd frontend/app && cp .env.example .env.local # .env.example already carries the live testnet contract IDs. # Set NEXT_PUBLIC_RELAYER_URL to your relayer (local or hosted). bun install && bun dev # → http://localhost:3000 ``` 3. In the browser: connect Xverse + Freighter, derive the deposit P2WSH address, send sBTC to it, and wait for confirmations. **Budget \~10 minutes per Signet confirmation** - this is the slow step, and the reason the demo script pre-stages deposits. 4. Click **Deposit**. The relayer assembles the SPV bundle, `bitcoin-spv` verifies inclusion on-chain, and the browser generates the deposit proof locally. 5. Borrow, then repay in full. On full repayment the protocol co-signs the release PSBT; countersign in Xverse and broadcast. BTC returns to your wallet. Watch both explorers: [mempool.space/signet](https://mempool.space/signet) and [stellar.expert testnet](https://stellar.expert/explorer/testnet). *** ## 7. Troubleshooting Errors seen while validating this runbook, and what they actually mean: | Symptom | Cause | | - | - | | `Func(MismatchingParameterLen)` on `deposit` | Caller passes the pre-#18 argument list. `deposit`/`borrow`/`repay` all take a trailing `enc_note: Bytes`. | | `Error(Contract, #11)` - `ProtocolParamMismatch` | The proof's `min_deposit_satoshis` public signal ≠ the contract's config (`10_000`). | | `Error(Contract, #6)` - `InvalidZkProof` | Usually a trusted-setup mismatch: your `.zkey` does not correspond to the verifier's on-chain vkey. See § 3. | | `jest.resetModules is not a function` in relayer tests | Ran `bun test` (Bun's runner). The relayer suite is Jest - use `bun run test`. | | `circom` errors on `pragma circom 2.0.0` | circom 1.x from npm. Install the 2.x release binary - see § 1. | | `keys/*_vkey.json` show as modified | `setup_dev.sh` regenerated them. Expected; see § 3. | *** **Next:** [Contribution Guide →](/developers/contribution-guide) · [Contract Reference →](/developers/contract-reference) # Bitcoin SPV SDK Source: https://docs.writz.xyz/developers/spv-sdk Reference for verify_transaction on the Writz bitcoin-spv contract: real signature, return type, errors, the relayer proof route, and what is not available yet. **Verify Bitcoin transactions from any Stellar smart contract.** `bitcoin-spv` is a Bitcoin light client that runs as a Soroban contract. It stores Bitcoin block headers that descend from a fixed checkpoint, checks each header's proof-of-work and difficulty, and follows the most-work chain. Any Soroban contract can then ask it whether a Bitcoin transaction is in a block of that chain, with a given number of confirmations. Writz Lend uses it to verify BTC deposits; you can use it for anything that should react to a Bitcoin transaction. The SDK is the deployed contract, the relayer proof route, and two packages whose first release is pending: [`@writz/sdk`](https://github.com/WritzProtocol/writz/tree/main/packages/sdk) for TypeScript and [`writz-spv-client`](https://github.com/WritzProtocol/writz/tree/main/contracts/contracts/writz-spv-client) for Soroban contracts (see [Packages](#packages)). Until they are published, the snippets below need only `soroban-sdk` or `@stellar/stellar-sdk`. For a step-by-step walkthrough with commands that were run against testnet, start with [Build on Writz](/developers/integrate). *** ## Deployment | | Value | | - | - | | Contract ID (Stellar testnet) | `CBX23YKCCJMAMBMSCNP6YVKEXUIYK2CTKSR4TQ3RVBPAFYZFVCW7W4EO` | | Bitcoin network tracked | Signet | | Checkpoint | Signet block 327,925 (only later blocks can be proven) | | Header submission | Restricted to one designated Writz account on signet, because signet blocks are authorised by a signature the contract does not verify | | Max headers per `submit_headers` call | 16 | | Max reorg depth per call | 20 blocks | Source: [`contracts/contracts/bitcoin-spv/src/lib.rs`](https://github.com/WritzProtocol/writz/blob/main/contracts/contracts/bitcoin-spv/src/lib.rs). Deployment record: [`contracts/deployments/testnet.md`](https://github.com/WritzProtocol/writz/blob/main/contracts/deployments/testnet.md). This contract was redeployed on 2026-10-25, with a new checkpoint, because testnet rent rose sharply; the storage windows of headers and consumed deposits are now deployment parameters (`header_ttl_ledgers`, `consumed_ttl_ledgers`, see the [Contract reference](/developers/contract-reference)). Blocks at or below the new checkpoint can no longer be proven against it. *** ## `verify_transaction` ```rust theme={null} pub fn verify_transaction( env: Env, block_hash: BytesN<32>, merkle_proof: Vec>, tx_index: u32, raw_tx: Bytes, min_confirmations: u32, ) -> Result ``` | Parameter | Meaning | | - | - | | `block_hash` | Hash of the block that contains the transaction, **internal byte order** (the reverse of what explorers show). The header must already be stored and on the most-work chain | | `merkle_proof` | Sibling hashes from the leaf level up to just below the Merkle root, each in internal byte order. Empty for a block with a single transaction | | `tx_index` | 0-based position of the transaction in the block | | `raw_tx` | The transaction **without witness data** (legacy serialization). Its SHA256d is the txid. Exactly 64 bytes is rejected | | `min_confirmations` | Minimum depth of the block below the light client's tip, where the tip itself counts as 1. Must be at least 1 | It is a read: it changes no state, needs no authorisation, and can be simulated for free. ### Return type `SpvVerificationResult`, defined in the shared crate [`spv-types`](https://github.com/WritzProtocol/writz/blob/main/contracts/contracts/spv-types/src/lib.rs): ```rust theme={null} #[contracttype] pub struct SpvVerificationResult { pub txid: BytesN<32>, // SHA256d(raw_tx), internal byte order pub block_hash: BytesN<32>, // internal byte order pub block_height: u32, // Bitcoin height of that block pub confirmations: u32, // the block's actual depth below the tip (tip = 1) } ``` That is the complete result. It says nothing about the transaction's outputs: if you need to know what the transaction paid and to whom, parse `raw_tx` yourself (see [Checking outputs](#checking-outputs)). ### Errors `SPVError` codes are stable. The ones a caller of `verify_transaction` can hit: | Code | Name | Meaning | | - | - | - | | 2 | `InsufficientConfirmations` | The block is shallower than `min_confirmations` | | 3 | `ZeroMinConfirmations` | `min_confirmations` is 0 | | 5 | `MerkleProofInvalid` | The proof does not reproduce the block's Merkle root | | 7 | `EmptyTransaction` | `raw_tx` is empty | | 10 | `NotInitialized` | The contract has no configuration | | 13 | `CheckpointNotSet` | No checkpoint yet | | 15 | `AmbiguousTransactionLength` | `raw_tx` is exactly 64 bytes | | 22 | `HeaderNotFound` | `block_hash` is not stored | | 23 | `NotOnBestChain` | The block is stored but not on the most-work chain | | 24 | `BlockNotAfterCheckpoint` | The block is the checkpoint or older | The full list, including the codes of `submit_headers` and the admin functions, is in [`error.rs`](https://github.com/WritzProtocol/writz/blob/main/contracts/contracts/bitcoin-spv/src/error.rs). ### Other reads | Function | Returns | | - | - | | `get_best_tip()` | `Option`: `hash`, `height`, `chainwork` of the most-work chain | | `get_header(block_hash)` | `Option`: `prev`, `merkle_root`, `height`, `bits`, `time`, `period_start_time`, `chainwork` | | `get_canonical_hash(height)` | `Option>`: the most-work chain's block hash at that height | | `get_checkpoint()` | `Option`: the trust root | | `is_deposit_consumed(txid)` | `bool`: whether a Writz lending contract has already used this deposit | `consume_deposit` and `set_consumer` are for the Writz lending contracts only; registering a consumer is admin-gated. Keep your own record of the txids you accept. *** ## Packages | Package | Registry | For | Status | | - | - | - | - | | [`@writz/sdk`](https://github.com/WritzProtocol/writz/tree/main/packages/sdk) | npm | TypeScript in the browser, Node or Bun: testnet preset, relayer client with typed errors and `Retry-After`, `bitcoin-spv` and `writz-lend` clients | Published: [`npm install @writz/sdk`](https://www.npmjs.com/package/@writz/sdk) | | [`writz-spv-client`](https://github.com/WritzProtocol/writz/tree/main/contracts/contracts/writz-spv-client) | crates.io | Soroban contracts: `BitcoinSpvClient`, the result and error types, the on-chain output parser `find_output_value` | Published: [`cargo add writz-spv-client`](https://crates.io/crates/writz-spv-client) | With `writz-spv-client` the contract below shrinks to the call itself: ```toml theme={null} [dependencies] soroban-sdk = "26" writz-spv-client = "0.1" ``` ```rust theme={null} use writz_spv_client::{find_output_value, BitcoinSpvClient, SPVError}; let proof = match BitcoinSpvClient::new(&env, &spv) .try_verify_transaction(&block_hash, &merkle_proof, &tx_index, &raw_tx, &6) { Ok(Ok(result)) => result, Err(Ok(SPVError::InsufficientConfirmations)) => return Err(Error::TooRecent), _ => return Err(Error::ProofRejected), }; let sats = find_output_value(&raw_tx, &expected_spk).ok_or(Error::NotPaid)?; ``` Its tests check every type, error code and the parser against the `bitcoin-spv` and `spv-types` source. ## Calling it from a Soroban contract ```rust theme={null} use soroban_sdk::{contractclient, contracttype, Bytes, BytesN, Env, Vec}; #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub struct SpvVerificationResult { pub txid: BytesN<32>, pub block_hash: BytesN<32>, pub block_height: u32, pub confirmations: u32, } #[contractclient(name = "BitcoinSpvClient")] pub trait BitcoinSpv { fn verify_transaction( env: Env, block_hash: BytesN<32>, merkle_proof: Vec>, tx_index: u32, raw_tx: Bytes, min_confirmations: u32, ) -> SpvVerificationResult; } // In your contract. `spv` is the bitcoin-spv contract ID, stored at construction. // `try_` returns the callee's error as a value instead of aborting your call. let proof = match BitcoinSpvClient::new(&env, &spv) .try_verify_transaction(&block_hash, &merkle_proof, &tx_index, &raw_tx, &6) { Ok(Ok(result)) => result, _ => return Err(Error::ProofRejected), }; ``` Without `writz-spv-client`, declare the struct locally as above; the call decodes the result by field name, so the names and types must match exactly. A complete, tested contract built this way is [`examples/spv-payment-check`](https://github.com/WritzProtocol/writz/tree/main/examples/spv-payment-check). ### Checking outputs `spv-types` includes the parser Writz Lend uses on chain. Add it as a git dependency pinned to a commit: ```toml theme={null} [dependencies] soroban-sdk = "26" spv-types = { git = "https://github.com/WritzProtocol/writz", rev = "0a1065991ee0498ad2c54367e0ffa69ca4a59a9a" } ``` ```rust theme={null} // Satoshi value of the first output whose scriptPubKey equals `expected_spk`, // or None. Despite the name, it compares raw scriptPubKey bytes, so it works // for P2WPKH, P2WSH, P2TR and any other script type. let sats = spv_types::btc_parser::find_p2wsh_output(&raw_tx, &expected_spk); ``` Pass the scriptPubKey as bytes (for example `0014...` for P2WPKH; mempool.space shows it as `scriptpubkey` for each output). *** ## Getting the proof: the relayer ``` GET https://api.testnet.writz.xyz/spv-proof/:txid?confirmations=6 ``` `txid` is in display byte order (as explorers show it). `confirmations` is optional, 1 to 20, default 6. Response (200), shortened: ```json theme={null} { "txid": "eac0f95c...9507e269", "rawTxNoWitness": "...", "blockHash": "fe324a44...02000000", "txIndex": 842, "merkleProof": ["...", "..."], "headers": ["...", "..."], "blockHeight": 327928, "confirmations": 6, "sorobanArgs": { "block_hash": "fe324a44...02000000", "merkle_proof": ["...", "..."], "tx_index": 842, "raw_tx": "...", "min_confirmations": 6 } } ``` `sorobanArgs` maps one to one onto the parameters of `verify_transaction`, already in internal byte order. `confirmations` and `min_confirmations` echo what you asked for; the block's real depth is what the contract returns. `headers` are the raw 80-byte headers from the block onwards, one per requested confirmation, for callers that want to inspect them; `verify_transaction` does not take them. Error responses: `400 invalid_txid`, `400 invalid_confirmations`, `404 unconfirmed`, `404 tx_not_found` (a txid that is not on signet), `409 insufficient_confirmations` (with `requested` and `available`), `409 light_client_behind` (the block is not yet in `bitcoin-spv`), `429 rate_limited`, `502 upstream_error`, `503 upstream_unavailable`. The 409, 429 and 503 responses carry `retryAfterSeconds` and a `Retry-After` header. ### Building the proof yourself The relayer only reads public Bitcoin data, so you can do the same from any Esplora API (for example `https://mempool.space/signet/api`): 1. `GET /tx/:txid` for `status.block_height`, then `GET /block-height/:height` for the block hash. 2. `GET /tx/:txid/merkle-proof` for `pos` (the `tx_index`) and `merkle` (the siblings). 3. `GET /tx/:txid/raw`, then strip the witness to get the legacy serialization. 4. Reverse the block hash and each sibling to internal byte order. The reference implementation is `buildSPVProof` in [`relayer/src/bitcoin/spv.ts`](https://github.com/WritzProtocol/writz/blob/main/relayer/src/bitcoin/spv.ts). *** ## Use cases * **Bitcoin payment checks.** Release a token, an NFT or access on Stellar once a Bitcoin payment confirms, without a bridge or a custodian. * **Bitcoin-collateralised lending.** What Writz Lend does: prove a deposit to a time-locked script before lending against it. * **Escrow.** Unlock funds on Stellar when the counterparty's Bitcoin payment is buried under N blocks. * **Bitcoin-gated actions.** Votes, mints or roles for anyone who proves a specific Bitcoin payment. ## Security notes for integrators * **Check the outputs.** Inclusion proves the transaction exists, not that it paid you. Parse `raw_tx`. * **Replay protection.** `verify_transaction` will verify the same transaction for anyone, any number of times. Store the txids you accept. * **Choose confirmations deliberately.** 6 is the usual choice and what Writz Lend uses. * **Testnet only.** The deployed light client tracks signet, with header submission restricted to Writz, and none of the contracts are audited. *** ## Not yet available These appeared in earlier versions of this page and do not exist today: * Output extraction in the result (an `outputs` field or address matching). The result has only the four fields above; use the parser in `spv-types`. * A `headers` argument to `verify_transaction`. Headers are submitted separately with `submit_headers`, and the call takes a stored `block_hash`. * A light client tracking Bitcoin mainnet on Stellar, and any per-call fee. Neither exists. *** **Next:** [Build on Writz](/developers/integrate) for a runnable walkthrough, or [Contract Reference](/developers/contract-reference) for every function of every contract. # Channels Source: https://docs.writz.xyz/go-to-market/channels How Writz reaches borrowers, lenders and builders: what each channel is for, the next step it asks of people, and when we stop it. A channel counts only if we can say who it reaches, what we tell them, what they do next and when we would stop. Posting more is not a strategy. We run a small number of channels at a time and add another only when one is working or has been stopped. ## In use now These channels serve the [three fronts](/go-to-market/step-by-step) directly. | Channel | Who it reaches | Next step we ask for | When we stop | | - | - | - | - | | **Direct conversations and guided sessions** | Bitcoin holders and USDC users, through people we know and communities we belong to | Take an interview, or try a full testnet loan with us | When new conversations stop teaching us anything | | **Direct institutional outreach** | Treasuries, funds, custodians, and the public bodies behind sovereign and state reserves | A technical evaluation, then a testnet pilot | When a type of institution cannot use Writz without a custodian or wrapping | | **Developer programs** (SDK, hackathons, bounties, [ideas page](/developers/request-for-startups)) | Stellar builders, starting in Latin America | Build something real on the tools, and keep it running | When projects stop as soon as the prize ends | | **Founder-led writing** (technical posts, the whitepaper, Bitcoin and Stellar forums) | Researchers, builders and technical Bitcoin readers | Read the design, challenge it, try the tools | When it brings no one into the conversations above | ## Next, as the plan advances | Channel | Who it reaches | Next step we ask for | When it starts | | - | - | - | - | | **Stellar community** | USDC holders and teams on Stellar | Supply test USDC, or discuss supplying at launch | With [Step 7](/go-to-market/step-by-step#step-7-build-the-lender-side) | | **Bitcoin self-custody communities** (meetups, podcasts, regional groups) | Long-term holders who keep their own keys | Join a guided session, or the early list | Now for interviews and guided sessions, open invitations after [Step 3](/go-to-market/step-by-step#step-3-make-the-first-loan-easy-to-try) | | **Product partners** | Users of Stellar apps that want Bitcoin-backed credit or yield on USDC | Partner: integrate. Their users: try the feature | When a partner sees value in testing the tools | | **Wallets and hardware signers** | People holding Bitcoin or USDC in those wallets | Open Writz from the wallet they already use | Integration on testnet, public names [after launch](/go-to-market/step-by-step#step-10-grow-through-partners-then-raise-limits) | ## What we are not doing | We are not | Why | | - | - | | Buying reach, or paying for promotion | People who arrive this way are not the ones who need a loan, and trust cannot be rented | | Running every social platform | Attention is limited, and we would rather do a few things well | | Reporting followers as progress | A follower has not changed what they do. We measure the [next step](/go-to-market/measuring-progress) | | Sending people into a flow that is not ready | A person who gets stuck rarely returns | | Announcing partnerships we do not have | Interest or a letter of intent is not a partnership, and we only name partners with their agreement | ## Why one channel does not fit all Each channel plays a different part in the same journey. * **Writing and developer programs** build credibility, and they reach builders first. They rarely reach someone who will borrow next month. * **Direct conversations** are slow, and they teach us the most. * **Institutional outreach** is slow and has few but heavy outcomes. * **Communities and partners** bring the people who would actually use a loan, and they only work once the product and its message are ready. That is why the order matters. Sending people to a product before it is ready spends the one thing a new protocol cannot buy back, which is a first impression. # Customers and message Source: https://docs.writz.xyz/go-to-market/customers-and-message The first borrower and the first lender Writz is going after, the alternatives they use today, and the message that speaks to their real objections. Writz serves holders who borrow, lenders who fund them, institutions and reserves that can act on either side, and projects that integrate the protocol. Each wants something different, so the plan names one starting group for each. > [!NOTE] > > The groups below are working hypotheses. So far we have a small number of recorded interviews, which give us direction, not proof. [Step 1 of the plan](/go-to-market/step-by-step#step-1-reach-holders-and-listen) tests them, and this page changes if the evidence disagrees. ## The first borrower **A Bitcoin holder who keeps their own keys and has recently sold Bitcoin they wanted to keep, to cover an expense.** | | | | - | - | | **The moment that matters** | A bill, a purchase or an opportunity arrives, and the only large asset they have is Bitcoin | | **What they do today** | Sell and try to buy back later, borrow from a custodial lender, or wrap part of their Bitcoin | | **What they care about** | Keeping their coins, not being surprised by a liquidation, and being able to get out alone | | **Why they come first** | The promise of Writz, that the Bitcoin stays in a script tied to their own key, matters most to someone who already acts on it | | **Where we start looking** | Latin America, where our first conversations and developer community are. Who may use Writz at launch is a separate legal question | ## The first lender **A Stellar team with idle USDC in its treasury, or supplied to a Stellar lending pool.** | | | | - | - | | **The moment that matters** | USDC sits idle in a treasury, on the network the team already uses | | **What they do today** | Hold it, or supply it to an existing Stellar lending market | | **What they care about** | Where the interest comes from, how losses are handled, and how fast they can withdraw | | **Why they come first** | They need no Bitcoin wallet and no new network, so they can start with the fewest steps | These teams can also route their users' USDC to Writz Lend through the [developer tools](/developers/request-for-startups#no-soroban-needed-build-on-writz-yield). Individual USDC holders on Stellar are the natural next group on this side. ## Institutions and reserves **Organizations that hold Bitcoin for the long term: treasury companies, funds, custodians, family offices, and the public bodies behind sovereign and state Bitcoin reserves.** | | | | - | - | | **The moment that matters** | The organization needs liquidity or wants its reserve to do more, and selling is not an option | | **What they do today** | Hold without using it, sell, or borrow through a custodial lender | | **What they care about** | Mandate and legal fit, control of keys, reporting, and an audit trail | | **Why they matter** | One agreement can carry more weight than many small loans, and it shows others that the design holds up to serious review | Institutions take longer, and they will need the audit, the legal entity and limits suited to their size. Early agreements are therefore evaluations and pilots, and no real funds are involved until mainnet. ## Projects that integrate Writz **Stellar teams whose product would improve by verifying Bitcoin activity, or by offering Bitcoin-backed credit or yield on USDC.** | | | | - | - | | **The moment that matters** | Their users ask about Bitcoin, or they need yield or credit they do not want to build | | **What they do today** | Skip Bitcoin, or depend on wrapped assets and trusted parties | | **What they care about** | Clear docs, stable interfaces and a clean integration | | **Why they matter** | Every integration brings its own users, and it can start on testnet today | ## Who we are not starting with Saying no is what makes a small team effective. These groups are not rejected forever. | Group | Why not yet | | - | - | | People new to Bitcoin | They have no collateral to lend against and no reason to compare options | | Traders looking for leverage | A conservative, capped launch does not serve them, and they are the most likely to leave | | Privacy-focused users | Writz Lend positions are public on chain. Privacy is a [separate edition, in progress](/how-it-works/privacy-edition) | ## The message One sentence, for anyone who needs to hear it once: > Borrow dollars against native BTC, without a custodian holding it and without wrapping it. Each side then needs an answer to the question they actually ask. ### For the borrower | What they ask | What we say | | - | - | | Why not just sell? | Selling ends your position. A loan keeps it, at the cost of interest and the risk of liquidation. If you would be glad to rebuy later at any price, selling may be the better choice | | What happens if the price falls? | You see your ratio, you can repay at any time, and liquidation has a known threshold. We explain this before the first borrow, not after it | | Who can touch my Bitcoin? | It stays on Bitcoin, in a script that needs your key on every path. [See the script](/how-it-works/bitcoin-side) | | What if Writz disappears? | A timelock lets you recover the Bitcoin alone. [The procedure is public](/how-it-works/manual-emergency-recovery) | | Is it audited? | Not yet, and there is no mainnet until it is. [Audit status](/security/audits) | ### For the lender | What they ask | What we say | | - | - | | Where does the interest come from? | From borrowers. The rate is variable, and it can be zero while nothing is borrowed. We never promise a fixed return | | What backs the loans? | Bitcoin locked in scripts on Bitcoin, verified on Stellar by a light client contract. Today a liquidation moves no Bitcoin, and the borrow cap bounds that exposure. [How liquidation works](/products/writz-lend) | | What can go wrong? | Smart contract failure, a bad price, or a loan that is not repaid. [The security model](/security/security-model) states each one | | Can I withdraw? | You can withdraw what is not lent out. High borrowing can make you wait | ### For institutions and reserves | What they ask | What we say | | - | - | | Does the Bitcoin leave our control? | No. It stays on Bitcoin in a script that requires your key on every path, and no custodian is added | | Can we verify it? | Positions and proofs are public on chain, so an auditor or the public can check them. An institution that needs confidentiality should weigh this | | What is the risk? | Liquidation, smart contract failure and price feed failure, as stated in [the security model](/security/security-model) | | What is the first step? | A technical evaluation and a testnet pilot, with no real funds | ### For projects that integrate | What they ask | What we say | | - | - | | What can we build today? | Bitcoin payment verification and lending flows on testnet. [Start here](/developers/integrate) | | Do we need to know Soroban? | Not for every integration. [See the options](/developers/request-for-startups#no-soroban-needed-build-on-writz-yield) | ## What the message must not do * **Hide liquidation.** It is the first thing a careful holder asks about, so it comes first in our explanations too. * **Promise a return.** We describe how interest works, never how much it will be. * **Claim more than we have.** Testnet is called testnet, a letter of intent is never called a partnership, and a conversation is never called an agreement. * **Make the safe option look bad.** Selling or holding can be the right answer. People trust advice that sometimes says no. # How we measure progress Source: https://docs.writz.xyz/go-to-market/measuring-progress The funnels Writz watches on each side of the market, and the rules behind every figure it reports. No targets and no forecasts. This page describes what we watch, not what we expect. We do not publish targets or forecasts, because a figure given before there is evidence behind it is only a hope. What we can commit to is how we measure, and how we report what we find. ## The borrower journey Each stage is a person doing something they were not doing before. **1. Hears about Writz** Through a conversation, a community or a partner. We note where they came from. **2. Understands it** They can say what Writz does and what happens if the price falls. **3. Tries it** They complete a Bitcoin lock and a borrow, first on testnet. **4. Finishes it** They repay the loan and their Bitcoin is released. This is the moment the promise is proven to them. **5. Comes back** They take another loan, or bring someone with them. ## The lender journey **1. Hears about Writz** Through the Stellar community, or a product they already use. **2. Understands the risk** They can say where interest comes from and what could cause a loss. **3. Supplies USDC** First test USDC, then real USDC after launch. **4. Stays** They keep their supply in the pool through a full cycle of borrowing, and they withdraw when they choose to. ## The builder journey **1. Discovers the tools** Through the docs, an event or another builder. **2. Makes a first call** Their code calls a Writz contract or the proof API. **3. Ships** They have something that real people can use. **4. Keeps it running** It is still in use after the event or bounty has ended. ## The institution journey **1. First conversation** We understand their mandate and what they would need. **2. Technical evaluation** Their team reviews the design and the security model. **3. Written agreement** A signed document with a defined scope and next step. **4. Pilot** A test on testnet, with no real funds. **5. Live use** Limited use on mainnet, within the protocol's limits. ## One constraint at a time At every point we name the single stage where most people drop out, and that stage is where the team's effort goes. If people do not finish a loan, more reach only sends more people to the same problem. This keeps a small team from working on everything at once. ## Rules for every figure we report * **On-chain figures can be checked.** Wallets, transactions and loans come from public contract data, so anyone can verify them. * **Testnet is labelled as testnet.** Activity with test coins shows that the system works. It is not demand for real loans. * **Wallets are not people.** We report wallets and accounts, and we say so. * **Agreements are stated as they are.** We say what was signed and at what stage, and we name an organization only with its consent. * **Interviews are a signal, not a statistic.** We state how many people we spoke with, and we do not turn a few conversations into a percentage of the market. * **Dates come with numbers.** Any figure we publish states when it was measured. * **Interest is described, never promised.** We explain how the rate works and do not forecast returns. ## What would change this plan This plan will change as we learn. A different first customer, a better channel or a lender design that does not work would each rewrite part of this section, and we will update it when that happens. The pages stay public, so that people can hold us to what we said and see why it changed. # Go-to-market strategy Source: https://docs.writz.xyz/go-to-market/overview How Writz plans to reach its first Bitcoin borrowers and USDC lenders: who comes first, in what order, through which channels, and how each step is judged. A lending protocol does not get used because it works. It gets used when a Bitcoin holder trusts it with a real decision and when a lender is willing to fund that decision. This section explains how Writz plans to earn both, one step at a time. > [!NOTE] > > Writz is on testnet. This is a plan, not a record of results, and it contains no targets or forecasts. Steps advance when evidence says so, not on a calendar. The mainnet target stays [Q2 to Q3 2027](/products/writz-lend#mainnet), and it remains a target, not a commitment. ## The strategy in one paragraph Writz goes to market on three fronts. We attract Bitcoin holders who need dollars and do not want to sell, and show them on testnet exactly what a loan feels like, including what happens when the price falls. We work towards agreements with institutions and with sovereign and state Bitcoin reserves, organizations that hold Bitcoin they intend to keep. And we make Writz infrastructure that other projects can integrate, so that each integration brings users of its own. Underneath, we build the lender side and publish what a careful reader needs in order to trust the protocol. We launch with low limits on borrowing and raise them only after the protocol has behaved well in public. ## The three fronts > [card] [Bitcoin holders](/go-to-market/step-by-step#front-1-bitcoin-holders) > > People who want liquidity without selling, wrapping or handing over their coins. > [card] [Institutions and reserves](/go-to-market/step-by-step#front-2-institutions-and-reserves) > > Treasuries, funds, custodians, and sovereign and state reserves that hold Bitcoin for the long term. > [card] [Infrastructure](/go-to-market/step-by-step#front-3-infrastructure) > > Projects that integrate Writz to verify Bitcoin, or to offer Bitcoin-backed credit in their own products. ## The market problem we are solving A holder who needs cash today has three common options: sell and try to buy back later, hand their coins to a lender, or wrap them to use them in DeFi. Each one asks them to give something up, and for an institution or a public reserve, some of those options may not be allowed at all. Writz offers a fourth option, where the Bitcoin stays on Bitcoin in a script tied to the holder's own key. That option has a real cost of its own. A borrower can be liquidated, and a new protocol has not yet earned trust. So the go-to-market job is less about announcing a product and more about helping each audience compare it honestly with what they do now. ## The principles behind the plan > [card] One clear offer per audience > > Each front has its own first customer, its own message and its own next step. > [card] Two sides, from the start > > A loan needs a borrower and a lender. Neither side is treated as something that will follow on its own. > [card] Evidence before the next step > > Every step has a signal to advance and a reason to stop. Claims about users come from real conversations and on-chain activity. > [card] Trust before reach > > We would rather be understood by a few people than be noticed by many. Risks are stated next to the claims they qualify. > [card] Distribution never breaks the promise > > No channel or agreement is worth wrapping the collateral or adding a custodian. Growth that needs either is growth we decline. > [card] No race for capacity > > We limit borrowing, not deposits, so nobody has to outbid anyone in miner fees to take part. > [card] Use before incentives > > This plan does not depend on a token or on points. We want to learn whether people use Writz because it solves their problem. ## How the section is organized > [card] [Customers and message](/go-to-market/customers-and-message) > > Who comes first on each front, what they do today, and what we say to them. > [card] [The plan, step by step](/go-to-market/step-by-step) > > Ten steps from the first conversations to scaling after mainnet. > [card] [Channels](/go-to-market/channels) > > Each channel, who it reaches, what it asks of them, and when we stop it. > [card] [How we measure progress](/go-to-market/measuring-progress) > > The journey for each audience, and the rules behind every figure we report. # The plan, step by step Source: https://docs.writz.xyz/go-to-market/step-by-step How Writz goes to market on three fronts, Bitcoin holders, institutions and reserves, and projects that integrate it, with what each step does and how it is judged. The plan opens on three fronts at the same time, because each one strengthens the others. Holders give the protocol real use, institutions and reserves give it weight, and integrators carry it to users we would not reach alone. Two more stages turn that work into a live market. | Stage | Steps | What it achieves | | - | - | - | | **Front 1: Bitcoin holders** | 1 to 3 | People who hold Bitcoin understand Writz and complete a loan | | **Front 2: Institutions and reserves** | 4 and 5 | The first agreements with organizations that hold or manage Bitcoin | | **Front 3: Infrastructure** | 6 | Other projects build on Writz and bring their own users | | **Foundations** | 7 and 8 | Lenders are ready, and the protocol has earned trust in public | | **Launch and scale** | 9 and 10 | Real loans under limits, then growth through partners | Each step closes with a **signal to advance**, the evidence we need before moving on, and a **reason to stop**, which tells us something needs rethinking. *** ## Front 1: Bitcoin holders ### Step 1: Reach holders and listen **What we do.** We go where long-term holders already are: self-custody communities, meetups, podcasts and regional groups, starting in Latin America. We hold structured interviews and ask about the last time they needed money, what they did, what they feared, and how they turn USDC into money they can spend. **Why.** Holders do not need another product announcement. They need a better answer to a moment they have already lived. In our first interviews, custody was not the first reason people gave for avoiding a loan. Liquidation risk and the habit of selling and rebuying came up more often. **Signal to advance.** We can name who feels the problem most, in their own words, and where to find more people like them. **Reason to stop.** Nobody in the group has a recent, concrete need. Then the group is wrong. ### Step 2: Make the message answer the real objection **What we do.** We rewrite the landing page, the pitch and the first borrow screen around what we heard. Liquidation is explained before a loan, in plain language, with an example of what a price fall does to a position. **Why.** A message that answers the wrong fear wastes every channel behind it. **Signal to advance.** New readers repeat our one sentence correctly after hearing it once, and they can explain what happens if the price falls. **Reason to stop.** People understand the product and still prefer selling. Then the difference we offer is not strong enough for this group. ### Step 3: Make the first loan easy to try **What we do.** We run guided sessions in which a holder takes a full loan on testnet, from locking test BTC to repaying and releasing it, and we record every hesitation. [Sign-in and test Bitcoin](/guides/sign-in-with-email) already work without a browser wallet, and the sessions show what still blocks people. **Why.** A loan touches two networks, and a flow that looks simple to its builders may not be simple to a newcomer. Friction has to go before we invite a wider audience. **Signal to advance.** Someone new completes a loan and a repayment alone, using only [the guides](/guides/start-here). **Reason to stop.** People complete the flow and do not care. Then the problem is value, not usability. *** ## Front 2: Institutions and reserves ### Step 4: Open the conversations **What we do.** We map the organizations that hold or manage Bitcoin: treasury companies, funds, custodians, family offices, and the public bodies behind sovereign and state Bitcoin reserves. We approach them with a short technical brief and ask what they would need in order to use their Bitcoin without selling it or moving it. **Why.** A reserve or treasury is often built never to be sold, which is why it sits idle. Writz fits that position directly: the Bitcoin stays on Bitcoin, no custodian is added, and the position can be verified on chain by anyone. For a public body, that visibility can be a requirement and not a drawback. **Signal to advance.** We understand the requirements of each type of institution: legal, custody, reporting and approval, in their own words. **Reason to stop.** Requirements cannot be met while keeping Writz non-custodial. We keep the design and walk away from that deal. ### Step 5: Close the first agreements **What we do.** We turn the best conversations into agreements with a defined scope: an evaluation of the protocol, a pilot on testnet, and then a limited mainnet use once the launch conditions are met. Institutions can take part on either side, by borrowing against Bitcoin or by supplying USDC. **Why.** One institution that uses Writz properly changes how every later conversation goes. Starting with evaluations and pilots lets both sides learn before real funds are involved, and public bodies in particular move through formal processes that take time. **Signal to advance.** An institution signs a written agreement with a clear next step, and agrees to what can be said about it in public. **Reason to stop.** An agreement would require wrapping the Bitcoin, adding a custodian or giving anyone special control over the protocol. We decline it. *** ## Front 3: Infrastructure ### Step 6: Become what other projects build on **What we do.** We treat Writz as infrastructure as well as an application. The [SDK and verification tools](/developers/integrate) let a Stellar product check what happened on Bitcoin, and Writz Lend can sit behind another product as its source of Bitcoin-backed credit or yield on USDC. We publish [ideas worth building](/developers/request-for-startups), support hackathons and bounties, and work directly with teams that want to integrate. **Why.** Every integration has its own users, so Writz grows without having to reach each person itself. Developers can also adopt the testnet tools today, long before lending is on mainnet, and a protocol that others depend on becomes harder to replace. **Signal to advance.** A team outside Writz ships an integration, and keeps it running after any event or bounty has ended. **Reason to stop.** Projects appear for prizes and stop afterwards. Then we change what we reward, or spend that effort elsewhere. *** ## Foundations ### Step 7: Build the lender side **What we do.** We explain lending to Stellar teams with USDC, invite them to supply test USDC, and show how interest flows from borrowers. In parallel we work out how the pool gets its first liquidity at launch, including from institutions met in Front 2. **Why.** A borrower who arrives at an empty pool does not come back, and a lender with no borrowers earns nothing. Each side waits for the other unless we plan for it. **Signal to advance.** We know who would supply USDC at launch, what they need to see first, and that they have said so directly. **Reason to stop.** Lenders will not accept the risk at any reasonable rate. Then the launch design changes before anything else moves. ### Step 8: Earn trust in public **What we do.** We complete an independent [audit](/security/audits), publish the result, and open the [bug bounty](/security/bug-bounty) for mainnet. We add a second price source. Today a liquidation moves no Bitcoin, so we make the collateral answer for a default and name who carries out liquidations. We move the signing key to a multisig and state exactly what its holders can and cannot do. We turn the [manual timelock procedure](/how-it-works/manual-emergency-recovery) into a guided flow in the app. **Why.** A careful holder, a risk committee and a public auditor all ask the same thing: what happens when something goes wrong? Publishing the answers lets them check for themselves. **Signal to advance.** The audit is finished, its findings are resolved or disclosed, and a reviewer outside the team can verify our claims from public sources. **Reason to stop.** Mainnet does not ship with an open critical finding, at any date. *** ## Launch and scale ### Step 9: Launch small, with limits on borrowing **What we do.** Once counsel defines who may use Writz, we invite the people, teams and institutions from earlier steps to an early list. We launch [Writz Lend on mainnet](/products/writz-lend#mainnet) only after its gates are closed, including the legal entity, the audit and collateral that answers for a default. Liquidity comes first and borrowers follow, under conservative caps. We publish how the first loans go, including the first repayment and release of Bitcoin. **Why.** The first real loans are where trust is earned or lost. Limits reduce what can go wrong, and capping borrowing rather than deposits means nobody has to race for a place. **Signal to advance.** Loans open, get repaid and release Bitcoin without incident, and any liquidation behaves as documented. **Reason to stop.** Any incident affecting funds. Caps stay where they are, or lending pauses within what the contracts allow, until the cause is understood and published. ### Step 10: Grow through partners, then raise limits **What we do.** With a working mainnet to point at, we extend integrations to wallets, hardware signers, products and regional communities that already hold the audience, and users still sign with their own keys. We raise caps gradually, and institutions from Front 2 move from pilots to larger use as the limits allow. **Why.** A small team does not sell one user at a time. Each partner multiplies reach, and each new group needs its own proof. **Signal to advance.** People come back for a second loan, and partners bring users we did not reach ourselves. **Reason to stop.** Growth depends on effort or incentives alone, and stops when they stop. Then we go back to what the first users actually valued. # Borrow USDC against BTC Source: https://docs.writz.xyz/guides/borrow Lock test BTC on Bitcoin signet, register it on Stellar and borrow test USDC, step by step in the Writz app. A loan has two parts. First you **lock BTC** in a Bitcoin address that only you and Writz together can spend from, and register that deposit on Stellar. Then you **borrow USDC** against it, as much or as little as you want up to the limit. **You need:** a [Stellar wallet](/guides/set-up-freighter) with test XLM (or a [Google, GitHub or email sign-in](/guides/sign-in-with-email)), [Xverse on signet](/guides/set-up-xverse) and at least 0.00005 BTC of [test BTC](/guides/get-test-funds#test-btc-on-bitcoin-signet) plus a few hundred sats for the fee. **Time:** a few minutes of clicks, plus the wait for Bitcoin to confirm your deposit. You can close the page while it confirms and come back later. ## Part 1: Lock your BTC Open [testnet.writz.xyz](https://testnet.writz.xyz) and select **Borrow** in the menu, or **Start** on the home page. The app walks you through six steps, shown at the top as **Step 1 of 6** to **Step 6 of 6**. **1. Amount** Under **How much BTC do you want to lock?**, enter the amount. The minimum on testnet is **0.00005 BTC**. The app shows **You can borrow up to**, the price at which the loan would be liquidated if you borrowed the full amount, and the oracle BTC price. Select **Continue**. ![Step 1 of 6, the BTC amount field with the borrow limit, liquidation price and BTC price](https://mintcdn.com/writz-protocol/AtDBkTL5W_JkQblu/images/guides/borrow-1-amount.png?fit=max\&auto=format\&n=AtDBkTL5W_JkQblu\&q=85\&s=77e39bde3896e40788a6145332ef32f6) Step 1. With 0.0001 BTC you can borrow a few USDC at the current test price. **2. Stellar wallet** Select **Connect Stellar wallet** and approve in your wallet. If the account has no XLM, the app shows **Not funded**: select **Fund with Friendbot**, then **Check again**. ![Step 2 of 6 with the Connect Stellar wallet button](https://mintcdn.com/writz-protocol/AtDBkTL5W_JkQblu/images/guides/borrow-2-connect-stellar.png?fit=max\&auto=format\&n=AtDBkTL5W_JkQblu\&q=85\&s=bafef0cd4a387b0a6f106cd66b010d04) Step 2. Connect the wallet that receives your USDC. **3. Bitcoin wallet** Select **Connect Xverse** and approve in Xverse. The app then checks your signet balance. * **Wrong network**: Xverse is not on signet. [Switch it to Signet](/guides/set-up-xverse#2-switch-to-signet) and select **Connect Xverse** again. * **Not enough BTC**: the balance is below your amount. Use one of the faucet links shown, wait for the coins, then select **Check balance again**. ![Step 3 of 6 showing Not enough BTC, two faucet links and Check balance again](https://mintcdn.com/writz-protocol/AtDBkTL5W_JkQblu/images/guides/borrow-3-not-enough-btc.png?fit=max\&auto=format\&n=AtDBkTL5W_JkQblu\&q=85\&s=63477f8436f1867b3bf6c1719c391b86) Step 3 when the balance is too low. The faucet links are right there. **4. Review and send** The app shows everything about the lock before any BTC moves: * **You lock**: the amount, plus the Bitcoin network fee. * **To your lock address**: the Bitcoin address built from your Xverse key and Writz's key. **What is this?** explains it. * **Release**: Writz co-signs the release when your loan is repaid. * **Your way out**: the block height (and rough date) after which your key alone can move the BTC, even if Writz is gone. Read the line **Writz cannot recover your Bitcoin key. Keep its recovery phrase.**, tick **I understand my BTC stays locked until I repay and release it, or until block …**, then drag **Slide to send** all the way to the right. Xverse opens and asks you to send the amount plus the fee: confirm it there. ![Step 4 of 6, Check everything then send, with the lock details and the Slide to send control](https://mintcdn.com/writz-protocol/AtDBkTL5W_JkQblu/images/guides/borrow-4-review.png?fit=max\&auto=format\&n=AtDBkTL5W_JkQblu\&q=85\&s=c273f5030a3e448abcb631170c1c466c) Step 4. Check the amount, the lock address and your way out, then slide to send. Example amounts and address. **5. Wait for Bitcoin** The app counts the confirmations it needs, with an estimate of the time left and a **View on mempool** link to watch your transaction. You can close the page. Your progress is saved in this browser for this Stellar wallet, and the home page shows the deposit when you come back. ![Step 5 of 6, Bitcoin is confirming your deposit, with the confirmation counter](https://mintcdn.com/writz-protocol/AtDBkTL5W_JkQblu/images/guides/borrow-5-waiting.png?fit=max\&auto=format\&n=AtDBkTL5W_JkQblu\&q=85\&s=a8227181305305a976619fb1a200bef5) Step 5. Bitcoin signet blocks arrive about every 10 minutes. **6. Register on Stellar** When the deposit has the confirmations it needs, the app shows **One signature left**. Select **Register on Stellar**, then: 1. **Confirm in Xverse.** It signs a free message that proves the Bitcoin key is yours. No BTC moves and there is no fee. 2. **Confirm in your Stellar wallet.** One transaction, with a fee of about 0.01 XLM. Writz verifies your Bitcoin transaction on Stellar itself, with a Bitcoin light client contract, before it records the deposit. ![Step 6 of 6, Registering your deposit on Stellar, with the Xverse and Stellar signature steps](https://mintcdn.com/writz-protocol/AtDBkTL5W_JkQblu/images/guides/borrow-6-registering.png?fit=max\&auto=format\&n=AtDBkTL5W_JkQblu\&q=85\&s=cd19306ebcf69e4652aca3724b91a83b) Step 6. A free message in Xverse, then one Stellar transaction. When it finishes, the app shows your BTC as locked and **Your loan is ready**. Select **Borrow now** to open the loan. ![BTC locked, Your loan is ready, with Borrow now and Make another deposit buttons](https://mintcdn.com/writz-protocol/AtDBkTL5W_JkQblu/images/guides/borrow-done.png?fit=max\&auto=format\&n=AtDBkTL5W_JkQblu\&q=85\&s=b80375ff7c9c4d0060b3fa9b4d4931f7) The deposit is registered. Example amounts. ## Part 2: Borrow USDC **1. Add USDC, once** If your Stellar account can't hold USDC yet, the loan page shows **Add USDC to your Stellar wallet so it can receive what you borrow.** Select **Add USDC** and confirm in your wallet. Your wallet sets aside 0.5 XLM for it. You only do this once per account. ![Loan page with the Add USDC step above the Borrow form](https://mintcdn.com/writz-protocol/AtDBkTL5W_JkQblu/images/guides/loan-add-usdc.png?fit=max\&auto=format\&n=AtDBkTL5W_JkQblu\&q=85\&s=d0af824d917c7cf7394ca5052c6e9fe0) Only shown if USDC is not added to your account yet. **2. Choose an amount** On the **Borrow more** tab, enter how much USDC to borrow, or use **25%**, **50%** or **Max safe**. The app shows the collateral ratio the loan would have and the BTC price at which it could be liquidated. **3. Borrow** Select **Borrow** and confirm in your wallet. The USDC arrives in your Stellar wallet within seconds. ![Loan page with a 200% collateral ratio marked Healthy, collateral, debt and available to borrow](https://mintcdn.com/writz-protocol/AtDBkTL5W_JkQblu/images/guides/loan-healthy.png?fit=max\&auto=format\&n=AtDBkTL5W_JkQblu\&q=85\&s=e8c388841d62397a89b4504dbac6607b) An open loan. The meter shows the collateral ratio; the tabs borrow more, repay and release. Example amounts. ## Keep the loan healthy The **collateral ratio** is the value of your locked BTC divided by what you owe. The loan page shows it with a meter: | Ratio | Label | What it means | | - | - | - | | 150% and above | **Healthy** | You can borrow more, as long as the ratio stays at 150% or above. | | 130% to 150% | **Below borrow limit** | You can't borrow more. Repay some to get back above 150%. | | 120% to 130% | **Close to liquidation** | Repay now, or the next price drop can make the loan liquidatable. | | Below 120% | **Can be liquidated now** | Anyone running a liquidator can repay your debt and close the loan. | The ratio moves with the BTC price. Borrowing less than the maximum gives you room if the price drops. The page also states the BTC price at which your loan would become liquidatable. If a loan is liquidated, you keep the USDC you borrowed and owe nothing more on it. Writz does not take or sell your BTC: it stays in the lock address until the block height under **Your way out**, after which your key alone can [reclaim it](https://testnet.writz.xyz/help/reclaim). ## Your loans on other devices Loans are recorded on Stellar, so they load on any device where you connect the same Stellar wallet. A deposit that is still confirming is saved only in the browser where you made it, so finish steps 5 and 6 in that same browser. ## What's next [Repay and release your BTC](/guides/repay-and-release). *Last checked against the app on 2026-10-25.* # Connect your wallets Source: https://docs.writz.xyz/guides/connect-your-wallets Connect a Stellar wallet and Xverse to the Writz app, and read what the Wallets panel tells you. The app uses two wallets, one per network: * **Stellar wallet**: receives the USDC you borrow and signs loan actions. Freighter, xBull, Lobstr and other Stellar wallets work, or [sign in with Google, GitHub or email](/guides/sign-in-with-email). * **Bitcoin wallet**: Xverse. It holds the BTC you lock. Only needed to borrow. Writz never sees your keys. Every action opens your wallet and waits for you to confirm. ## Connect **1. Open the Wallets panel** Go to [testnet.writz.xyz](https://testnet.writz.xyz) and select **Connect wallets** at the top right. ![Wallets panel with Connect Stellar wallet and Connect Xverse buttons](https://mintcdn.com/writz-protocol/AtDBkTL5W_JkQblu/images/guides/wallets-panel.png?fit=max\&auto=format\&n=AtDBkTL5W_JkQblu\&q=85\&s=de2eff0ce637e0e6d9dc589126af7769) The Wallets panel. Stellar first, then Bitcoin. **2. Connect your Stellar wallet** Select **Connect Stellar wallet**. A list of Stellar wallets opens: choose Freighter, or the wallet you use. The button reads **Confirm in your wallet** until you approve the connection in the wallet's own window (in Freighter, select **Connect**). **3. Connect Xverse** Select **Connect Xverse** and approve in Xverse. Xverse must be on **Signet**, see [Set up Xverse](/guides/set-up-xverse#2-switch-to-signet). ## What the panel shows ![Wallets panel with a Stellar address, USDC added Yes, a Bitcoin address and its balance](https://mintcdn.com/writz-protocol/AtDBkTL5W_JkQblu/images/guides/wallets-connected.png?fit=max\&auto=format\&n=AtDBkTL5W_JkQblu\&q=85\&s=d9e4116568eed8b5458c68fcdb6a5a37) Both wallets connected. Example addresses and balance. | Line | Meaning | | - | - | | Stellar address | The account that receives USDC and signs. **View on Stellar Expert** opens it in the explorer. | | **USDC added** | **Yes** when your account can hold USDC. **Not yet** means you need to [add USDC](/guides/get-test-funds#test-usdc-on-stellar-testnet) before you can receive it. | | Bitcoin address and **Balance** | The Xverse payment address and the signet BTC Writz sees in it. **View on mempool** opens it in the explorer. | | **Your loans** | How many loans Writz found on Stellar for this Stellar wallet. They load on any device where you connect the same wallet. | | **Testnet faucets** | Links to the signet BTC faucets and Friendbot. See [Get test funds](/guides/get-test-funds). | The panel also warns you when something needs fixing: * **Wrong network**: your Stellar wallet is not on testnet. Switch it to **Testnet**. * **Account not funded**: the account has no XLM yet. Select **Fund with Friendbot**. * **Low XLM**: fewer than 2 XLM left. Fund it again so you can keep paying fees. ## Disconnect Select **Disconnect** under either wallet. If a transaction is still running, the app asks first; the transaction keeps going on chain either way. *Last checked against the app on 2026-10-25.* # Get test funds Source: https://docs.writz.xyz/guides/get-test-funds Free test XLM, test BTC on Bitcoin signet and test USDC on Stellar testnet, and where each one goes. You need up to three test coins. All are free and have no value. | Coin | What it is for | Where to get it | | - | - | - | | XLM (Stellar testnet) | Network fees and the small reserve each Stellar account keeps | Friendbot | | BTC (Bitcoin signet) | The collateral you lock to borrow | A signet faucet | | USDC (Stellar testnet) | To lend, or to pay the interest when you repay | Circle's testnet faucet | ## Test XLM Skip this if you [signed in with Google, GitHub or email](/guides/sign-in-with-email): your account already has XLM. Use any one of these: * **In Freighter:** with Freighter on **Testnet**, select **Fund with Friendbot** on the empty account. See [Set up Freighter](/guides/set-up-freighter#3-fund-the-account-with-test-xlm). * **In the Writz app:** when your connected account has no XLM, the app shows **Account not funded**. Select **Fund with Friendbot**. * **In Stellar Lab:** open [lab.stellar.org/account/fund](https://lab.stellar.org/account/fund), make sure the network at the top is **Testnet**, paste your Stellar address and request funds. Friendbot sends 10,000 test XLM. You need only a few. ## Test BTC on Bitcoin signet The smallest deposit Writz accepts on testnet is **0.00005 BTC** (5,000 sats), plus a Bitcoin network fee of a few hundred sats. One faucet payout of 10,000 sats covers it. **1. Copy your Xverse payment address** Connect Xverse in the app and select **Copy address** under **Bitcoin wallet** in the Wallets panel, or copy the payment address from Xverse itself. See [Set up Xverse](/guides/set-up-xverse#3-find-your-payment-address). **2. Request coins from a faucet** Use one of the faucets the app links to under **Testnet faucets** in the Wallets panel: * [bitcoinsignetfaucet.com](https://bitcoinsignetfaucet.com): paste the address in **Signet address**, enter an **Amount (sats)** from 1,000 to 10,000 (10,000 is best for Writz), and select **Request test sats**. Up to five requests per address in 24 hours. * [signet.dcorral.com](https://signet.dcorral.com): sends 0.0001 BTC (10,000 sats) per request. Paste the address in the **Enter your Signet address** field, complete the human check and select **Get Signet BTC**. **3. Wait for it to arrive** Faucets send in batches. The coins usually show up in Xverse within a few minutes as unconfirmed, and confirm with the next signet block. Writz shows the balance it sees in the Wallets panel. > [!NOTE] > > A faucet may be empty or paused for a while. If one fails, try the other one. Faucets are run by third parties, not by Writz. ## Test USDC on Stellar testnet Writz uses **Circle's testnet USDC** on Stellar, issued by `GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5`. > [!WARNING] > > The Wallets panel also links to **Blend test USDC**. That is a different token and does not work for borrowing, repaying or lending. Use Circle's faucet below. **1. Add USDC to your Stellar account** A Stellar account must opt in to a token before it can receive it (this is called a trustline). The easiest way is in the app: open **Lend**, and if your account can't hold USDC yet, select **Add USDC** and confirm in your wallet. Your wallet sets aside 0.5 XLM for it. Signed in with Google, GitHub or email? USDC is already added. The Wallets panel shows **USDC added: Yes**. **2. Open Circle's faucet** Go to [faucet.circle.com](https://faucet.circle.com). Select **USDC**, open the **Network** menu and choose **Stellar Testnet**. **3. Send to your Stellar address** Paste your Stellar address in **Send to** and select **Send 20 USDC**. ![Circle testnet faucet with USDC selected, network Stellar Testnet and a Send to field](https://mintcdn.com/writz-protocol/AtDBkTL5W_JkQblu/images/guides/faucet-circle.png?fit=max\&auto=format\&n=AtDBkTL5W_JkQblu\&q=85\&s=01521b4e295dd7f3e475a5e1145136f0) Circle's faucet with Stellar Testnet selected. Circle allows one request per asset and network every 2 hours. If the request fails, check that USDC is added to your account first: Stellar rejects a payment to an account without the trustline. You can also get test USDC by [borrowing it](/guides/borrow) against test BTC. *Last checked on 2026-10-25.* # Lend USDC Source: https://docs.writz.xyz/guides/lend Supply test USDC to the Writz Lend pool that Bitcoin borrowers draw from, and withdraw it with interest. Lenders supply the USDC that Bitcoin borrowers borrow. The interest borrowers pay goes to the lenders, so your supplied balance grows while USDC is borrowed. Nothing else sits in between: no vault and no second protocol. **You need:** a Stellar account ([Freighter](/guides/set-up-freighter) or a [Google, GitHub or email sign-in](/guides/sign-in-with-email)) and [test USDC](/guides/get-test-funds#test-usdc-on-stellar-testnet). No Bitcoin wallet. > [!NOTE] > > The rate is variable and can be zero. Lenders earn only while borrowers are borrowing, so with little or nothing borrowed you earn little or nothing. Writz never promises a fixed APY. ## Supply **1. Open Lend** Go to [testnet.writz.xyz](https://testnet.writz.xyz), select **Lend** in the menu and connect your Stellar wallet. The page shows **You supplied**, **Available in pool** and **You can withdraw now**. **2. Add USDC, once** If your account can't hold USDC yet, the page shows **Add USDC to your Stellar wallet.** Select **Add USDC** and confirm in your wallet. Your wallet sets aside 0.5 XLM for it. ![Lend page with the Add USDC button](https://mintcdn.com/writz-protocol/AtDBkTL5W_JkQblu/images/guides/lend-add-usdc.png?fit=max\&auto=format\&n=AtDBkTL5W_JkQblu\&q=85\&s=b4821185df43c89212010c4f4b583902) Only shown if USDC is not added to your account yet. **3. Get test USDC** If **Wallet:** shows **0.00 USDC**, get some from [Circle's faucet](/guides/get-test-funds#test-usdc-on-stellar-testnet). Do not use the **Blend test USDC** link: it is a different token. **4. Supply** On the **Supply** tab, under **Supply USDC**, enter an amount or select **Max**, then select **Supply** and confirm in your wallet. ![Lend page on the Supply tab with the USDC amount field, Max and the Supply button](https://mintcdn.com/writz-protocol/AtDBkTL5W_JkQblu/images/guides/lend-supply.png?fit=max\&auto=format\&n=AtDBkTL5W_JkQblu\&q=85\&s=402de2e3196d9decd974b47e641b3a0c) The Supply tab. Example balances. Your supply appears under **You supplied** once the transaction confirms, usually within seconds. ## Withdraw **1. Open the Withdraw tab** On the Lend page, select the **Withdraw** tab. It shows **Withdraw USDC** and how much you supplied. **2. Withdraw** Enter an amount, up to **You can withdraw now**, or select **Max**. Select **Withdraw** and confirm in your wallet. The USDC, with the interest it earned, returns to your Stellar wallet. ![Lend page on the Withdraw tab](https://mintcdn.com/writz-protocol/AtDBkTL5W_JkQblu/images/guides/lend-withdraw.png?fit=max\&auto=format\&n=AtDBkTL5W_JkQblu\&q=85\&s=84310204bec2ecbd75201863d6536dab) The Withdraw tab. Example balances. ## When you can't withdraw everything You can only withdraw USDC that is not lent out at that moment. If borrowers are using most of the pool, **You can withdraw now** is lower than **You supplied**, and the Withdraw tab says so, for example *You can withdraw 800.00 USDC now. Borrowers are using the rest of the pool.* The rest becomes available as borrowers repay or other lenders supply, and a high share borrowed raises the borrow rate, which encourages exactly that. ## The risk you take Your USDC funds loans backed by BTC at a 150% minimum collateral ratio, which can be liquidated below 120%. The BTC price comes from one oracle today, and the contracts are not audited yet. On testnet none of this costs real money, which is the point of trying it here first. The details are in [Writz Lend, For USDC Lenders](/products/writz-lend#for-usdc-lenders-earn). *Last checked against the app on 2026-10-25.* # Repay and release your BTC Source: https://docs.writz.xyz/guides/repay-and-release Pay back a Writz loan in USDC, then release your locked BTC back to Xverse. Getting your BTC back takes two actions: **repay** what you owe in USDC on Stellar, then **release** the BTC on Bitcoin. Writz co-signs the release only after the loan is fully repaid, and the BTC can only go to the address of the Bitcoin key that made the deposit. **You need:** the Stellar wallet that opened the loan, the same Xverse account, and enough test USDC to cover the debt. ## Open your loan Go to [testnet.writz.xyz](https://testnet.writz.xyz) and connect your wallets. The home page shows your loan with **Borrow more**, **Repay** and **Open Loan 1** (the number is your loan's). Select **Open Loan** to see the full loan page. Loans are found on Stellar, so this works from any device with the same Stellar wallet. ![Home page with Loan 1, its collateral ratio, collateral and debt, and the Borrow more, Repay and Open Loan 1 buttons](https://mintcdn.com/writz-protocol/AtDBkTL5W_JkQblu/images/guides/home-loan.png?fit=max\&auto=format\&n=AtDBkTL5W_JkQblu\&q=85\&s=4c0cde0a00de45e4aaa3577c0f04a021) The home page with an open loan. Example amounts. ## 1. Repay **1. Open the Repay tab** On the loan page, select **Repay**. It shows your USDC balance and what you owe, for example **Wallet: 1,620.00 USDC. You owe 1,500.00 USDC.** **2. Enter the amount** Enter an amount, or select **Repay all** to pay the whole debt. A partial repayment lowers the debt and raises your collateral ratio; only a full repayment lets you release the BTC. **3. Confirm** Select **Repay** and confirm in your Stellar wallet. ![Loan page on the Repay tab, with the amount field, Repay all and the wallet and debt line](https://mintcdn.com/writz-protocol/AtDBkTL5W_JkQblu/images/guides/loan-repay.png?fit=max\&auto=format\&n=AtDBkTL5W_JkQblu\&q=85\&s=ba7f5e4bedb9f540cdbb25b59f34289b) The Repay tab. Example amounts. > [!TIP] > > Interest accrues while the loan is open, so you owe slightly more than you borrowed. If your wallet holds exactly what you borrowed, get a few more test USDC from [Circle's faucet](/guides/get-test-funds#test-usdc-on-stellar-testnet) before you select **Repay all**. ## 2. Wait for Writz to co-sign After a full repayment the loan shows **Loan repaid** and **BTC still locked**. Writz now co-signs the release of your BTC, which is usually ready within a minute. Until then the page reads **Waiting for Writz**. Your BTC is safe while you wait: it stays in the lock address, and nobody can move it without your key. ![Loan page showing Loan repaid, BTC still locked and Waiting for Writz](https://mintcdn.com/writz-protocol/AtDBkTL5W_JkQblu/images/guides/loan-waiting-cosign.png?fit=max\&auto=format\&n=AtDBkTL5W_JkQblu\&q=85\&s=0cdc8c96c28ff6d37ebd853771f7d1c4) Repaid, waiting for Writz's co-signature. ## 3. Release your BTC **1. Open the Release BTC tab** Select **Release BTC**. The page shows where the BTC goes and what you receive: * **Send to your Bitcoin key's address**: the address of the Xverse key that made the deposit. * **Amount**, **Bitcoin network fee** (a few hundred sats) and **You receive**, the amount less the fee. **2. Slide to release** Drag **Slide to release** all the way to the right. The app checks the loan on Stellar, confirms it is repaid and gets Writz's signature. **3. Confirm in Xverse** Xverse opens and asks you to sign. Confirm it. The app sends the transaction to Bitcoin. ![Release BTC tab with the destination address, amount, network fee, amount received and Slide to release](https://mintcdn.com/writz-protocol/AtDBkTL5W_JkQblu/images/guides/loan-release.png?fit=max\&auto=format\&n=AtDBkTL5W_JkQblu\&q=85\&s=23aa3ce43989c3a58be58df312d18a36) Release review. The BTC can only go to your own key's address. Example address and amounts. When it is sent, the loan shows **BTC released** with a **View on mempool** link. The BTC appears in Xverse once the transaction confirms on Bitcoin signet. ![Loan page showing BTC released, the amount and destination, and a View on mempool link](https://mintcdn.com/writz-protocol/AtDBkTL5W_JkQblu/images/guides/loan-released.png?fit=max\&auto=format\&n=AtDBkTL5W_JkQblu\&q=85\&s=182ffd539ef72f652feb0fdba249d4d3) Released. Example amount and address. ## If something blocks the release * **Xverse not connected, or a different Xverse account**: the release needs the same Bitcoin key that made the deposit. Connect it in the Wallets panel and try again. * **Waiting for Writz takes longer than a few minutes**: refresh the page later. Your loan is repaid on Stellar and the release does not expire. * **Writz is not available at all**: you don't need Writz to get your BTC back in the end. After the block height shown under **Your way out**, your key alone can spend from the lock address. See [How to reclaim](https://testnet.writz.xyz/help/reclaim) in the app and [Manual Emergency Recovery](/how-it-works/manual-emergency-recovery). *Last checked against the app on 2026-10-25.* # Set up Freighter Source: https://docs.writz.xyz/guides/set-up-freighter Install Freighter, create a wallet, switch it to Stellar testnet and fund it with test XLM. [Freighter](https://www.freighter.app) is a Stellar wallet that runs as a browser extension, made by the Stellar Development Foundation. In Writz it receives the USDC you borrow and signs every loan action. Other Stellar wallets that work with the app include xBull and Lobstr; the steps below are for Freighter. > [!TIP] > > No wallet to install? [Sign in with Google, GitHub or email](/guides/sign-in-with-email) instead and skip this page. ## 1. Install and create a wallet **1. Install the extension** Install Freighter from [freighter.app](https://www.freighter.app), which links to the official store for your browser. Pin it to the toolbar so it is easy to open. **2. Create a wallet** Open Freighter and select **Create new wallet**. Under **Create a Password**, enter a **New password** and **Confirm password**, tick **I have read and agree to** the Terms of Use, and select **Confirm**. **3. Save the recovery phrase** Freighter shows **Your Recovery Phrase**. Write the words down in order and keep them somewhere safe, then select **I’ve saved my phrase somewhere safe**. On **Confirm Your Recovery Phrase**, select the words in the right order and select **Confirm**. > [!WARNING] > > Anyone with the recovery phrase controls the wallet. Writz never asks for it, and neither does Freighter outside the extension. Already use Freighter for real funds? Add a separate account for testing instead of reusing your main one. ## 2. Switch to Testnet Freighter starts on Mainnet. Writz runs on Stellar testnet. **1. Open the network menu** Open Freighter. At the top of the main screen, select the **globe icon**. A list of networks opens. **2. Select Testnet** Select **Testnet**. The account now shows its testnet balance, which is empty until you fund it below. If you connect to Writz while Freighter is on another network, the app shows **Wrong network** and asks you to switch. ## 3. Fund the account with test XLM A new Stellar account does not exist on the network until it holds some XLM. On testnet, a service called Friendbot gives you free test XLM. **1. Fund with Friendbot** With Freighter on **Testnet**, the main screen shows an empty account with a **Fund with Friendbot** button. Select it. **2. Check the balance** After a few seconds the account shows a balance of 10,000 XLM. That covers every fee you will pay in Writz many times over. You can also fund the account from Writz itself: when a connected account has no XLM, the app shows **Account not funded** with a **Fund with Friendbot** button. ## What's next [Set up Xverse](/guides/set-up-xverse) if you want to borrow, or go straight to [Get test funds](/guides/get-test-funds) to get test USDC. *Labels checked against Freighter 5.49.0 on 2026-10-25. Freighter updates often, so a label may differ slightly in a newer version.* # Set up Xverse on Bitcoin signet Source: https://docs.writz.xyz/guides/set-up-xverse Install Xverse, switch it to Bitcoin signet and find the address that receives your test BTC. [Xverse](https://www.xverse.app) is the Bitcoin wallet the Writz app connects to. It holds the BTC you lock, and its key is part of your lock address, so only you and Writz together can move the BTC while the loan is open. You only need it to **borrow**; lending USDC does not use Bitcoin. ## 1. Install and create a wallet **1. Install the extension** Install the Xverse browser extension from [xverse.app](https://www.xverse.app) and use it on a desktop browser. **2. Create a wallet** Open Xverse and select **Create a new wallet**. Set a password when asked. **3. Back up the seed phrase** Xverse shows your seed phrase. Write it down in order and keep it safe, then confirm the words it asks for. > [!WARNING] > > Writz cannot recover your Bitcoin key. If you lose the seed phrase and the extension, BTC locked in a loan can't be released to you. On testnet that costs nothing, but treat it as practice for real funds. ## 2. Switch to Signet Writz on testnet uses **Bitcoin signet**, a Bitcoin test network. Xverse starts on Bitcoin mainnet. **1. Open the network settings** In Xverse, open **Settings**, then **Network**. **2. Select Signet** Select **Signet** and then **Save**. Do not pick Testnet3 or Testnet4: they are different test networks and Writz will not see coins on them. If Xverse is on any other network when you connect, the app tells you which network it found and asks you to switch to Bitcoin signet. Switch and select **Connect Xverse** again. ![Borrow step 3 with a Wrong network notice asking to switch Xverse to Bitcoin signet](https://mintcdn.com/writz-protocol/AtDBkTL5W_JkQblu/images/guides/borrow-3-wrong-network.png?fit=max\&auto=format\&n=AtDBkTL5W_JkQblu\&q=85\&s=a683ce1d7447f1e37056e725dfd1afc4) What the app shows when Xverse is on the wrong network. ## 3. Find your payment address Xverse has two Bitcoin addresses: a **payment** address and an **ordinals** address. Writz uses the **payment** address. * Xverse labels them in its receive screen. Use the one marked for **Bitcoin** payments, not **Ordinals**. * Send faucet coins to the payment address. Coins sent to the ordinals address do not count toward your balance in Writz. Once you connect Xverse to Writz, the Wallets panel shows the exact address it uses and its balance, with a **Copy address** button. Copying the address from there is the safest way to get the right one. ## What's next [Get test BTC](/guides/get-test-funds#test-btc-on-bitcoin-signet) for this address. *Labels checked on 2026-10-25. Xverse updates often, so a menu name may differ slightly in your version. The app always tells you if the network is wrong.* # Sign in with Google, GitHub or email Source: https://docs.writz.xyz/guides/sign-in-with-email Get a Stellar test account without installing a wallet, through Pollar. If you don't have a Stellar wallet, you can sign in with Google, GitHub or an email code. The app then creates a Stellar test account for you through [Pollar](https://pollar.xyz), with test XLM already in it and USDC already added. > [!NOTE] > > **Pollar holds the key of this account**, not you. That is fine for test coins and the reason this option exists only on testnet. For anything you want to control yourself, use [Freighter](/guides/set-up-freighter). After you sign in, the app also creates a **Signet test key** in your browser for the Bitcoin side, and asks the Writz faucet to send it test BTC. You can then lock BTC, borrow, repay and release without installing anything. If you prefer, you can still use [Xverse](/guides/set-up-xverse) instead. ## Sign in **1. Open the Wallets panel** Go to [testnet.writz.xyz](https://testnet.writz.xyz) and select **Connect wallets** at the top right. **2. Choose how to sign in** Under **Stellar wallet**, below **Connect Stellar wallet**, select **Google**, **GitHub** or **Email**. ![Wallets panel with Connect Stellar wallet, then Google, GitHub and Email buttons](https://mintcdn.com/writz-protocol/AtDBkTL5W_JkQblu/images/guides/wallets-panel.png?fit=max\&auto=format\&n=AtDBkTL5W_JkQblu\&q=85\&s=de2eff0ce637e0e6d9dc589126af7769) The Wallets panel before connecting. The three sign-in buttons are under the Stellar wallet. **3. Finish signing in** * **Google or GitHub:** a popup opens. Sign in there and allow access. The panel shows **Finish signing in in the popup** until you do. * **Email:** type your email, select **Send code**, then enter the code you receive under **Code** and select **Verify**. ![Wallets panel with an email field and the Send code button](https://mintcdn.com/writz-protocol/AtDBkTL5W_JkQblu/images/guides/pollar-email.png?fit=max\&auto=format\&n=AtDBkTL5W_JkQblu\&q=85\&s=9c795283c860fd023ff2cc1961ea66c8) Email sign-in. The code arrives by email; enter it in the next screen. **4. Wait for your account** The panel shows **Creating your Stellar account...** and then **Your account is being created on Stellar. This takes a few seconds.** When it is ready, your Stellar address appears in the panel and **USDC added** reads **Yes**. ## What you get * A Stellar testnet account with test XLM in it, so you never need Friendbot. * USDC already added to the account (the trustline), so you can receive test USDC straight away. * Network fees paid by Pollar on Writz transactions. If a transaction is not covered, the test XLM in the account pays its small fee. ## Your Signet test key * **Where it lives:** only in this browser's storage, one key per signed-in account. It is never sent to Writz, to Pollar or to the relayer. * **Test coins only:** it exists only on testnet, with Bitcoin signet coins that have no value. The mainnet app has no such key. * **Funding:** right after sign-in, the Writz faucet sends it 20,000 test sats once per account. The coins can be locked as soon as they are sent. If the faucet is off, empty or has reached its daily limit, the Home page says so and links to public signet faucets; send test BTC to the key's address shown there. * **Losing it:** clearing site data, a private window or another device does not have the key. A test loan whose key is lost cannot be released. To move it, open the Wallets panel and select **Back up or restore** under **Signet test key** to download the key file, then restore it in the other browser. * **Anyone with the key file** can spend its test coins. Keep it private, even though they have no value. ## Your first loan Home shows **Your first loan, in five steps**: get test BTC, lock BTC, borrow USDC, repay and release. Each step opens the usual screen with a suggested amount: most of your test BTC to lock, and a quarter of what you could borrow, far from liquidation. If interest leaves you a few cents short when you repay, the Repay panel offers to swap a little test XLM for the missing USDC. ## Sign out Open the Wallets panel and select **Disconnect** under the Stellar wallet. Signing in again with the same Google account, GitHub account or email gives you the same Stellar account back, with your loans and balances. *Last checked against the app on 2026-10-25.* # Start here Source: https://docs.writz.xyz/guides/start-here What you can do in the Writz testnet app, what you need for each part, and the order to set things up. These guides walk you through the Writz app at [testnet.writz.xyz](https://testnet.writz.xyz), click by click. Everything runs on test networks: Stellar testnet and Bitcoin signet. The coins are free test coins with no value, so you can try every step without risk. > [!WARNING] > > Writz is on testnet only. The contracts are not audited yet. Never send real BTC, real USDC or a wallet that holds real funds to the testnet app. ## What you can do | You want to | What you need | Guide | | - | - | - | | Borrow USDC against your Bitcoin | A Stellar wallet and Xverse, test XLM and test BTC | [Borrow USDC against BTC](/guides/borrow) | | Pay back a loan and get your BTC back | The same two wallets, plus test USDC for the interest | [Repay and release your BTC](/guides/repay-and-release) | | Earn interest by lending USDC | A Stellar wallet and test USDC | [Lend USDC](/guides/lend) | ## Set up in this order **1. Get a Stellar account** Either install [Freighter](/guides/set-up-freighter), a browser wallet, or [sign in with Google, GitHub or email](/guides/sign-in-with-email) to get a test account with no wallet to install. Both work for every part of the app. **2. Install Xverse on Bitcoin signet** Only needed to borrow. Follow [Set up Xverse](/guides/set-up-xverse). **3. Get test funds** Test XLM for Stellar fees, test BTC to lock and test USDC to lend or to repay interest. All free: [Get test funds](/guides/get-test-funds). **4. Connect your wallets in the app** See [Connect your wallets](/guides/connect-your-wallets). ## Before you start * **Use a desktop browser.** Freighter and Xverse are browser extensions. Chrome, Brave and Edge work. * **Use test wallets.** Create new wallets for testnet, or at least a separate account, and never type a recovery phrase that protects real funds into anything you are testing. * **Plan for the Bitcoin wait.** A loan starts with a real Bitcoin signet transaction, and signet blocks arrive about every 10 minutes, sometimes slower. The app shows how many confirmations it is waiting for, and you can close the page and come back. ## About the screenshots The screenshots come from the app itself. Some show example amounts, addresses and prices, so your numbers will differ. Button names and messages are exactly what the app shows. If something on your screen does not match a guide, see [Troubleshooting](/guides/troubleshooting) or [report it on GitHub](https://github.com/WritzProtocol/writz/issues). *Last checked against the app on 2026-10-25.* # Troubleshooting Source: https://docs.writz.xyz/guides/troubleshooting What the Writz app's messages mean and how to fix the common problems on testnet. The app tells you what is wrong in plain words. Find the message you see below. Your funds are never at risk from any of these: on testnet they are test coins, and a failed or declined step never moves BTC out of a lock. ## Wallets and networks **Wrong network** (Stellar) Your Stellar wallet is on Mainnet or another network. Switch it to **Testnet**: in Freighter, select the globe icon at the top and choose **Testnet** ([details](/guides/set-up-freighter#2-switch-to-testnet)). **Wrong network** (Bitcoin), or a message asking you to switch Xverse to Bitcoin signet Xverse is on Bitcoin mainnet, Testnet3 or Testnet4. In Xverse, open **Settings**, then **Network**, select **Signet** and **Save**. Then select **Connect Xverse** again ([details](/guides/set-up-xverse#2-switch-to-signet)). **You declined in your wallet. Nothing was shared.** You closed or rejected the wallet window. Select the connect button again and approve it in the wallet. **Account not funded** or **Not funded** The Stellar account has no XLM, so it can't pay fees. Select **Fund with Friendbot**, then **Check again** if shown. **Low XLM** Fewer than 2 XLM left. Fund the account again from Friendbot (in Freighter, or **Test XLM from Friendbot** in the Wallets panel). ## Test funds **Not enough BTC** Your Xverse payment address has less signet BTC than the amount you chose. Get more from a [signet faucet](/guides/get-test-funds#test-btc-on-bitcoin-signet), wait for it to arrive, and select **Check balance again**. **Faucet coins don't show up** Check that you sent them to the Xverse **payment** address, not the ordinals address, and that Xverse is on **Signet**. Faucets send in batches, so give it a few minutes. You can paste the address into [mempool.space/signet](https://mempool.space/signet) to see incoming transactions. **USDC added: Not yet**, or Circle's faucet fails Your account has not opted in to USDC. Open **Lend** and select **Add USDC**, confirm in your wallet, then request from the faucet again ([details](/guides/get-test-funds#test-usdc-on-stellar-testnet)). **USDC shows in my wallet but not in Writz** You probably have Blend test USDC, a different token. Writz uses Circle's testnet USDC, issuer `GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5`. Get it from [faucet.circle.com](https://faucet.circle.com) with **Stellar Testnet** selected. ## While borrowing **Not seen yet**: *Bitcoin hasn't seen this transaction yet.* The deposit has not reached the Bitcoin network yet. If Xverse shows it as sent, wait a few minutes and select **Check again**. Use **View on mempool** to watch it. **Can't reach the Writz relayer. Retrying on its own.** The service that reads Bitcoin for the app is briefly unreachable. Nothing to do: it retries, and your BTC is safe on Bitcoin. **Waiting for the Writz relayer to see it** Your deposit has its confirmations and Writz is catching up with the latest Bitcoin block. This usually clears within a few minutes. **You declined in Freighter. Your BTC is still safe in the lock.** You rejected the Stellar signature that registers the deposit. Select **Sign again**. **One more step adds your loan** The deposit is recorded on Stellar but the app has not loaded it yet. Select **Finish deposit**. **The confirmation count is slow** Bitcoin signet blocks come about every 10 minutes on average, and a single block can take much longer. You can close the page; the deposit keeps confirming and the app picks it up when you come back in the same browser. ## While repaying and releasing **Wallet balance lower than what you owe** The Repay tab shows both, for example *Wallet: 120.00 USDC. You owe 1,500.00 USDC.* Get more test USDC from [Circle's faucet](/guides/get-test-funds#test-usdc-on-stellar-testnet), or repay part now and the rest later. **Different Bitcoin account** The release must be signed by the Xverse account that made the deposit. Select **Use a different account** and connect that one. **Connect Xverse to sign the release.** Xverse is not connected. Select **Connect Xverse**. **Waiting for Writz** Writz is co-signing your release, usually within a minute. Come back later if it takes longer; your loan stays repaid and the BTC stays in the lock until you release it. ## Network and service **Can't reach Stellar testnet right now. Your funds are not affected.** The Stellar testnet node did not answer. Select **Try again** in a moment. **Loans not loaded** The app could not read your loans. Select **Try again**. Your loans are on Stellar and nothing is lost. ## Still stuck? [Open an issue on GitHub](https://github.com/WritzProtocol/writz/issues) with the message you see, the page you were on and, if there is one, the transaction link. Never post a recovery phrase or secret key. *Last checked against the app on 2026-10-25.* # The Bitcoin Side Source: https://docs.writz.xyz/how-it-works/bitcoin-side **How your BTC is locked - and why nobody else can touch it.** Before anything happens on Stellar, a Bitcoin transaction secures your collateral. This page explains the Bitcoin locking mechanism: what it is, how it works, and what the security guarantees are. For how keeping BTC in your own script compares with wrapped Bitcoin and bridges, see [SPV vs Wrapped Bitcoin](/security/spv-vs-wrapped). *** ## The Core Idea: Rules Written Into Bitcoin When you make a standard Bitcoin payment, you send BTC to an address, and the recipient can spend it with their private key. That is a simple rule: "this key can spend this output." Bitcoin Script allows more complex rules. Writz uses a P2WSH - Pay to Witness Script Hash - which can encode multiple spending conditions. The address is a hash of a full script; to spend from it, you reveal the script and satisfy one of its conditions. Writz's P2WSH script has exactly two conditions: **Condition A (normal - loan repaid):**\ The BTC can be spent only if both the user's signature AND Writz's protocol signature are present. This is the cooperative release - it happens automatically when a loan is repaid and the Stellar contract generates the co-signature. **Condition B (emergency - timelock expiry):**\ After a defined time-lock (loan duration + a 7-day safety buffer), the user can spend the BTC alone - no Writz signature required. This protects users if the protocol becomes unavailable, goes offline, or ceases to exist. The critical property: **neither Writz alone nor the user alone can move the BTC during the active loan period**. Both signatures are required for Condition A. The time-lock enforces Condition B. *** ## The Script ```bitcoin theme={null} OP_IF OP_CHECKSIGVERIFY OP_CHECKSIG OP_ELSE OP_CHECKLOCKTIMEVERIFY OP_DROP OP_CHECKSIG OP_ENDIF ``` **Spending Condition A** (OP\_IF branch): * Requires protocol signature (`OP_CHECKSIGVERIFY` - fails without it) * Then requires user signature (`OP_CHECKSIG`) * Both must be present. Either alone is insufficient. **Spending Condition B** (OP\_ELSE branch): * Requires the block height to exceed `` (`OP_CHECKLOCKTIMEVERIFY`) * Then requires only the user signature * No protocol involvement needed after the time-lock expires The script is 114 bytes. Each deposit gets a **unique address** derived from the protocol key, the user key, and the time-lock value - ensuring no two deposits share an address. *** ## How the Address Is Derived ``` deposit_address = P2WSH( redeem_script(protocol_pubkey, user_pubkey, locktime) ) ``` The `locktime` is calculated as: ``` locktime = current_block_height + loan_duration_blocks + safety_buffer_blocks ``` For a 90-day loan starting at block 900,000: * Loan duration: \~12,960 blocks (90 days × 144 blocks/day) * Safety buffer: \~1,008 blocks (7 days) * `locktime` = 900,000 + 12,960 + 1,008 = 913,968 After Bitcoin block 913,968, the user can spend the locked BTC alone. *** ## Spending Path A: The Normal Release When a user repays their USDC loan, the following happens: 1. The Stellar `writz-lend` contract records the full repayment, closes the position and emits a `repay_full` event. 2. The relayer's repay watcher sees the event, builds the release PSBT (Partially Signed Bitcoin Transaction) paying the user's own key, signs it with the protocol key, and publishes the half-signed PSBT on `writz-lend` with `publish_release_psbt`. 3. The Writz UI fetches it with `get_release_psbt` and the user's Bitcoin wallet adds the user signature. 4. The PSBT is finalized and broadcast to the Bitcoin network. 5. The BTC arrives in the user's wallet after 1–6 confirmations. The witness structure for Condition A: ``` [user_sig (71 bytes), protocol_sig (72 bytes), 0x01, redeemScript (114 bytes)] ``` This produces a transaction of \~149 vbytes - efficient and inexpensive. **Verified on Bitcoin Signet:**\ A real Path A release transaction was broadcast and accepted by the Bitcoin Signet mempool: [`11932100`](https://blockstream.info/signet/tx/119321009b2f92dac8f25f6bcddb2ed6a3ae778e8748ec52910cce90742e4098) * 89,631 satoshis locked; 88,131 satoshis released (Path A co-signed release) * 149 vbytes at 10.1 sat/vbyte * Both keys signed the PSBT independently (multi-party flow) *** ## Spending Path B: The Emergency Release If the time-lock has expired and a user wants to reclaim their BTC without a protocol co-signature: 1. The user assembles a transaction with `nLockTime` set to `locktime`. 2. **The input's `nSequence` must be set to a value less than `0xFFFFFFFF`** (Writz uses `0xFFFFFFFE`) - see the warning below. 3. The transaction is signed with the user's key only. 4. Once the Bitcoin block height exceeds `locktime`, the transaction becomes valid. 5. The user broadcasts it directly to the Bitcoin network - no Writz involvement. This ensures that user funds are never permanently inaccessible, even in a worst-case scenario where Writz stops operating. > **\[WARNING] `nSequence` interaction:** `OP_CHECKLOCKTIMEVERIFY` has a non-obvious interaction with `nSequence` - if the spending input's `nSequence` is `0xFFFFFFFF` (Bitcoin's "final" value, which many wallets use by default), the Bitcoin Script interpreter causes `CLTV` to fail immediately, **even if the timelock has genuinely expired**. There is no clear error message - the transaction is simply rejected by the network, and it's easy to mistakenly conclude the timelock hasn't expired when it has. The Writz frontend and the `bitcoin-script` package's `buildEmergencyTransaction`/`finalizePathB` helpers already set this correctly and hardcode it (it cannot be overridden by a caller). If you are constructing this recovery transaction manually with a third-party wallet or library instead of using Writz's own tooling, you must set `nSequence` yourself - see [Manual Emergency Recovery](/how-it-works/manual-emergency-recovery) for a safe, copy-pasteable reference implementation. *** ## Security Properties **What Writz can do:** * Co-sign releases (with user agreement) when loans are repaid * Refuse to co-sign (keep BTC locked) if a loan is still outstanding * Nothing else **What Writz cannot do:** * Move the BTC unilaterally (no user signature = no valid Condition A spend) * Prevent the user from recovering BTC after the time-lock (Condition B requires no protocol involvement) * Access the BTC if both the user key and protocol key are lost (standard key management risk) **What happens if Writz disappears:** * Active loans: Users wait for the time-lock expiry, then claim BTC via Condition B. They keep their USDC. The protocol absorbs the loss. * No active loans: No locked BTC exists; nothing is at risk. *** ## Protocol Co-signing Key Architecture The protocol co-signing key is used only to co-sign release transactions for fully repaid loans. It can never move Bitcoin alone: every spending path of the script also requires the user's key, and the user's time-lock exit does not need it at all. **Planned:** the protocol side becomes 2-of-3 keys, held separately and written into the P2WSH script itself (see the script form below), so Bitcoin enforces the threshold natively and no single key can co-sign a release. This replaces earlier plans for threshold MPC signing. The signing policy, what the protocol signers can and cannot sign, will be published. *** ## Script Roadmap: P2WSH, not Taproot Writz stays on P2WSH with an absolute block-height time-lock (`OP_CHECKLOCKTIMEVERIFY`) for mainnet v1. An earlier plan to move to Taproot (P2TR) with a MuSig2 key path is withdrawn, because: * Every launch wallet can sign a P2WSH miniscript today, while Taproot script-path and MuSig2 signing are not broadly supported. * The protocol side is planned as 2-of-3 keys. MuSig2 is n-of-n, so a 2-of-3 key path would need threshold Schnorr signing (FROST), which no launch wallet or custody option supports. * The Soroban contract checks a P2WSH output with one SHA-256 comparison. A Taproot output check would need Schnorr or elliptic-curve arithmetic that Soroban does not provide as host functions. The planned change is to the script's form, not its type. It moves to a miniscript policy with the user's key and any 2 of 3 protocol keys on the cooperative path, and the user's key alone after the time-lock: ``` wsh(and_v(v:pk(U),or_i(multi(2,K1,K2,K3),after(H)))) ``` This keeps the user's emergency exit exactly as it is and lets policy-based hardware wallets sign it. Until it ships, the deployed script is the one shown above. Taproot is reconsidered only once threshold Schnorr signing is available in the launch wallets and signers. *** **Next:** [How SPV Verification Works →](/how-it-works/spv-verification) # Privacy edition (in progress) Source: https://docs.writz.xyz/how-it-works/privacy-edition > **Status: design in progress, testnet only, not part of Writz v1.** Writz v1 (Writz Lend on `bitcoin-spv` and `writz-lend`) has no privacy layer: positions, amounts and collateral are public on chain. Nothing on this page protects a Writz Lend position today. Privacy is not part of the Writz protocol. It is being designed as a separate, opt-in edition: its own contracts (today `commitment-tree`, `zk-verifier` and the Circom circuits, all under `privacy/` in the repository), its own lending pool, its own audit and its own trusted-setup ceremony. It is planned for after v1 is live on mainnet, on its own subdomain, and has no date; it is not on the v1 mainnet plan. Users who do not want privacy never interact with it, and `writz-lend` never calls these contracts. The rest of this page records the current design of that edition, as built and deployed on testnet. Zero-knowledge proofs would let the edition's contracts verify facts about a position (that the loan is adequately collateralized, that the deposit is real, that a repayment is correct) without learning the underlying numbers. *** ## Zero-Knowledge Proofs in Plain English A zero-knowledge proof lets you prove something is true without revealing anything else. The classic analogy: imagine you want to prove to someone that you know the solution to a Sudoku puzzle, without revealing the solution. A ZK proof is a mathematical protocol that makes this possible - you can demonstrate you know the answer, and the verifier becomes convinced, without ever seeing the answer. In the privacy edition, the "fact" being proved is: *"I have a deposit commitment in this Merkle tree, my loan amount is X% or less of my collateral value, and I have the secret that unlocks this commitment."* The ZK proof convinces the Soroban contract that all of this is true - without the contract ever learning the deposit amount, the loan amount, or anything else about the position. *** ## What Is Hidden, What Is Visible | Information | Visibility | | - | - | | That a commitment exists in the tree | Public (commitment hash stored on-chain) | | The collateral amount | **Private - never on-chain** | | The loan amount | **Public** - the USDC transfer and the borrow/repay events show it (see below) | | The health factor | **Private - never on-chain** | | The user's public key linked to a position | **Private** | | The depositor's Stellar address and the Bitcoin deposit transaction | **Public** - the deposit event links them, and the deposit transaction is public | | That a liquidation occurred | Public (event emitted) | | Who was liquidated | **Private** (only a nullifier is revealed, not a user identity) | | How much debt was repaid in a liquidation | Public - the liquidator must know the amount to pay it. The published amount is provably correct (bound to the private commitment via a circuit constraint, not caller-supplied), but it is not hidden. | | Total TVL (aggregate) | Public | | Total USDC outstanding (aggregate) | Public | The protocol sees only commitments and nullifiers - opaque hashes that prove actions occurred without revealing the substance of those actions. **What the ZK layer does not hide:** the commitment-tree contract publishes the depositor's address with its Bitcoin transaction id, and the borrow and repay events and USDC transfers carry the Stellar address and exact amount. Anyone reading the ledger can link a Stellar address to a Bitcoin deposit and see loan sizes. Hiding these would take different transfer rails, not event changes, so this is a known limit of the current design. *** ## The Commitment Scheme At the heart of the ZK layer is a **commitment**. A commitment is a hash of private data: ``` liq_key = Poseidon(secret) commitment = Poseidon(collateral, debt, liq_key, nonce, timelock_height) ``` Where: * `collateral` is the BTC deposit amount in satoshis (private) * `debt` is the USDC owed, in stroops (private) * `secret` is derived from the user's wallet signature and never leaves the device (private) * `liq_key` is enough to liquidate the position, not to borrow, repay or release it (private; also encrypted to the protocol keeper, see below) * `nonce` rotates on every change, so the new commitment cannot be linked to the old one (private) * `timelock_height` is the CLTV height of the deposit's Bitcoin script (private after deposit) **Poseidon** is a ZK-friendly hash function - designed specifically to be efficient inside ZK circuits. It is collision-resistant and hiding: given only the commitment, computing the preimage is computationally infeasible. **Keeper notes.** Every borrow and repay proof also encrypts the new position's `liq_key`, nonce and amounts to the protocol keeper's key, inside the circuit, and the contract publishes the ciphertext in the event. Only the keeper can open it, and what it learns lets it liquidate an undercollateralized position without the owner, never move the owner's funds. Observers learn nothing from it. The commitment is stored on-chain in a Merkle tree. The preimage stays with the user. *** ## The Nullifier When a commitment is "consumed" (used in a borrow, repay, or withdraw), a **nullifier** is published: ``` nullifier = Poseidon(liq_key, nonce) ``` The nullifier is stored on-chain in a spent-nullifier set. It proves this commitment has been used - preventing double-spending - without revealing which commitment it corresponds to or what amount it represents. This is the same technique used by Tornado Cash, Aztec Network, and other ZK privacy protocols: the nullifier is the receipt that something happened, but it is unlinkable to the original commitment without the position's private keys. *** ## The ZK Circuits The privacy edition uses five Circom circuits (deposit, borrow/repay, liquidation, zero-debt release and Merkle insertion), each compiled to a Groth16 proof system over the BN254 curve. The constraint counts below are measured with `circom --O2`. ### 1. Deposit Circuit **What it proves:** "I know (collateral, secret, nonce) such that `commitment = Poseidon(collateral, 0, Poseidon(secret), nonce, timelock_height)`, the collateral equals what the SPV-verified transaction paid, and the transaction ID matches my claimed deposit." **Public inputs:** * The commitment hash and nullifier (stored on-chain) * The `txid` returned by the SPV contract (binding the ZK proof to a specific Bitcoin transaction) * The minimum deposit and the real amount the contract parsed from the transaction * `timelock_height`, which the contract checks against the deposit's Bitcoin script (#212) **Private inputs:** * Collateral (satoshis) * Secret * Nonce **Constraint count:** 855 **Why the txid binding matters:** Without this binding, a malicious user could submit a ZK deposit proof that references a Bitcoin transaction that doesn't actually exist. The txid binding ensures that every ZK commitment corresponds to a real, SPV-verified Bitcoin deposit. *** ### 2. Borrow/Repay Circuit **What it proves for borrow:** "I know the preimage of a commitment in the Merkle tree, the Oracle price P, and a loan amount L such that: `L / (amount × P) ≤ 1/min_ratio` (the loan is within the allowed LTV)." **What it proves for repay:** "I know the preimage of a commitment in the Merkle tree, and the repay amount correctly reduces the outstanding debt." **Public inputs:** * The Merkle root (current state of the commitment tree) * The oracle price * The minimum collateral ratio (150%) * A nullifier for the old state * A new commitment for the updated state (after borrow or repay) * The recipient binding * `min_timelock_height`: a borrow proves the position's timelock is at least this, and the contract requires it to be at least the Bitcoin tip plus a 30-day margin (#212) * The keeper's public key and the keeper note: the new position's liquidation witness, encrypted to that key inside the proof (#212) **Private inputs:** * Collateral, secret, nonces and timelock (commitment preimage) * Current debt * Delta (borrow or repay amount) * Merkle path (proof of membership in the tree) * A fresh ephemeral key for the keeper note **Constraint count:** 15,410 **Key design decision - no division in ZK circuits:** Division is expensive in ZK (requires a range proof for the denominator). The collateral ratio check is implemented as a multiplication: `loan_amount × min_ratio_bp ≤ collateral_amount × price × 10000`. This avoids division entirely. **Field negation for repay:** The repay amount is encoded as a field element negation - `p − delta_stroops` where `p` is the BN254 field prime. The circuit recovers the repay amount correctly. This was verified on-chain. **`is_borrow` soundness fix:** earlier versions of this circuit left `is_borrow` unconstrained - nothing forced it to be `0` or `1`, and nothing tied it to the actual sign of `delta_stroops`. A prover could submit `is_borrow=0` (skipping the collateral-ratio check entirely, which is only gated on `is_borrow=1`) while still passing a positive `delta_stroops` that increases debt with zero ratio enforcement. The only thing that prevented this from being exploitable end-to-end was that `commitment-tree::repay`'s Rust-side decode of `delta_stroops` (field-negation, described above) happens to overflow `i128` for a non-negative delta - a property of that one caller, not a guarantee the circuit itself made. The circuit now directly constrains `is_borrow` to be boolean and requires `new_debt >= old_debt` when `is_borrow=1`, `new_debt <= old_debt` when `is_borrow=0` - see `privacy/circuits/src/borrow_repay.circom`, Step 3b, and the negative tests in `privacy/circuits/test/borrow_repay.test.js`. *** ### 3. Liquidation Circuit **What it proves:** "I know the preimage of a commitment in the Merkle tree, and the position's current health ratio is below 120% at the current oracle price." The witness is the position's `liq_key`, not its secret, so the keeper can prove it from the keeper note without the owner (#212). **Public inputs:** * The Merkle root * The oracle price * The usdc\_debt (the debt amount - extracted from the proof so the liquidator cannot inflate it) * The liquidation threshold (120%) **Private inputs:** * Collateral, `liq_key`, nonce and timelock (commitment preimage) * Current debt * Merkle path **Constraint count:** 5,699 **Critical security property - `usdc_debt` binding:** The liquidation circuit's `usdc_debt` public output is computed inside the circuit from the private `debt_stroops` field. The liquidator cannot supply an arbitrary debt amount - it is derived from the private commitment and constrained by the circuit. This prevents a malicious keeper from claiming a larger debt than actually exists. *** ### 4. Zero-Debt Release Circuit Proves a leaf in the tree encodes zero debt, without revealing its collateral or keys. It publishes the leaf's `commitment` and `nullifier`: `commitment-tree::mark_released` spends the nullifier so the leaf can never borrow again, and `/api/cosign` only co-signs the BTC release once that is recorded. *** ### 5. Insertion Circuit Proves that a new Merkle root is the current root with one commitment written into the next empty leaf. The relayer generates it from public data when a deposit is inserted, and `commitment-tree::insert_commitment` accepts the proof instead of a root, so neither the admin key nor a relayer bug can install a tree with invented or missing leaves (#211). *** ## Groth16 and Protocol X-Ray All five circuits use **Groth16** - the most widely deployed ZK proof system, used by Zcash, Tornado Cash, and Stellar's own Private Payments reference implementation. A Groth16 proof is a small constant-size object (3 elliptic curve points, \~128 bytes on BN254) that can be verified in constant time - regardless of the size of the computation being proven. Verification uses **BN254 pairing checks**: elliptic curve operations that confirm the relationship between the proof and the verification key. These are computationally expensive but deterministic. Stellar's **Protocol X-Ray** (Protocol 26) added native host functions for BN254 operations: * `bn254_g1_msm` - multi-scalar multiplication on the G1 curve * `bn254_pairing_check` - pairing check across multiple pairs The `zk-verifier` contract uses these host functions to run the Groth16 verification: ``` verify = e(A, B) == e(vk.alpha, vk.beta) × e(vk_x, vk.gamma) × e(C, vk.delta) ``` This is a 4-pair pairing check. Using Protocol 26 host functions, it costs approximately 15–20M instructions - within budget for a Soroban transaction. *** ## The Merkle Commitment Tree All commitments are stored in a Poseidon Merkle tree with depth 20 - supporting up to 1,048,576 (2²⁰) positions. The tree root is stored on-chain. When a new commitment is inserted, the root is updated. Every ZK proof that references the tree must match the current root - ensuring ZK proofs are valid only against the current state of the protocol. **Sparse tree:** The tree uses a sparse representation. Only occupied leaves require storage. Empty subtrees are represented by a precomputed empty-subtree hash. **On-chain initial root:** `0x2134e76ac74b4b8765b6e37992aa15f06ff... ` (Poseidon-2 empty tree root - verified on-chain on Stellar testnet) *** ## The Trusted Setup Ceremony Groth16 requires a one-time **trusted setup ceremony** - a multi-party computation event that generates the proving and verification keys for each circuit. The ceremony has two phases: 1. **Powers of Tau (Phase 1):** A universal setup shared across all circuits. The edition is planned to use the Hermez ceremony artifact (the same trusted setup used by Stellar's own Private Payments reference implementation). 2. **Phase 2 (circuit-specific):** A separate ceremony for each of the edition's five circuits, incorporating the circuit-specific parameters. The ceremony requires multiple independent participants. The security guarantee is: as long as at least one participant honestly discards their randomness, the setup is sound. A ceremony with 5+ parties from different organizations and jurisdictions provides strong security. The ceremony transcript is published publicly. Anyone can verify it was executed correctly. **When:** The ceremony belongs to the privacy edition and has no date. It is not on the Writz v1 mainnet path. The current development keys (`pot15` from snarkjs) are used for testing only - they are NOT suitable for production. **Rotating the on-chain key once the real ceremony completes:** `zk-verifier.set_verification_key` (admin-only) replaces a circuit's key immediately - there is no grace period during which the previous key is also accepted. That's a deliberate choice: accepting proofs against a stale key is a strictly weaker security posture than requiring the current one. What the contract does provide is an audit trail - every call emits a `vk_rotated` event carrying a fingerprint of the old and new key, so there's an on-chain record of exactly when the dev key was replaced by the real ceremony's output, even though storage itself only ever holds the current key. *** ## What Gets Stored On-Chain The ZK layer is designed to minimize on-chain storage while preventing double-spending and enabling verifiable state. | Data | Storage type | Lifetime | | - | - | - | | Commitment (leaf hash) | Per-entry persistent | 180-day window (refreshable) | | Nullifier (spent marker) | Per-entry persistent | 180-day window (refreshable) | | Merkle root | Instance persistent | 180-day window (refreshable) | | Verification keys | Instance persistent | Permanent | **TTL management:** Soroban's storage has a time-to-live (TTL) system. Entries that are not accessed expire. The edition's contracts provide permissionless `refresh_*` functions that any keeper can call to extend the TTL of critical entries - ensuring user positions never expire unexpectedly. *** **Back to v1:** [The Stellar Side →](/how-it-works/stellar-side) # SPV Verification Source: https://docs.writz.xyz/how-it-works/spv-verification **How Writz proves a Bitcoin transaction happened - without trusting anyone.** SPV stands for Simplified Payment Verification. It is a technique - described in the original Bitcoin whitepaper by Satoshi Nakamoto - that allows a lightweight client to verify that a transaction is included in the Bitcoin blockchain without downloading the entire chain. Writz implements SPV inside a Soroban smart contract. This means the Stellar blockchain itself can verify Bitcoin transactions cryptographically. No oracle. No bridge operator. No committee. Just math. *** ## Why SPV Works Bitcoin's blockchain is a chain of blocks. Each block has a header that contains: * The hash of the previous block (linking the chain) * A Merkle root - a single hash that commits to every transaction in the block * A nonce and target - proof that miners did computational work to produce this block To prove a transaction is in a block, you need: 1. **The block header** - to verify it has valid proof-of-work 2. **A Merkle proof** - a set of sibling hashes that, combined with your transaction's hash, reproduce the Merkle root in the header 3. **The raw transaction** - to verify the transaction data matches the hash If these three things are consistent - the header has valid PoW, the Merkle proof reconstructs the correct root, and the raw transaction matches - then the transaction is in that block. No full Bitcoin node required. *** ## The Writz Approach: an On-Chain Header Light Client `bitcoin-spv` keeps a compact record of Bitcoin block headers on Stellar, and proves transactions against that record. A caller can no longer hand the contract a header chain it built itself - the earlier "stateless" design accepted exactly that, which is what allowed a privately mined chain to pass as real. How it works: * **One trusted anchor.** The admin sets a checkpoint - a recent real Bitcoin block - exactly once. Every stored header must descend from it. * **Anyone can extend the chain.** `submit_headers` accepts a run of contiguous headers, checks each one (proof-of-work, link to a stored parent, the exact difficulty Bitcoin requires at that height, sane timestamp) and keeps the chain with the most cumulative work. The Writz relayer does this routinely, but any party can. * **Proofs are cheap.** `verify_transaction` names a stored block and checks a Merkle proof against its root. It no longer re-validates a header chain on every call. The trade-offs are honest ones: the contract now holds state that grows with each submitted header, and someone has to keep submitting headers (permissionless, so the system does not depend on a single relayer, but a deposit can only be proven once its block and the blocks above it have been submitted). In exchange, forging a deposit requires real Bitcoin proof-of-work rather than an internally consistent private chain. On signet, where blocks are authenticated by a signature the contract does not verify, header submission is restricted to a configured submitter. See `docs/security/security-model.md` for the full trust model. *** ## What the Contract Verifies `verify_transaction` takes: ```rust theme={null} pub fn verify_transaction( env: Env, block_hash: BytesN<32>, // Hash of the (already stored) block holding the tx merkle_proof: Vec>, // Sibling hashes from tx to block root tx_index: u32, // Transaction's index in the block raw_tx: Bytes, // The raw Bitcoin transaction (non-witness) min_confirmations: u32, // Minimum depth required (default: 6) ) -> SpvVerificationResult ``` (`SpvVerificationResult` is defined once, in the shared `spv-types` crate, and reused by every contract that calls into `bitcoin-spv` - not redefined per contract.) **Header ingestion - `submit_headers(headers)`:** * Each header's `prev_block_hash` must be a header the contract already stores (ultimately the checkpoint) * `SHA256d(header)` must meet the target encoded in `bits` * `bits` must equal what Bitcoin's difficulty rules require at that height: unchanged inside a 2016-block period, the exact retarget at each boundary * The timestamp may not be more than two hours ahead of ledger time * The chain with the most cumulative work becomes the best chain; a heavier fork replaces it **Verification - `verify_transaction`:** * The block must be stored, after the checkpoint, and on the best chain * Its depth below the best tip must be at least `min_confirmations` * `raw_tx` must not be exactly 64 bytes (the size of a Merkle inner-node preimage) * Compute `txid = SHA256d(raw_tx)`, walk the Merkle proof (alternating left/right by `tx_index`), and the result must equal the stored block's `merkle_root` * Return `SpvVerificationResult { txid, block_hash, confirmations }`, where `confirmations` is the block's real depth There is no output-parsing step - the contract does not extract or return the transaction's outputs. A caller that needs to know which output paid a given address parses `raw_tx` itself; see the accuracy note in `docs/developers/spv-sdk.md` for the same point. *** ## SHA256d: Bitcoin's Hash Function Bitcoin uses double-SHA256 (SHA256 applied twice) for all cryptographic operations: block hashes, transaction IDs, and Merkle tree nodes. ``` SHA256d(x) = SHA256(SHA256(x)) ``` Soroban does not natively provide SHA256 as a host function - the `bitcoin-spv` contract implements it in Wasm. This was the primary concern about feasibility: would the compute cost be acceptable? **Benchmarked instruction counts:** | Operation | Instructions | | - | - | | Single SHA256d | \~500,000 | | Header PoW check (1 header) | \~600,000 | | Merkle proof (20 levels) | \~10,000,000 | | Full SPV verify (6 headers + proof) | \~37,000,000 | | Soroban transaction limit | \~100,000,000 | These figures were measured for the earlier design, which validated a header chain inside every verification. With the light client, headers are validated once in `submit_headers` and `verify_transaction` only checks a Merkle proof, so the per-deposit cost is lower; the header-submission costs are being re-measured. *** ## The Relayer Service Header submission is permissionless, so the relayer is not a trust component, but someone has to keep the contract's header chain current and assemble proof bundles for users. The Writz relayer handles both. **What the relayer does:** 1. Watches a Bitcoin Esplora API for transactions to monitored addresses 2. Fetches the raw transaction, the block header it was included in, and sufficient ancestor headers to meet `min_confirmations` 3. Computes the Merkle proof from the block's transaction list 4. Packages everything into a `sorobanArgs` bundle ready to submit to the SPV contract **REST API:** ``` GET /spv-proof/{txid} Response: { "txid": "11932100...", "block_hash": "00000000...", "confirmations": 6, "sorobanArgs": { "headers": [...], "merkleProof": [...], "txIndex": 42, "rawTx": "0200000001..." } } ``` **Fallback:** If the Writz relayer is unavailable, users can assemble the proof bundle themselves from any Bitcoin Esplora instance (Blockstream, mempool.space, or a self-hosted node). The SPV contract accepts any valid proof from any caller. *** ## Security Guarantees **6-confirmation requirement:** Writz requires 6 Bitcoin block confirmations before accepting a deposit. Reorganizations that deep are very rare: the deepest on mainnet are the 2010 value-overflow fix and the 2013 BIP-50 fork. Six confirmations make reversal of an accepted deposit negligible at launch exposure caps. **PoW validation:** Each header's proof-of-work is checked independently. A forged header chain would require generating valid PoW - computationally infeasible against Bitcoin's current hash rate. **Merkle proof soundness:** The Merkle proof check is collision-resistant under SHA256. A fabricated proof would require a SHA256 preimage attack - computationally infeasible. **What SPV does NOT protect against:** * A user sending BTC to the wrong P2WSH address (user error) * A deep reorg affecting more than 6 blocks (extraordinarily unlikely, but theoretically possible on a heavily attacked network) * Bugs in the SPV contract itself (mitigated by audits and extensive testing) For how this trust model compares with wrapped Bitcoin and bridges, including the risks a light client does share with them, see [SPV vs Wrapped Bitcoin](/security/spv-vs-wrapped). *** ## Tested on Real Bitcoin Transactions The SPV contract has been tested against: * Real Bitcoin mainnet transactions (correct `txid` and `block_hash` verified against Python-computed SHA256d) * Multiple block depths (1, 3, 6, 12 confirmations) * Multi-transaction blocks with varying Merkle proof sizes * Edge cases: single-transaction blocks, maximum-size transactions 68/68 tests pass. The contract is deployed on Stellar testnet at `CBX23YKCCJMAMBMSCNP6YVKEXUIYK2CTKSR4TQ3RVBPAFYZFVCW7W4EO`. *** **Next:** [The Stellar Side →](/how-it-works/stellar-side) # The Stellar Side Source: https://docs.writz.xyz/how-it-works/stellar-side **Two contracts, one system - how Writz lives on Soroban.** The Stellar side of Writz v1 consists of two Soroban smart contracts (`bitcoin-spv` and `writz-lend`), Circle's native USDC, the Reflector BTC/USD oracle, and an off-chain relayer that submits Bitcoin headers and co-signs releases. This page walks through each component, how they interact, and the design decisions behind them. Positions are plaintext: amounts, collateral and addresses are public on chain. *** ## The Contracts ``` Reflector oracle USDC (SAC) │ │ └────────┬───────────┘ │ relayer ──► writz-lend ──► bitcoin-spv ◄── relayer (release PSBT) (header submission) ``` ### 1. bitcoin-spv Verifies Bitcoin transactions on Soroban using Simplified Payment Verification. It keeps a compact header chain on Stellar, anchored at a checkpoint, and proves transactions against it (see [SPV Verification](/how-it-works/spv-verification)). `verify_transaction` takes a block hash, a Merkle proof, the transaction index and the raw transaction, and returns a `SpvVerificationResult` (defined once in the shared `spv-types` crate) containing the txid, block hash, block height and confirmation count. It also holds the shared deposit registry: `consume_deposit` records a Bitcoin txid the first time a registered lending contract uses it, so the same output can never back two positions. **Deployed:** `CBX23YKCCJMAMBMSCNP6YVKEXUIYK2CTKSR4TQ3RVBPAFYZFVCW7W4EO` *** ### 2. writz-lend The lending contract. It registers Bitcoin deposits as positions keyed by txid, runs the USDC pool, accrues interest, handles liquidation and stores the co-signed release PSBT. Positions are plaintext. **Key functions** (simplified - `deposit` also takes the full SPV arguments, see `contracts/contracts/writz-lend/src/lib.rs`): ```rust theme={null} // Deposit: rebuilds the Writz P2WSH from the protocol key, user_pubkey and timelock_height, // verifies SPV, checks key_sig, consumes the txid and creates a position keyed by txid. pub fn deposit(depositor: Address, /* SPV proof args */, p2wsh_script_pubkey: Bytes, timelock_height: u32, user_pubkey: BytesN<33>, key_sig: BytesN<65>) -> Result, WritzLendError> // Supply / withdraw USDC to and from the pool (scaled balances that grow with interest). pub fn supply_usdc(supplier: Address, amount: i128) -> Result<(), WritzLendError> pub fn withdraw_supply(supplier: Address, amount: i128) -> Result<(), WritzLendError> // Borrow: checks the 150% collateral ratio at the current oracle price, pool liquidity and the total borrow cap. pub fn borrow(borrower: Address, txid: BytesN<32>, usdc_amount: i128) -> Result<(), WritzLendError> // Repay: reduces outstanding debt. Emits `repay`, or `repay_full` and closes the position when the debt reaches zero. pub fn repay(repayer: Address, txid: BytesN<32>, usdc_amount: i128) -> Result<(), WritzLendError> // Liquidate: the designated keeper, or anyone once the keeper is stale (24h) - checks oracle undercollateralization (< 120%). pub fn liquidate(keeper: Address, txid: BytesN<32>) -> Result<(), WritzLendError> // Relayer only: store the co-signed Path A release PSBT for a repaid position; anyone can read it back. pub fn publish_release_psbt(relayer: Address, txid: BytesN<32>, psbt: Bytes) -> Result<(), WritzLendError> pub fn get_release_psbt(txid: BytesN<32>) -> Option ``` **Security properties:** * The deposit output must be exactly the P2WSH of the Writz script for the protocol key in the contract's config, so BTC the depositor could spend alone can never become collateral * `key_sig` is a BIP-137 signature by `user_pubkey` over a message naming this contract, the depositor and the txid, so nobody can register someone else's deposit * The timelock must lie 1,008..=105,000 blocks above the confirming block * Liquidation pays the liquidator a USDC bonus from the protocol reserve and never moves BTC (GHSA-5rxp-7f9g-r66x) * A pause (`set_paused`) stops new deposits, borrows and supply, never `repay`, `withdraw_supply`, `liquidate` or `publish_release_psbt` **Events emitted** (topic strings, matching `contracts/contracts/writz-lend/src/events.rs` exactly): `deposit`, `borrow`, `repay`, `repay_full` (emitted instead of `repay` when a repayment fully closes the debt), `liquidate`, `supply`, `withdraw`, `paused_set`, `oracle_set`. **Deployed:** `CABF7VK7F25ZGDLV6EWSAU3NYQYKDQNDSJIISDMDZAUASMSXUADSHZGG` *** ## Interest Rate Model Writz uses a **kinked utilization curve** - the same model pioneered by Aave and Compound, adapted for Writz's parameters. ``` utilization = total_borrowed_usdc / total_supplied_usdc if utilization ≤ Uoptimal (75%): borrow_rate = base_rate + (utilization / Uoptimal) × slope1 else: borrow_rate = base_rate + slope1 + ((utilization − Uoptimal) / (1 − Uoptimal)) × slope2 ``` **Parameters:** | Parameter | Value | Rationale | | - | - | - | | `base_rate` | 0% | No charge when pool is empty | | `Uoptimal` | 75% | Target utilization where rates are attractive to both sides | | `slope1` | 8% APR | Gradual increase up to target | | `slope2` | 200% APR | Steep increase above target - strong incentive to repay/supply | | Protocol fee | 15% | Share of interest kept as the protocol reserve; the rest goes to USDC suppliers | **Rate examples:** | Utilization | Borrow APR | Supply APR | | - | - | - | | 0% | 0% | 0% | | 50% | 5.33% | 2.27% | | 75% | 8.00% | 5.10% | | 90% | 128.0% | 97.9% | | 100% | 208.0% | 176.8% | Supply APR = borrow APR x utilization x (1 - 15%), as in `contracts/contracts/writz-lend/src/rates.rs`. Interest accrues continuously. Every position-touching call (borrow, repay, liquidate) applies accrued interest before processing the action. *** ## Oracle Design Writz uses a multi-oracle approach for BTC/USD price feeds, with a **median aggregation** strategy to resist price manipulation. **Primary oracle:** Pyth Network (pull model, verifier contract - real cross-exchange BTC/USD) **Secondary oracle:** Reflector's external-prices instance (SEP-40 interface, `lastprice({"Other":"BTC"})`) - confirmed live on testnet with a real, updating BTC/USD price *(Corrected 2026-09-16, then re-verified by directly calling both contracts on testnet - see `docs/research/oracle-design.md`. RedStone was dropped: it doesn't list Stellar among its supported chains. DIA was parked: its documented testnet contract address returns "contract not found" when queried.)* **Staleness check:** Price data older than 60 minutes is rejected (corrected - previously stated 90 seconds; see `docs/research/oracle-design.md` for rationale). At full design maturity, borrowing and liquidation pause when both oracles are stale. **Interim status (2026-09-17):** only Reflector is wired into `writz-lend` today - see `contracts/contracts/writz-lend/src/oracle.rs`. There is no median yet, so "both oracles stale" does not apply; a stale or unavailable Reflector price alone currently blocks the calling operation (`OraclePriceStale`/`OraclePriceUnavailable`) rather than silently mispricing. Do not treat this as the finished manipulation-resistance story until Pyth is also wired - see `docs/security/security-model.md` for the same caveat. **Manipulation resistance:** * Median of two oracles: a single oracle manipulation requires moving the median * Liquidation smoothing (design): large liquidations could be executed in parts to blunt single-block oracle manipulation; not implemented in `writz-lend` today **SEP-40 interface:** `writz-lend` calls a real Reflector oracle live over the Stellar SEP-40 standard interface (`contracts/contracts/writz-lend/src/oracle.rs`); (The privacy edition's `commitment-tree` still uses a hardcoded stub; it is not part of v1.) For `writz-lend`, switching oracle providers requires only updating the oracle contract address - no changes to lending logic. *** ## Storage and TTL Management Soroban's storage has a time-to-live (TTL) system. Every persistent entry has an expiration point; an entry past it is archived, not deleted. An archived entry cannot be read or overwritten until it is restored, unchanged, which the RPC's simulation does automatically for any entry a transaction touches (the transaction pays the restore). So an archived position still blocks a second deposit of the same txid, and comes back with its debt when it is next used. The transaction that creates an entry pays rent for its whole window, at the rate the network charges at that moment. The windows for entries created per use are therefore set at deployment: | Entry type | Contract | TTL window | Mainnet | Testnet | | - | - | - | - | - | | Position (by txid) | writz-lend | `entry_ttl_ledgers` | 180 days | 40 days | | Per-lender supply balances | writz-lend | `entry_ttl_ledgers` | 180 days | 40 days | | Published release PSBT (by txid) | writz-lend | `entry_ttl_ledgers` | 180 days | 40 days | | Header and height index (two per block) | bitcoin-spv | `header_ttl_ledgers` | 90 days | 7 days | | Consumed deposit txid | bitcoin-spv | `consumed_ttl_ledgers` | 90 days | 7 days | | Config / Protocol accounting | writz-lend | fixed | 90 days | 90 days | | Config / Checkpoint / best tip | bitcoin-spv | fixed | 90 days | 90 days | The testnet contracts were redeployed with these parameters on 2026-10-25, after testnet rent rose sharply: a deposit was charged about 428 XLM in fees on the previous contracts, which used the mainnet windows, and about 89 XLM with the testnet windows (measured 2026-10-25). Positions on the previous contracts were closed and their BTC released. The constructor arguments are in the [Contract reference](/developers/contract-reference). Testnet runs short windows because its live state is above the network's target size, which prices rent far higher than on mainnet: a 180-day position would cost more resource fee than one transaction can carry. Seven days is the network's minimum persistent TTL, so a 7-day window adds no rent beyond what creating the entry already costs. An entry that is read or rewritten in the last third of its window (at most its last 30 days) is extended back to its full window. The admin can change these windows without a redeploy (`set_entry_ttl` on writz-lend, `set_ttls` on bitcoin-spv, same bounds as the constructors), so they can follow the rent rate. A change applies to entries written or touched after it; existing entries keep their TTL until a call tops them up or refreshes them. **Permissionless refresh:** every entry above has a public `refresh_*` function (or is covered by a contract-wide `refresh_ttl`/`refresh_instance_ttl`) that extends its TTL. Any keeper - including Writz's own keeper, a third-party keeper, or even the user themselves - can call these to prevent expiry. No permission required. This is a genuinely load-bearing safety net, not a nice-to-have: none of these entries auto-renews just from being read by an unrelated call, and an idle deployment (low traffic, or a position nobody touches for months) is exactly the case these functions exist for. ```rust theme={null} // writz-lend pub fn refresh_position_ttl(env: Env, txid: BytesN<32>) -> bool pub fn refresh_supply_balance_ttl(env: Env, lender: Address) -> bool pub fn refresh_protocol_ttl(env: Env) pub fn refresh_release_psbt_ttl(env: Env, txid: BytesN<32>) -> bool // bitcoin-spv pub fn refresh_ttl(env: Env) // extends Config, Checkpoint, the best tip and its header ``` `writz-lend`'s `refresh_release_psbt_ttl` is a recent addition - before it, a published release PSBT could only have its TTL bumped as a side effect of fetching or re-publishing it, with no standalone way to keep it alive. *** ## Events Contract state changes emit structured events using Soroban's `#[contractevent]` annotation. Topic strings below match `events.rs` in each contract exactly - an indexer or the relayer's event watcher must filter on these, not on invented names. | Topic | Contract | When emitted | | - | - | - | | `deposit` | writz-lend | SPV deposit accepted, position created | | `borrow` | writz-lend | USDC loan issued | | `repay` | writz-lend | Loan partially repaid | | `repay_full` | writz-lend | Loan fully repaid, position closed (emitted instead of `repay`) | | `liquidate` | writz-lend | Position liquidated | | `supply` | writz-lend | Lender supplied USDC to the pool | | `withdraw` | writz-lend | Lender withdrew USDC from the pool | | `paused_set` | writz-lend | Admin paused or unpaused new deposits/borrows/supply | | `oracle_set` | writz-lend | Admin changed the oracle address | `supply`/`withdraw` carry `supplier`, `usdc_amount`, and the resulting `total_supplied` - enough to enumerate every lender an off-chain indexer needs, the same way `deposit` events let you enumerate borrowers (see `docs/architecture/contract-migration-runbook.md`). Events are the primary mechanism for the Writz backend to detect loan repayments (`repay_full` on `writz-lend`) and trigger the BTC co-signing release on the Bitcoin side - see `relayer/src/repay-watcher/`. *** ## Contract Interactions: Full Deposit Flow ``` User (browser) │ │ 1. fetch the SPV proof bundle (relayer API or Esplora) │ 2. sign the key-control message with the Bitcoin wallet (BIP-137) │ ▼ writz-lend.deposit(spv_proof, p2wsh_script_pubkey, timelock_height, user_pubkey, key_sig) │ ├── check: p2wsh_script_pubkey == P2WSH(protocol key, user_pubkey, timelock_height) ├──► bitcoin-spv.verify_transaction(block_hash, merkle_proof, tx_index, raw_tx, min_confirmations) │ returns: SpvVerificationResult { txid, block_hash, block_height, confirmations } ├── check: timelock_height within 1,008..=105,000 blocks above block_height ├── check: key_sig is user_pubkey's signature over (contract, depositor, txid) ├──► bitcoin-spv.consume_deposit(writz-lend, txid) (shared registry: one position per output) ├── read: amount paid to the P2WSH output in raw_tx; require >= min_deposit_satoshis │ ├── store: position[txid] = { depositor, btc_satoshis, user_pubkey, timelock_height, debt: 0 } └── emit: DepositEvent { txid, btc_satoshis, timelock_height } On full repayment: writz-lend.repay(...) ──► emit: RepayFullEvent { txid, repayer, p2wsh_script_pubkey } relayer repay watcher ──► build + co-sign Path A PSBT ──► writz-lend.publish_release_psbt(txid, psbt) User ──► writz-lend.get_release_psbt(txid), sign with Bitcoin wallet, broadcast ``` *** ## Privacy edition contracts (in progress, not part of v1) `zk-verifier` and `commitment-tree` are deployed on testnet for the separate, opt-in [privacy edition (in progress)](/how-it-works/privacy-edition). They have their own pool, and `writz-lend` never calls them; the only thing they share with v1 is `bitcoin-spv` and its deposit registry. The summary below records their current design. ### zk-verifier Verifies Groth16 BN254 proofs on Soroban using Protocol X-Ray's BN254 host functions. Stores one verification key per circuit type (`Deposit`, `BorrowRepay`, `Liquidation`). The core verification: ```rust theme={null} pub fn verify_groth16( env: Env, circuit: CircuitType, proof: Groth16Proof, public_signals: Vec, ) -> bool ``` Internally, this: 1. Loads the verification key for the specified circuit 2. Computes `vk_x = Σ (public_signals[i] × vk.ic[i])` using `bn254_g1_msm` 3. Runs a 4-pair `bn254_pairing_check`: `e(A,B) == e(alpha,beta) × e(vk_x,gamma) × e(C,delta)` Returns `true` if the proof is valid, `false` otherwise. Malformed proofs (invalid curve points) cause the host to reject the transaction entirely - the correct security behavior. **Deployed:** `CB5XAP6MF2GVVFATFTVCKVEXUODWWRQP4B3QFDMRTM55REP7N2YMXEUD`\ All three verification keys are set on testnet (Deposit IC=6, BorrowRepay IC=9, Liquidation IC=6). *** ### commitment-tree The privacy edition's lending contract. Manages the Poseidon Merkle commitment tree and all ZK-gated lending operations. This is where positions are created, loans are issued, and repayments are recorded. **Key functions** (simplified signatures - see `privacy/contracts/commitment-tree/src/lib.rs` for the exact ABI, including the full SPV header/Merkle-proof arguments `deposit` also takes): ```rust theme={null} // Deposit: verify SPV, the depositor's Writz script and the ZK proof; create commitment. Returns the new commitment. pub fn deposit(depositor: Address, /* SPV proof args */, user_pubkey: BytesN<33>, timelock_height: u32, zk_proof: Proof, public_signals: Vec>, enc_note: Bytes) -> Result, CommitmentTreeError> // Insert a pending commitment into the Merkle tree, with a proof that the new root is the // old one plus this leaf in the next empty slot (#211). Admin/relayer. Emits `insert_leaf`. pub fn insert_commitment(caller: Address, zk_proof: Proof, public_signals: Vec>) -> Result<(), CommitmentTreeError> // Borrow: ZK proof + oracle price → USDC transferred to borrower. pub fn borrow(borrower: Address, zk_proof: Proof, public_signals: Vec>, enc_note: Bytes) -> Result<(), CommitmentTreeError> // Repay: ZK proof → outstanding debt reduced. pub fn repay(repayer: Address, zk_proof: Proof, public_signals: Vec>, enc_note: Bytes) -> Result<(), CommitmentTreeError> // Liquidate: ZK undercollateral proof → keeper collects proven debt amount. pub fn liquidate(keeper: Address, zk_proof: Proof, public_signals: Vec>) -> Result<(), CommitmentTreeError> // Supply USDC to the pool. pub fn supply_usdc(supplier: Address, amount: i128) -> Result<(), CommitmentTreeError> // Withdraw USDC supply. pub fn withdraw_supply(supplier: Address, amount: i128) -> Result<(), CommitmentTreeError> ``` **Events emitted** (topic strings, matching `privacy/contracts/commitment-tree/src/events.rs` exactly): `deposit`, `insert_leaf`, `borrow`, `repay`, `liquidate`. There is currently no event for `supply_usdc`/`withdraw_supply` - an indexer tracking pool liquidity needs to read `get_pool_state`/`get_supply_balance` directly rather than listen for an event that doesn't exist. **Security properties:** * The `borrow` amount is extracted from the ZK proof's public signal - the caller cannot supply an arbitrary amount * The `repay` amount is recovered from field-negation inversion of the proof's delta signal * The `liquidate` `usdc_debt` is extracted from the proof - the liquidator cannot inflate the debt they claim * Nullifier freshness is checked before any state change - double-spending is impossible * Merkle root must match the current on-chain root - stale proofs are rejected **Deployed:** `CAQBLCAPGXXCZTJDAJLY7E4PTJYGR6CY4YWH4JSJKFXYIHW3GANP6RFM` *** **Next:** [Developer Quick Start →](/developers/quick-start) # Writz Protocol documentation Source: https://docs.writz.xyz/index Guides, architecture, security and developer references for Writz, a lending protocol on Stellar backed by native BTC. Writz is a lending protocol on Stellar backed by native BTC. A Bitcoin Script holds the collateral on Bitcoin itself rather than a company, and it never becomes a wrapped token. Borrow USDC against it, repay on your own schedule, and recover your Bitcoin even if Writz disappears. Live on Stellar testnet. > [!WARNING] > > Writz is on testnet only. The contracts are not audited yet. Never send real BTC, real USDC or a wallet that holds real funds to the testnet app. ## Start here > [card] [What is Writz?](/introduction/what-is-writz) > > The short version, and why it was not possible before. > [card] [How Writz works](/introduction/how-writz-works) > > A loan from deposit to release, in plain language. > [card] [Try the testnet app](/guides/start-here) > > What you need and the order to set it up, with free test coins. ## Use the app > [card] [Borrow USDC against BTC](/guides/borrow) > > Lock test BTC on signet and draw test USDC on Stellar. > [card] [Repay and release your BTC](/guides/repay-and-release) > > Pay back the loan and take your collateral back. > [card] [Lend USDC](/guides/lend) > > Supply USDC to the pool and earn the interest borrowers pay. ## Build on Writz > [card] [Integration guide](/developers/integrate) > > Prove a Bitcoin transaction from your Soroban contract or your app. > [card] [Bitcoin SPV SDK](/developers/spv-sdk) > > The `verify_transaction` reference: signature, errors and the proof route. > [card] [Contract reference](/developers/contract-reference) > > The public functions of the Writz contracts. > [card] [For AI agents](/developers/for-ai-agents) > > Contract IDs, signatures and verified examples in a compact format. ## Understand the design > [card] [Technical architecture](/architecture/technical-overview) > > How the Bitcoin script, the light client and the lending contract fit together. > [card] [Security model](/security/security-model) > > What the protocol protects against, and what it assumes. > [card] [SPV vs wrapped Bitcoin](/security/spv-vs-wrapped) > > Where a light client differs from bridges, and where it shares their risks. > [card] [Research](/research/spv-implementations) > > The studies behind the design: SPV, oracles, rates and liquidation. # How Writz Works Source: https://docs.writz.xyz/introduction/how-writz-works *No technical knowledge required. Read this first.* *** ## The Safe Deposit Box Analogy Imagine a safe deposit box at a bank. The box has two locks - one for you, one for the bank. Neither of you can open it alone under normal conditions. When you want to close your loan, both keys turn and the box opens. But there is also an emergency rule: if the bank disappears, a time-lock eventually lets you open the box alone. Now replace the physical bank with a Bitcoin Script - a program written directly into Bitcoin's blockchain. Replace the physical box with a Bitcoin address. Replace the bank key with the protocol's co-signing key, which signs automatically once the Stellar contract records that your loan is repaid in full. That is the foundation of Writz. Your BTC is secured by rules on Bitcoin itself. No company holds your keys. No federation can collude. If Writz disappears entirely, you can still recover your BTC after a time-lock period - because the emergency rule is written into Bitcoin Script, not controlled by anyone. *** ## The Five Steps ### Step 1 - You lock BTC on Bitcoin Writz generates a unique Bitcoin address for your deposit. It looks like any Bitcoin address: `bc1q...`. You send BTC from your wallet to this address. Under the hood, this address is a P2WSH - a Bitcoin "smart contract" address with two spending conditions encoded directly in Bitcoin: * **Normal path:** Both you and Writz must sign to release the BTC. Writz only co-signs when your USDC loan is repaid. * **Emergency path:** After a time-lock period (loan term + safety buffer), you can spend the BTC alone - no Writz signature needed. Your BTC is on Bitcoin. It never leaves. Nobody else can touch it. ### Step 2 - The deposit is verified on Stellar After your BTC transaction gets 6 confirmations on Bitcoin (about 1 hour), a cryptographic proof is assembled. This proof contains: * The Bitcoin block headers covering your transaction * A Merkle proof showing your transaction is included in a specific block * The raw transaction data This bundle gets submitted to a Soroban smart contract on Stellar called the **bitcoin-spv** contract. The contract runs Bitcoin's own verification logic - double-SHA256 hashing, Merkle proof checking, proof-of-work validation - and confirms that your Bitcoin transaction happened. No oracle. No trusted third party. Pure cryptographic verification. This is called **Simplified Payment Verification** (SPV). The contract acts like a lightweight Bitcoin node. ### Step 3 - Your position is created You sign a short message with the Bitcoin key in the script, proving the deposit is yours, and approve one Stellar transaction. The **writz-lend** contract checks that your BTC is locked under the exact Writz script, asks the **bitcoin-spv** contract to verify the transaction, and records your position: the Bitcoin transaction, the amount of BTC and your Stellar address. ### Step 4 - You borrow USDC You request a USDC loan against your locked BTC. Your loan can be up to 66% of your BTC's current USD value. The **writz-lend** contract prices your BTC with an on-chain BTC/USD oracle, checks that your collateral stays at 150% or more of the loan, and sends the USDC - Circle's native USDC on Stellar - to your Stellar wallet. ### Step 5 - You repay and get your BTC back When you repay the USDC loan plus interest in full, the Writz relayer co-signs the Bitcoin release transaction with the protocol key and publishes it on Stellar. You add your own signature and broadcast it on Bitcoin. Your BTC arrives back in your wallet. *** ## What Is Public Writz Lend positions are plaintext, like in any other lending market. Your Stellar address, your Bitcoin deposit transaction, how much BTC you locked, how much USDC you borrowed and repaid, and whether a position was liquidated are all visible on chain. Privacy is not part of the protocol. It is being designed as a separate, opt-in edition, in progress and on testnet only, that Writz Lend users never touch: see [Privacy edition (in progress)](/how-it-works/privacy-edition). *** ## The Full Picture ``` BITCOIN NETWORK ───────────────────────────────────────── Your Wallet │ │ Send BTC to P2WSH address ▼ P2WSH Script Address ┌──────────────────────────────────┐ │ NORMAL: You + Writz co-sign │ │ EMERGENCY: Timelock → you alone │ └──────────────────────────────────┘ │ │ 6 confirmations (~1 hour) │ ───────────┼───────────────────────────────────── │ STELLAR / SOROBAN │ ▼ writz-lend contract (checks the script, records the position) │ ▼ bitcoin-spv contract (verifies block headers + Merkle proof) │ ▼ Borrow: USDC lands in your Stellar wallet When you repay ──► relayer co-signs, PSBT on Stellar ──► you sign, BTC released ``` *** ## The Core Pieces Are Already Working As of mid-2026: * A real Bitcoin Signet transaction was locked in a P2WSH script and co-signed released: [`11932100`](https://blockstream.info/signet/tx/119321009b2f92dac8f25f6bcddb2ed6a3ae778e8748ec52910cce90742e4098) * `bitcoin-spv` and `writz-lend` are deployed on Stellar testnet and verify Signet deposits against the header chain stored on Stellar. See `contracts/deployments/testnet.md` for the current contract IDs and verified calls, and `docs/developers/runbook.md` for the manual Signet walkthrough. What remains before mainnet is an external audit of the contracts, a formed legal entity and a second price source. No trusted-setup ceremony is on that path. *** **Next:** [Why Stellar, Why Now →](/introduction/why-stellar-why-now)\ **Or dive deeper:** [The Technical Architecture →](/how-it-works/bitcoin-side) # The Problem Source: https://docs.writz.xyz/introduction/the-problem ## Bitcoin in DeFi Requires Trusting Someone With Your BTC Bitcoin is the world's largest crypto asset by market cap. Yet borrowing against it in DeFi almost always means handing it to someone else first: ### Give your BTC to a custodian or a signer set **WBTC** is held by BitGo. One company controls the keys. BitGo can be hacked, regulated, or go bankrupt. In 2023, concerns about custodianship triggered \$1B+ in WBTC redemptions in a single week. **tBTC** uses a threshold signature network. A sufficient coalition of signers could theoretically collude. The trust assumption is distributed, but it is still a trust assumption. **Stacks sBTC** uses a committee of signers to peg and unpeg BTC. The peg mechanism relies on off-chain coordination that must be trusted. In every case, you are trusting humans with your Bitcoin. ### Or leave Bitcoin altogether The alternative is to sell or bridge BTC onto another chain, which turns it into a different asset with a different set of risks: bridge contracts, peg mechanisms and the custody behind them. Every bridge adds a place where the BTC can be lost. *** ## The Market Left 900 Million People Behind An estimated 77% of Bitcoin holders have never participated in DeFi. Not because they don't want yield on their BTC. Because: 1. They don't want to give up custody. 2. They don't want to trust a bridge or a wrapped token. 3. The UX of wrapped BTC across multiple chains is too complex. The BTCfi market grew 28x in 18 months - from $304M to $8.6B TVL between January 2024 and mid-2025. That growth happened despite these barriers. The market is still in early innings because the fundamental problems have never been solved. *** ## What Doesn't Exist Yet There is currently no protocol that is simultaneously: * **Trustless** - BTC secured by Bitcoin Script itself, not by any human or committee * **Native** - real BTC that never leaves Bitcoin, verified on-chain by an SPV light client, with no bridge and no wrapped token * **On Stellar** - where native USDC lives Writz is built to be all three. Writz Lend positions are public on chain, like in any other lending market; privacy is being designed as a separate, opt-in edition, in progress (see [Privacy edition (in progress)](/how-it-works/privacy-edition)). *** **Next:** [How Writz Works →](/introduction/how-writz-works) # What is Writz? Source: https://docs.writz.xyz/introduction/what-is-writz Bitcoin is where wealth lives. It is the foundation - scarce, reliable, and built to hold value through anything. USDC is where wealth moves - the stablecoin that pays invoices, covers expenses, and flows through the real economy. **Writz connects the two on Stellar, without moving your Bitcoin off Bitcoin.** You deposit real Bitcoin. You borrow real USDC. Your BTC stays in a Bitcoin script that you co-own with the protocol, and if Writz ever disappears, a timelock lets you take it back alone. *** ## The Short Version Writz lets Bitcoin holders borrow USDC without: * Giving their BTC to a custodian * Wrapping their BTC into a synthetic token * Trusting a bridge or a federation The BTC stays on Bitcoin, locked by a Bitcoin Script that nobody but the user controls. The USDC arrives on Stellar, native and real. The position lives in the `writz-lend` contract and is public on chain, like any other lending market. *** ## Why This Hasn't Existed Before Two technologies had to converge before Writz was possible: **1. Bitcoin SPV on Soroban.** A Soroban smart contract can now verify that a Bitcoin transaction happened - cryptographically, without trusting anyone - using Simplified Payment Verification. This was technically possible only after Soroban matured enough to handle the compute (Protocol 23, September 2025) and after the right Rust libraries existed to port the verification logic. **2. Native USDC on Stellar.** Circle issues USDC natively on Stellar - not bridged, not synthetic. Writz borrowers receive the same USDC used by banks and fintechs worldwide. The output has real-world utility from day one. Both are production-ready. Writz combines them: native BTC collateral verified on Stellar, borrowed against in native USDC. *** ## What You Can Do With Writz ### Writz Lend *(on testnet today, mainnet planned for Q2 to Q3 2027)* Deposit BTC as collateral. Borrow USDC up to 66% of your BTC's value. Interest accrues at market rates. Repay anytime and get your BTC back. This is a loan against your Bitcoin savings, taken without giving up custody of your Bitcoin. ### Earn *(lender side of Writz Lend, live on testnet)* Lend USDC to the Writz Lend pool and earn the interest Bitcoin borrowers pay. The rate is variable and follows how much of the pool is borrowed, so it is zero while nothing is borrowed. Withdraw your USDC and its interest at any time, up to what is not lent out at that moment. See [Writz Lend, For USDC Lenders](/products/writz-lend#for-usdc-lenders-earn). Writz Lend is the only product today. Features planned for later are listed in [Vision](/roadmap/vision#planned-for-later). *** ## How Writz Compares | | WBTC / tBTC | Stacks sBTC | Blend | **Writz** | | - | - | - | - | - | | Needs a custodian | Yes | Partial | N/A | **No** | | Wraps your BTC | Yes | Yes | N/A | **No** | | Works with real BTC | No | Partial | No | **Yes** | | On Stellar | No | No | Yes | **Yes** | | Native USDC output | No | No | Yes | **Yes** | *** ## The Moat Writz's competitive position compounds with time: * **Technical depth:** A Bitcoin SPV client on Soroban takes 12–18 months to build, audit, and deploy safely. A competitor starting today arrives after Writz is established with real TVL and ecosystem integrations. * **SDK ecosystem lock-in:** The Writz Bitcoin SPV SDK is free for any Stellar protocol. Once wallets and protocols build on it, switching costs compound. Writz becomes load-bearing infrastructure for the entire Stellar ecosystem. *** ## Current Status As of June 2026, Writz has completed Phase 1 - Foundation: * `bitcoin-spv` and `writz-lend` deployed on testnet * 623 tests passing across all modules * Real Bitcoin transactions broadcast and confirmed on Bitcoin Signet The protocol is not yet available on mainnet. Mainnet launch is targeted for Q2 to Q3 2027 (Phase 2), starting with a gated, TVL-capped release of Writz Lend. No trusted-setup ceremony is on that path. **Privacy** is not part of the protocol. It is being designed as a separate, opt-in edition, in progress and on testnet only: see [Privacy edition (in progress)](/how-it-works/privacy-edition). *** **Next:** [How Writz Works →](/introduction/how-writz-works) - A plain-English explanation anyone can follow. # Why Stellar, Why Now Source: https://docs.writz.xyz/introduction/why-stellar-why-now Writz is not bringing Bitcoin to DeFi. Writz is making Bitcoin verifiable on Stellar, where native USDC already lives. That distinction matters. "Bringing Bitcoin to DeFi" is what every wrapped-token protocol tries to do - WBTC, tBTC, sBTC. They all require trusting someone to hold the real BTC. Writz makes the Bitcoin transaction itself verifiable on Stellar, without moving the BTC or trusting anyone. The BTC stays on Bitcoin. Only the proof crosses chains. This was not possible until 2026. Here is why it is possible now - and why Stellar is the right chain. *** ## The Infrastructure That Had to Exist First ### Soroban compute for Bitcoin verification Verifying a Bitcoin transaction on-chain means double-SHA256 over block headers, proof-of-work and difficulty checks, and a Merkle inclusion proof, all inside one Soroban transaction. Soroban's compute budget and Rust toolchain make that practical: `bitcoin-spv` keeps a compact header chain on Stellar and `writz-lend` verifies a deposit against it in a single call. Stellar's Protocol X-Ray (January 2026) and Protocol 26 also added BN254 host functions for Groth16 verification. Writz v1 does not use them; they matter only to the separate [privacy edition (in progress)](/how-it-works/privacy-edition). ### Native USDC - No Bridge Risk Circle issues USDC natively on Stellar. This is not bridged USDC, not a synthetic representation, not a wrapped token. When a Writz user borrows, they receive the same USDC used by cross-border payment networks, fintechs, and financial institutions worldwide. The output of Writz has real-world utility from day one. There is no bridge to break, no peg to maintain, no synthetic to redeem. The USDC simply exists on Stellar because Stellar is one of Circle's native USDC chains. Stellar processes over \$500 million in USDC volume per month. That is real liquidity, not speculative volume. ### Soroban - Production-Grade Smart Contracts Stellar's Soroban smart contract platform reached production grade with Protocol 23 (September 2025), which introduced parallel transaction execution. By the time Writz began building, Soroban had: * A mature Rust SDK with comprehensive documentation * Billions of operations processed on mainnet * Known compute costs and resource limits (benchmarked and published) * A growing ecosystem of DeFi protocols (Blend, Phoenix DEX, and others) Writz's Soroban contracts are written in Rust - the same language as the best Bitcoin SPV library in existence (`summa-tx/bitcoin-spv`). Porting the Bitcoin verification logic to Soroban was a natural fit. ### Protocol 27 - Delegation Infrastructure Protocol 27 (Zipper) introduced `delegate_account_auth`, which lets authority on the Stellar side be distributed across delegated accounts. On the Bitcoin side, Writz plans a 2-of-3 protocol side written into the P2WSH script itself, so no single key can co-sign a release; the signing policy will be published. *** ## Why Not Ethereum? The natural question is: why build on Stellar rather than Ethereum, where the existing BTCfi ecosystem lives? | Factor | Ethereum | Stellar | | - | - | - | | Native USDC | No (bridged) | Yes (Circle native issuance) | | Fees | Variable, often high | Low per transaction | | BTC DeFi competition | Saturated (WBTC, Aave, Compound) | Zero - the category doesn't exist on Stellar | | Regulatory posture | Uncertain | Clear - Stellar SDF has regulatory relationships | Stellar is not the obvious choice. It is the *correct* choice for Writz specifically: a chain with native USDC, low fees, and no trustless native-BTC lending yet. *** ## The First-Mover Window *(Updated 2026-08-04 - see `docs/research/market-landscape.md` for the full competitive picture.)* The BTCfi category on Stellar is not empty anymore: Solv Protocol runs an audited, custodial wrapped-BTC deployment with real trading volume, and Templar Protocol has a live, audited Soroban vault plus native-BTC-without-wrapping lending tech already proven on NEAR. Neither offers what Writz does: **trustless, SPV-verified native BTC collateral that never leaves Bitcoin, with a timelock exit the user controls alone.** That is still open on Stellar today. The window for a from-scratch entrant to replicate that combination is 12 to 18 months wide - building a Bitcoin SPV client on Soroban, getting audited, and launching mainnet is not a weekend project. But for Templar specifically, that window is materially shorter: their Stellar infrastructure and native-BTC tech already exist separately, so connecting them is integration work, not a from-scratch build. Testnet-to-mainnet speed is a real competitive variable against Templar, not just a generic risk. A well-funded team starting today would arrive in early-to-mid 2028 - by which time Writz will have: * Established TVL and organic user growth * An open-source SPV SDK integrated into Stellar wallets and protocols * A completed external audit with zero open critical findings * A community that has been using the protocol for over a year The first protocol to make Bitcoin natively verifiable on Stellar will hold that position for years - not because of lock-in mechanics, but because infrastructure compounds. Every wallet that integrates the Writz SPV SDK, every protocol that builds on it, every user who has a working position: these are switching costs that accumulate with each passing month. *** ## The Timing Argument in One Sentence The compute budget for on-chain Bitcoin verification was confirmed feasible. The Rust SPV library exists. Native USDC is live. No protocol on Stellar offers trustless native BTC collateral yet - but Templar Protocol already has the adjacent infrastructure live and in active development. The window is open right now, and it will not stay open forever. *** **Next:** [Writz Lend - the flagship product →](/products/writz-lend) # Writz Lend Source: https://docs.writz.xyz/products/writz-lend **Deposit Bitcoin. Borrow USDC. Take your BTC back alone if Writz ever disappears.** Writz Lend is the lending product of Writz Protocol. Bitcoin is the collateral and USDC is the borrowed asset. The BTC never leaves Bitcoin: it is locked in a script that you co-own with the protocol, and the `writz-lend` contract on Stellar recognizes it through `bitcoin-spv`, an on-chain Bitcoin header light client. There is no bridge, no custodian and no wrapped BTC. Positions are plaintext. Your collateral amount, debt, health ratio, Stellar address and Bitcoin deposit transaction are public on chain, like in any other lending market. Privacy is not part of the protocol; it is being designed as a separate, opt-in [privacy edition (in progress)](/how-it-works/privacy-edition) that Writz Lend users never touch. *** ## What Writz Lend Does You lock BTC in a P2WSH script on Bitcoin and register it with `writz-lend` using an SPV proof. You borrow up to 66.7% of its USD value in USDC. When you repay in full, the protocol co-signs the release and you take your BTC back with your own signature. If the protocol ever stops co-signing, the script's timelock lets you spend the BTC alone. Interest accrues continuously at a variable rate determined by how much of the pool is currently borrowed (utilization). Repay at any time, in full or in part. *** ## Key Parameters These are the values set by the `writz-lend` constructor today. | Parameter | Value | | - | - | | Minimum collateral ratio | 150% (BTC value must be 1.5x the USDC borrowed) | | Maximum LTV | 66.7% (you can borrow up to 2/3 of your BTC's value) | | Liquidation threshold | 120% (position is eligible for liquidation below this ratio) | | Liquidation bonus | 10% of the debt, paid to the liquidator in USDC from the protocol reserve | | Minimum deposit | 0.001 BTC (100,000 sats); 0.00005 BTC (5,000 sats) on testnet | | Minimum confirmations | 6 Bitcoin blocks (\~60 minutes); 1 on testnet | | Timelock window | 1,008 to 105,000 blocks above the deposit block (about 7 days to 2 years) | | Total borrow cap | 50,000 USDC across the pool (admin-set) | | Protocol fee | 15% of interest, kept as the protocol reserve | | Price source | Reflector BTC/USD oracle (single source today) | *** ## Interest Rate Model The borrow rate adjusts dynamically based on how much of the USDC pool is being borrowed. ``` Optimal utilization: 75% Base borrow rate: 0% Slope 1 (0 to 75%): rises linearly to 8% APR at 75% utilization Slope 2 (75 to 100%): adds up to 200% APR, reaching 208% APR at 100% Protocol fee: 15% of interest Supply rate: borrow rate x utilization x 0.85 ``` **What this means in practice** (from `contracts/contracts/writz-lend/src/rates.rs`): * At 50% utilization: \~5.3% APR borrow rate, \~2.3% APR supply rate * At 75% utilization: 8% APR borrow rate, \~5.1% APR supply rate * Above 75%: rates rise steeply to incentivize repayment and new USDC supply The model is designed to keep utilization near 75% - the point where lenders earn competitive yield and borrowers pay reasonable rates. *** ## Step-by-Step: Borrowing Against Your BTC ### Prerequisites * A Bitcoin wallet (Xverse recommended) * A Stellar wallet (Freighter, Lobstr, or any wallet compatible with Stellar Wallets Kit) * BTC to deposit (minimum 0.001 BTC; 0.00005 BTC on testnet, which one Signet faucet payout covers) * A small amount of XLM for Stellar transaction fees (\~1 XLM) You don't need to understand SPV or Merkle proofs to use Writz Lend. The steps below tell you what to click and what to expect; each step also has an optional "How this works" note for anyone who wants the technical detail. For a click-by-click walkthrough with screenshots, including wallet setup and test funds, see the [Guides](/guides/start-here). ### Step 1 - Connect Your Wallets Open the Writz app at [testnet.writz.xyz](https://testnet.writz.xyz). Connect your Bitcoin wallet (Xverse, on Bitcoin signet) and your Stellar wallet (Freighter, Lobstr, or a Google, GitHub or email sign-in on testnet). The wallet connections happen in your browser - no private keys leave your device. ### Step 2 - Choose Your Deposit Amount Select how much BTC you want to deposit as collateral. Writz shows you the maximum USDC you can borrow at the current BTC price (up to 66.7% of the BTC value in USD), and generates a Bitcoin deposit address for you to send to.
How this works technically The generated address is a P2WSH (Pay-to-Witness-Script-Hash) address. Its script holds your Bitcoin public key, the protocol's co-signing key and a CLTV timelock height - see [Bitcoin Side](/how-it-works/bitcoin-side) for the full script design.
### Step 3 - Send BTC to the Deposit Address Send the BTC to the generated address from your Bitcoin wallet. Double-check the address before sending - this is a standard Bitcoin transaction. Wait for 6 confirmations (\~60 minutes; 1 on testnet). The Writz app shows a live confirmation count while you wait. ### Step 4 - Register the Deposit Once your transaction has the confirmations it needs, the app asks your Bitcoin wallet to sign a short message and your Stellar wallet to approve one transaction. That transaction registers the deposit with `writz-lend`, and your position appears in the app.
How this works technically The app fetches an SPV proof (the block hash, the Merkle inclusion path and the raw transaction) from the relayer and calls `writz-lend.deposit`. The contract: 1. Rebuilds the Writz redeem script from the protocol key in its config, your Bitcoin public key and the timelock height, and requires the deposit output to be exactly that P2WSH. An output you could spend alone, or someone else's output, cannot match. 2. Calls `bitcoin-spv.verify_transaction`, which checks proof of work, Merkle inclusion and 6 confirmations against the header chain stored on Stellar. No oracle and no third party vouches for the deposit. 3. Requires the timelock to lie 1,008 to 105,000 blocks above the block that confirmed the deposit. 4. Checks the signed message (a standard Bitcoin "sign message" signature by the key in the script, bound to this contract, your Stellar address and the txid), so nobody watching Bitcoin can register your deposit as their own. 5. Consumes the txid in `bitcoin-spv`'s shared registry, so the same output can never back two positions, reads the deposited amount from the raw transaction and records the position, keyed by the Bitcoin txid. See [SPV Verification](/how-it-works/spv-verification) for the full mechanics.
### Step 5 - Borrow USDC Choose how much USDC to borrow (up to 66.7% of your BTC's current value). USDC arrives in your Stellar wallet within seconds of the Soroban transaction confirming.
How this works technically `writz-lend.borrow` accrues interest on the position, prices the collateral with the Reflector BTC/USD oracle and refuses any borrow that would leave the collateral ratio below 150%, that exceeds the pool's available liquidity, or that would take total borrowing above the 50,000 USDC cap. Only the Stellar address that registered the deposit can borrow against it.
### Step 6 - Monitor Your Health Factor Your position has a **health factor** - the ratio of your collateral value to your debt. Above 1.5 you are safe. Below 1.2, your position is eligible for liquidation. The contract exposes it as `get_health_ratio_bp`, and the app shows it on your position. Watch the BTC/USD price. If BTC drops significantly, repay part of your loan to restore your health factor. ### Step 7 - Repay Repay your USDC loan plus accrued interest at any time. Partial repayment is supported. A repayment that clears the whole debt closes the position. ### Step 8 - Release Your BTC After a full repayment, the protocol co-signs the transaction that returns your BTC, and the app asks your Bitcoin wallet to sign and broadcast it. Your BTC arrives in your wallet once the transaction confirms on Bitcoin.
How this works technically A full repayment emits a `repay_full` event. The relayer's repay watcher sees it, rebuilds your redeem script from the position, builds the cooperative-path (Path A) release PSBT paying your key's own address, co-signs it with the protocol key and publishes it on `writz-lend` with `publish_release_psbt`. Because the half-signed PSBT is stored on Stellar, you can fetch it with `get_release_psbt` and finish it even if the rest of the Writz stack is down. Your wallet adds your signature and broadcasts.
### If Writz Stops Co-signing: the Timelock Exit If the protocol never co-signs (for example, if Writz disappears), the script's second path lets you spend the BTC with your key alone once the CLTV timelock height is reached. No Writz service is needed. See [Manual Emergency Recovery](/how-it-works/manual-emergency-recovery). *** ## Liquidation If your health factor drops below 1.2 (collateral ratio below 120%), your position becomes eligible for liquidation. **How liquidation works today:** `writz-lend.liquidate` can be called by a designated keeper address. If the keeper has not liquidated or sent a heartbeat for 24 hours, any caller may liquidate an undercollateralized position instead, so a stalled keeper cannot block liquidations. Who calls does not change the check: the contract recomputes the health ratio from the oracle price and refuses a healthy position. The liquidator pays the full outstanding USDC debt and receives a 10% USDC bonus from the protocol reserve (funded by 15% of interest). If the reserve holds less than the bonus, the liquidator gets what the reserve holds. The position is marked liquidated and its debt is cleared. **No BTC moves in a liquidation today.** The protocol never takes or sells your BTC, and it does not hand BTC to the liquidator. The relayer only co-signs a release after a full repayment, so a liquidated position's BTC stays in its script until the timelock height, when the key in the script can spend it alone. The protocol's exposure from this is bounded by the 50,000 USDC total borrow cap. **Protecting yourself from liquidation:** * Keep your health factor above 1.5 - the app color-codes it (green above 150%, amber 120 to 150%, red below 120%) * Watch the BTC/USD price * Maintain USDC to repay quickly if needed * Borrow conservatively - taking 50% LTV instead of 66.7% gives you a significant buffer *** ## For USDC Lenders: Earn Earn on Writz means lending USDC to the Writz Lend pool. The USDC lenders supply is what Bitcoin borrowers draw against, and the interest those borrowers pay is the lenders' yield. Nothing else is in between: no vault, no strategy, no second protocol. On testnet, use the **Lend** tab at [testnet.writz.xyz](https://testnet.writz.xyz), or call the contract directly. **How the yield works** * You supply USDC with `supply_usdc(supplier, amount)`. The pool records your deposit as scaled units, so your balance grows as the supply index grows with every interest payment. `get_supply_balance(lender)` returns your balance with the interest included. * The supply rate follows utilization: `supply rate = borrow rate x utilization x 0.85` (see the interest rate model above). The 15% that is not passed to lenders is the protocol reserve, which pays liquidation bonuses. * The rate is variable and can be zero. With nothing borrowed, nobody pays interest and lenders earn nothing; with very little borrowed, `get_supply_rate_bp()` still reads `0` because the rate is counted in whole basis points. Read it live. Writz never promises an APY. **Withdrawing** * `withdraw_supply(supplier, amount)` returns up to your balance, principal plus interest. There is no lockup and no fee on deposits or withdrawals. * You can only withdraw USDC that is not lent out at that moment. If borrowers hold most of the pool, a withdrawal above the free liquidity fails (`InsufficientLiquidity`) until borrowers repay or new lenders supply. High utilization raises the borrow rate steeply above 75%, which is what pulls that liquidity back. **What you are exposed to** * Your USDC funds loans backed by BTC at a 150% minimum collateral ratio, eligible for liquidation below 120%. A liquidation repays the full debt to the pool in USDC; the liquidator is paid from the protocol reserve, not with BTC (see Liquidation above). A position that nobody liquidates stays borrowed, and the pool's total exposure is bounded by the 50,000 USDC borrow cap. * Collateral is priced by a single oracle today (Reflector BTC/USD), and the contracts are not audited yet. Writz lending is on testnet only, with test USDC that has no value. | Lender call | What it does | Signed by | | - | - | - | | `supply_usdc(supplier, amount)` | Moves `amount` USDC stroops from the supplier into the pool | The supplier | | `withdraw_supply(supplier, amount)` | Returns `amount` USDC stroops, up to the balance and the free liquidity | The supplier | | `get_supply_balance(lender)` | The lender's balance with interest, in stroops (7 decimals) | Read only | | `get_supply_rate_bp()` | The current annual supply rate in basis points (`500` is 5%) | Read only | Code for a web page, a bot or the CLI is in [Build on Writz](/developers/integrate#7-earn-yield-on-usdc-as-a-lender). *** ## Mainnet Writz Lend is targeted for mainnet in Q2 to Q3 2027, starting with conservative parameters: * **Borrow cap:** starting low and raised progressively (the contract's cap is 50,000 USDC today) * **Open access under caps:** open to outside lenders and borrowers, with no whitelist, under a global and a per-borrower limit * **Protocol fee:** the launch value is not decided yet (the contract charges 15% of interest today) The date is a target, not a commitment. It depends on gates that are not yet closed, including a formed legal entity, an external audit of the contracts, a second price source cross-checking Reflector, and Bitcoin collateral that answers for a default. No trusted-setup ceremony is on the v1 mainnet path. None of this blocks using Writz Lend on testnet today. *** **Deeper reading:** [The Bitcoin Side →](/how-it-works/bitcoin-side) · [The Stellar Side →](/how-it-works/stellar-side) # Research: Bitcoin Locking Script Design Source: https://docs.writz.xyz/research/bitcoin-locking-script **Author:** Research **Date:** 2026-06-22 **Status:** Complete **Roadmap task:** Phase 0.4 > **Updated 2026-10-26.** Parts of this June 2026 research are superseded. The output type for mainnet v1 stays P2WSH with an absolute-height CLTV (`OP_CHECKLOCKTIMEVERIFY`, heights only). The move to Taproot with a MuSig2 key path is withdrawn until threshold Schnorr signing is supported by the launch wallets and signers. The planned script change is a 2-of-3 protocol side written as miniscript, `wsh(and_v(v:pk(U),or_i(multi(2,K1,K2,K3),after(H))))`, so policy-based hardware wallets can sign it; today's deployed script is not miniscript and those devices cannot sign it yet. The SPV contract is an on-chain header light client, not the stateless design recommended below. The sections below are kept as the original research, with these points corrected in place. *** ## Overview When a user deposits BTC into Writz Protocol, that BTC must be provably locked on the Bitcoin side - the user cannot move it until the loan is repaid. This document designs the Bitcoin-side locking mechanism, evaluating P2WSH vs. Taproot approaches and defining the exact script logic. *** ## The Core Problem Bitcoin has no native way to "freeze" funds based on activity on another chain. Writz needs a mechanism where: 1. BTC is deposited to a specific Bitcoin address 2. That BTC **cannot be moved** without the protocol's co-signature (until loan is repaid) 3. The user has a **fallback recovery path** if the protocol becomes unavailable 4. The locking is **verifiable on Stellar** via SPV proof The solution is a **2-of-2 multi-signature with a timelock escape hatch**, encoded as a Bitcoin script. *** ## Option A: P2WSH (Pay-to-Witness-Script-Hash) **Chosen for mainnet v1** (decision of 2026-10-26) P2WSH locks funds to the hash of a custom redeem script, with spending data carried in the Witness field rather than ScriptSig. Lower witness weight = lower transaction fees when spending. ### Script Design **Redeem script:** ``` OP_IF OP_CHECKSIGVERIFY OP_CHECKSIG OP_ELSE OP_CHECKLOCKTIMEVERIFY OP_DROP OP_CHECKSIG OP_ENDIF ``` **Spending path A (normal - loan repaid):** * Branch: `OP_IF` (push `1` to unlock) * Requires: protocol signature + user signature * Triggered: when user repays USDC loan and Writz co-signs the BTC release **Spending path B (emergency - timelock expiry):** * Branch: `OP_ELSE` (push `0` to unlock) * Requires: only user signature + block height ≥ timelock * Triggered: after a predefined locktime if Writz becomes unavailable * Protects user from permanent loss if the protocol disappears ### Timelock value selection The timelock must be long enough that: * The loan has time to be repaid normally * The protocol can resolve issues before the escape hatch opens * But not so long that the user's capital is locked for an unreasonable period **Recommended timelock formula:** ``` timelock = loan_origination_block + (loan_duration_blocks) + safety_buffer_blocks For a 30-day loan at 10-min Bitcoin blocks: loan_duration = 30 × 24 × 6 = 4,320 blocks safety_buffer = 7 × 24 × 6 = 1,008 blocks (7 days) total_timelock = deposit_block + 5,328 blocks (~37 days from deposit) ``` This means if a user deposits and takes a 30-day loan, they retain the emergency option to recover their BTC after \~37 days even if Writz is completely offline. **Implementation:** Use `OP_CHECKLOCKTIMEVERIFY` (CLTV) with an absolute block height, not a relative timelock. This avoids complexity when the loan term is extended or renewed. ### P2WSH address generation Each deposit gets a unique P2WSH address derived from: ``` redeem_script = build_script(protocol_pubkey, user_pubkey, timelock) script_hash = SHA256(redeem_script) p2wsh_address = bech32_encode("bc", [0x00, script_hash]) ``` The `timelock` is unique per deposit (based on current block height + loan duration). The `user_pubkey` is the user's Bitcoin public key. The `protocol_pubkey` is Writz Protocol's co-signing key. ### On-chain footprint * **Output:** 34 bytes (version byte + 32-byte script hash) * **Spending (path A, normal):** \~250 bytes (user sig + protocol sig + redeem script in witness) * **Spending (path B, emergency):** \~180 bytes (user sig + redeem script in witness) * **Fee impact:** Witness bytes are discounted by 4x in Bitcoin's fee calculation - P2WSH spends are economically efficient *** ## Option B: Taproot / P2TR (Pay-to-Taproot) **Not planned.** Withdrawn on 2026-10-26 until threshold Schnorr signing exists in the launch wallets and protocol signers (see the reasons under "Why P2WSH, and not Taproot" below). Taproot (BIP 341, active since November 2021) is a significant privacy and efficiency upgrade to Bitcoin scripting. It uses Schnorr signatures and Merkle trees (MAST - Merkelized Abstract Syntax Tree) to hide spending conditions until they are used. ### How Taproot improves on P2WSH for Writz | Property | P2WSH | Taproot (P2TR) | | - | - | - | | **Normal spend appearance** | Clearly a multi-sig (reveals script) | Looks like single-key payment (key-path spend) | | **Script privacy** | Redeem script revealed when spent | Unused script branches never revealed | | **Fee efficiency (normal path)** | \~250 bytes | \~57 bytes (key-path spend with MuSig2) | | **Complex script support** | Full Bitcoin Script | Tapscript (similar, slightly extended) | | **Hardware wallet support** | Wide for standard templates; a custom script needs a miniscript form for policy-based devices (today's Writz script is not miniscript, a miniscript form is planned) | Script-path and MuSig2 key-path signing limited | | **MuSig2 compatibility** | No | Yes - aggregates protocol + user key into one key | ### Taproot design for Writz **Key-path spend (normal - loan repaid):** Using MuSig2, the protocol key and user key are **aggregated into a single tweaked public key**. When the loan is repaid, both parties cooperatively produce a single Schnorr signature that unlocks the funds. On-chain this looks identical to a standard single-key Bitcoin payment - no evidence that it was a 2-of-2 multi-sig or a bridge deposit. **Script-path spend (emergency - timelock):** The timelock recovery script is embedded in a Tapscript leaf, committed to in the Taproot output. If the key-path spend is unavailable (Writz offline), the user reveals the leaf script and spends using only their key after the timelock. **Privacy benefit:** Chain surveillance firms cannot identify Writz deposits from normal Bitcoin payments in the common case. Only the emergency path reveals the script structure. ### Why P2WSH, and not Taproot * MuSig2 for the key-path requires interactive signing protocols (nonce exchange) between the user and the Writz backend. This adds complexity to the UX flow (two round trips instead of one). * Hardware wallet support for Taproot key-path with MuSig2 is still limited in 2026. * P2WSH is well-audited and simpler to implement correctly, and a P2WSH miniscript can be signed by the launch wallets today. * The planned 2-of-3 protocol side does not fit a MuSig2 key path (MuSig2 is n-of-n); it would need FROST or nested aggregation, which no launch wallet or custody option supports. `multi(2,...)` in P2WSH works with ECDSA signers now. * Verifying a Taproot output on Soroban would need BIP-340 Schnorr verification or secp256k1 point arithmetic, which Soroban does not provide as host functions; a P2WSH check is one SHA-256 comparison, already built. * The security properties are equivalent - only the on-chain privacy and fee efficiency differ. *** ## Protocol Co-Signing Key Architecture ### The problem Writz holds a co-signing private key for the `protocol_pubkey` in every locking script. Every spending path also requires the user's key, so a compromised protocol key cannot move locked BTC on its own, but it is a single point of failure for cooperative releases, and any future path in which the protocol side completes a transaction the user pre-signed (such as liquidation) raises its value to an attacker. ### Solutions (in order of security) **Planned: 2-of-3 protocol side in the script** Instead of splitting one key with MPC, the script itself requires the user's key and any 2 of 3 protocol keys, `wsh(and_v(v:pk(U),or_i(multi(2,K1,K2,K3),after(H))))`, held separately. Bitcoin enforces the threshold natively, each protocol key signs plain ECDSA, and the user's timelock exit is unchanged. Threshold ECDSA (MPC, for example GG20/21 or CGGMP21) is not planned. The signing policy, what the protocol signers can and cannot sign, will be published. **Considered: Stellar Auth Delegation (Protocol 27)** Protocol 27 (July 8, 2026) enables distributing the co-signing authority on the Stellar side across multiple delegated accounts. While this doesn't directly affect the Bitcoin signing key, it enables a multi-party signing ceremony where multiple Stellar accounts must authorize a BTC co-signature before it's produced. **Only if Taproot is reopened: FROST (Flexible Round-Optimized Schnorr Threshold)** If Writz ever moves to Taproot, FROST enables threshold Schnorr signatures where t-of-n parties cooperate to sign. More efficient than ECDSA MPC. Produces a single Schnorr signature indistinguishable from a regular single-key spend. *** ## zkRelay Alternative: ZK-SNARK Bitcoin Relay Research uncovered an interesting alternative to verifying every header on chain: **zkRelay** - using ZK-SNARKs to batch-verify multiple Bitcoin block headers off-chain and post a single proof on-chain. ### zkRelay approach Instead of the caller providing raw headers + Merkle proofs, a prover computes a ZK proof that: * "I have validated N consecutive Bitcoin block headers" * "Each header has valid PoW" * "Transaction T is included in block at height H" The on-chain verifier only checks the ZK proof - a constant-cost operation regardless of how many headers were validated off-chain. **Cost reduction achieved in research:** 187x cheaper than BTC Relay's per-header approach when batching 504 headers. ### Why this matters for Writz Writz's SPV contract is an on-chain header light client, the same family as BTC Relay: each header is submitted and verified on Stellar. zkRelay's approach could reduce the on-chain cost of the header chain, or serve as an independent second check of it. **Recommendation (original, superseded):** this research recommended starting with stateless SPV (the summa-tx approach). The contract has since moved to a header light client, because a stateless design lets a caller present a header chain it mined privately. A zero-knowledge proof of the header chain remains under research as a second check that could only refuse a tip, never credit value (see [SPV vs Wrapped Bitcoin](/security/spv-vs-wrapped)). *** ## Bitcoin Reorg Handling A Bitcoin block reorganization ("reorg") occurs when the network switches to a longer chain, invalidating previously confirmed blocks. If Writz accepts a deposit based on a block that later gets reorged out, the BTC effectively disappears. ### Risk assessment * Reorgs of 1–2 blocks: happen occasionally (\~once per few days) * Reorgs of 3 blocks: extremely rare (a few times in Bitcoin's history) * Reorgs of 6 blocks or more: very rare; the deepest on mainnet are the 2010 value-overflow fix (about 53 blocks) and the 2013 BIP-50 fork (24 blocks) **Writz policy:** Require **6 confirmations** before accepting a deposit. This makes reorg risk negligibly small. The tradeoff is 60 minutes of user wait time - addressed in the UX section. ### Fast lane option - considered, not adopted For users willing to pay a higher fee and accept slightly elevated risk: * Require 3 confirmations (\~30 minutes) * Cap the maximum deposit size to \$5,000 at 3-confirmation tier * Standard deposits (>\$5,000) always require 6 confirmations **Status:** This option was evaluated but is not part of the current product spec. `docs/products/writz-lend.md`, `docs/how-it-works/spv-verification.md`, and `docs/how-it-works/stellar-side.md` all specify a flat 6-confirmation requirement with no fast lane, and the contract's `min_confirmations` parameter is set by the caller with no fast-lane logic in `bitcoin-spv`. If the fast lane is revisited, update those three docs together with this one. *** ## Complete Deposit-to-Repayment Flow ``` DEPOSIT: 1. User requests a deposit address from Writz UI 2. Writz generates unique P2WSH address: script = IF (writz_key + user_key) ELSE (timelock + user_key) address = P2WSH(SHA256(script)) 3. Writz stores: (address, user_pubkey, timelock, expected_deposit_amount) 4. User sends BTC to the P2WSH address 5. After 6 Bitcoin confirmations, Writz UI prompts user to submit SPV proof 6. User (or Writz UI) submits proof to Soroban SPV contract 7. SPV contract verifies and signals Writz Lend contract 8. Writz Lend creates ZK position commitment, allows USDC borrowing REPAYMENT (writz-lend's plaintext flow - automated): 1. User repays USDC + interest via Writz Lend contract 2. Writz Lend contract emits a repay_full event 3. relayer/src/repay-watcher polls Soroban RPC for this event (persisted cursor, survives relayer restarts) 4. The watcher co-signs a Bitcoin transaction spending path A: input: the P2WSH UTXO output: user's Bitcoin return address (derived from the on-chain user_pubkey) witness (once the user also signs): [user_sig, writz_sig, redeem_script] 5. The half-signed PSBT is published on-chain via publish_release_psbt - retrievable even if the Writz frontend is down 6. User retrieves the PSBT, adds their own signature, and broadcasts it 7. BTC arrives in user's wallet (standard Bitcoin transaction) REPAYMENT (commitment-tree's ZK flow - still manual): cosigning requires the user's own private witness to construct a zero-debt proof, which the backend does not have. The user submits this proof via the existing /api/cosign route themselves. EMERGENCY RECOVERY (if Writz offline): 1. User waits for timelock to expire (current block ≥ timelock_value) 2. User broadcasts a Bitcoin transaction spending path B: witness: [user_sig, 0x00, redeem_script] (0x00 selects ELSE branch) 3. BTC returned to user without Writz involvement ``` *** ## Key Decisions Summary | Decision | Choice | Rationale | | - | - | - | | Script type | P2WSH with absolute CLTV for mainnet v1; a 2-of-3 miniscript form planned; P2TR withdrawn | Signer support for P2WSH miniscript today; 2-of-3 does not fit MuSig2; Soroban can check P2WSH with one SHA-256 comparison | | Timelock | CLTV absolute block height | Simpler than relative timelocks, predictable | | Timelock duration | Loan term + 7 days safety buffer | Protects users without locking capital indefinitely | | Protocol signing | 2-of-3 protocol keys in the script (planned), with a published signing policy | Threshold enforced natively by Bitcoin Script rather than MPC; no single key can co-sign a release | | Confirmation requirement | 6 (fast lane considered, not adopted) | 6 makes reorg risk negligible at launch caps | | SPV type | On-chain header light client (original recommendation, stateless, superseded) | A stateless design lets a caller present a privately mined header chain | | Future direction | Zero-knowledge header-chain proof, under research | A second check that can only refuse a tip, never credit value | *** *Last updated: 2026-06-22* *Sources: [Bitcoin P2WSH - learnmeabitcoin.com](https://learnmeabitcoin.com/technical/script/p2wsh/) · [Taproot Technical - learnmeabitcoin.com](https://learnmeabitcoin.com/technical/upgrades/taproot/) · [zkRelay paper - eprint.iacr.org](https://eprint.iacr.org/2020/433) · [SmartCustody Timelocks - BlockchainCommons](https://github.com/BlockchainCommons/SmartCustody/blob/master/Docs/Timelocks.md)* # Research: Stellar USDC Liquidity & Blend Ecosystem Source: https://docs.writz.xyz/research/blend-usdc-integration **Author:** Research **Date:** 2026-06-22 **Status:** Complete *** ## Overview The supply side of Writz Lend - USDC lenders who provide the capital that borrowers draw against - depends on Stellar's existing USDC ecosystem. This document maps the USDC liquidity landscape on Stellar and evaluates how Writz fits into and potentially integrates with existing protocols. *** ## USDC on Stellar - State of Play (2026) | Metric | Value | | - | - | | USDC monthly volume on Stellar | \$500M+ | | USDC issuer | Circle (native issuance - not bridged) | | Largest USDC protocol | Blend (\$80M+ TVL as of early 2026) | | Network operations (Q3 2025) | 1 billion+ | | Typical transaction finality | 3–5 seconds | | Typical transaction fee | \< \$0.001 | **Critical point:** Stellar USDC is natively issued by Circle - not a bridged or wrapped version. This means: * No bridge risk on the USDC side * Regulatory compliance built-in (Circle's USDC is fully regulated) * Stellar USDC is the same USDC used in MoneyGram, payments integrations, and enterprise contexts * Users borrowing USDC from Writz get a real, liquid, institutional-grade asset *** ## Blend Protocol - The Primary Competitor and Reference **Repository:** [blend-capital/blend-contracts-v2](https://github.com/blend-capital/blend-contracts-v2) **TVL:** \$80M+ (early 2026) **Built on:** Soroban ### What Blend is Blend is Stellar's Aave equivalent - a lending protocol primitive that allows anyone to create isolated lending pools. It supports: * Pool creation by anyone (permissionless) * Per-pool interest rates and collateral factors * A unique backstop module (insurance against bad debt) * Auto-compounding vaults built on top ### Blend's limitation: No BTC, No Privacy Blend currently supports Stellar native assets and SAC tokens (Stellar Asset Contract). It does **not** support: * Bitcoin as collateral (no BTC on Stellar today) * Zero-knowledge position privacy **This is the gap Writz fills.** ### Blend as a co-existence opportunity Rather than competing with Blend for USDC liquidity, Writz can potentially co-exist: * **Short term:** Writz operates its own isolated USDC pools (independent from Blend) * **Medium term:** Writz could integrate with Blend pools as a source of USDC liquidity for borrowers, acting as a BTC-collateral gateway * **Long term:** If Blend adds SPV verification support, Writz's open SDK provides the primitive **Why independent pools first:** The ZK privacy requirement means position state must be managed within Writz's own contracts. Blend's architecture doesn't accommodate ZK-private positions without significant modification. *** ## Oracle Integration: SEP-40 **Correction (2026-09-16, on-chain verified):** this section previously claimed "Blend is already integrating RedStone for price feeds." That was wrong - Blend uses **Reflector**, confirmed via Stellar's own oracle-providers documentation. A follow-up correction also turned out to be wrong: Reflector's *Stellar-assets* oracle instance (testnet `CAVLP5...`) has no plain BTC, but its separate *external CEXs & DEXs* instance (testnet `CCYOZJCOPG34LLQQ7N24YXBM7LL62R7ONMZ3G6WZAAYPB5OYKOMJRN63`) does - confirmed by directly calling `assets()`/`lastprice()` on testnet, see `docs/research/oracle-design.md`. Writz can share the same oracle standard, and now plausibly the same specific BTC feed, that Blend's ecosystem already runs on. * Writz should build against the **SEP-40 standard**, the same interface Blend's oracle (Reflector) also speaks - provider-agnostic by design, so this part of the original point holds * Reflector's external-prices instance lists a plain `{"Other":"BTC"}` asset with a live, updating price (confirmed on-chain 2026-09-16: \~\$76,185, 5-minute resolution) - see `docs/research/oracle-design.md` for the full verified recommendation (Pyth primary, Reflector secondary; DIA parked - its documented testnet contract doesn't exist on-chain) * Consistent oracle standards (SEP-40) across the Stellar DeFi ecosystem still reduce the attack surface for oracle manipulation, independent of which specific provider is used *** ## USDC Liquidity Bootstrap Strategy The chicken-and-egg problem: USDC lenders won't supply unless there are BTC borrowers. BTC borrowers won't deposit unless there's USDC to borrow. ### Phase 2 Bootstrap Approaches **1. Protocol-owned liquidity (POL):** Writz can use initial funding (from SDF grants, SCF, or equity) to seed the USDC pool with protocol-owned capital. This provides initial liquidity before organic lender supply. Target seed: $50,000–$100,000 USDC from protocol treasury to bootstrap initial pool. **2. High initial supply APY:** Set initial protocol fee to 0% temporarily, routing 100% of interest to USDC suppliers. This creates above-market yields (e.g., 10–15% APY during bootstrap) that attract USDC lenders. **3. Target Stellar DeFi native users:** Stellar users who already hold USDC are the easiest to convert - they don't need to bridge anything. Target them via Stellar wallet integrations (Lobstr, Freighter), Stellar Discord communities, and integration with Stellar DeFi newsletters. **4. Institutional lenders:** Post-FTX, institutional crypto funds are looking for compliant, audited DeFi yield. Writz's compliance-friendly privacy (ASPs, audit trail available) and Circle's native USDC make it attractive for institutions. Direct outreach to crypto-native family offices and funds. *** ## USDC Pool Architecture in Writz Writz's USDC pools are separate from Blend's pools. Each pool has: | Component | Description | | - | - | | **Pool contract** | Soroban contract managing supply and withdrawal | | **Receipt tokens** | Users receive `wUSDC` (Writz USDC) representing their pool share | | **Interest accrual** | Continuous, per-ledger accrual | | **Withdrawal queue** | If utilization > 95%, withdrawal requests are queued until liquidity frees up | ### Multiple pool tiers (future consideration) For Phase 2+, Writz could offer multiple USDC pool tiers with different risk/reward profiles: | Pool | Collateral ratio | Interest rate | Target user | | - | - | - | - | | **Conservative** | 200% min collateral | Lower rates | Risk-averse lenders | | **Standard** | 150% min collateral | Standard rates | General market | | **Aggressive** | 130% min collateral | Higher rates | Yield-focused lenders | *** ## Key Findings 1. **Stellar USDC is ideal** - native issuance by Circle, \$500M/month volume, real institutional liquidity 2. **Blend is complementary, not a competitor** - Blend has no BTC and no privacy; Writz fills the gap 3. **SEP-40 is the oracle standard** - align with the broader Stellar DeFi ecosystem; see `docs/research/oracle-design.md` for the corrected provider choice (Pyth + Reflector, not RedStone - Blend itself uses Reflector, not RedStone; DIA was parked, not chosen) 4. **Protocol-owned liquidity is the best bootstrap mechanism** - use initial grants to seed the pool 5. **Institutional USDC lenders are a realistic target** - compliance-friendly privacy attracts institutional capital 6. **Independent pools required** - ZK position privacy cannot be retrofitted onto Blend's architecture *** *Last updated: 2026-06-22* *Sources: [Blend Protocol Introduction](https://medium.com/script3/introducing-blend-95aaf66bdf41) · [Blend Contracts v2](https://github.com/blend-capital/blend-contracts-v2) · [Stellar Oracle Providers](https://developers.stellar.org/docs/data/oracles/oracle-providers) · [Stellar DeFi Overview](https://stellar.org/blog/ecosystem/what-the-defi-is-happening-on-stellar)* # Research: Circom Circuit Design for Writz Protocol Source: https://docs.writz.xyz/research/circom-circuit-design **Author:** Research **Date:** 2026-06-22 **Status:** Complete *** ## Overview Writz requires three distinct ZK circuits: 1. **Deposit circuit** - prove BTC was deposited and create a private position commitment 2. **Borrow/repay circuit** - prove the state transition of a position without revealing amounts 3. **Liquidation circuit** - prove a position is undercollateralized without revealing amounts This document designs each circuit's structure, constraints, and inputs/outputs, drawing from the Stellar Private Payments reference implementation. *** ## Chosen Stack: Circom + Groth16 + BN254 **Why Circom:** * Most mature ZK circuit language in production (Tornado Cash, Zcash Sapling, Stellar Private Payments) * Compiles to R1CS (Rank-1 Constraint System) - directly compatible with Groth16 * Large library of reusable components (Poseidon hash, Merkle proofs, comparators, bit decomposition) * Browser-based proof generation via snarkjs + WebAssembly **Why Groth16:** * Constant-size proofs: 192 bytes regardless of circuit complexity * Fastest verification: single bilinear pairing check * Production-proven on Soroban via Protocol X-Ray (BN254 pairing host function) * Stellar Private Payments uses exactly this stack - direct reference available **Why BN254:** * Protocol 25 added BN254 host functions specifically * Protocol 26 added BN254 MSM and scalar arithmetic host functions * Most Circom tooling targets BN254 * Same curve as Ethereum EIP-196/197 - large ecosystem of audited tooling **Tradeoff acknowledged:** Groth16 requires a trusted setup ceremony per circuit. This is a one-time per-circuit event. Must be done carefully (Powers of Tau + circuit-specific setup). After setup, verification is fully trustless. *** ## Shared Components ### Poseidon Hash (ZK-friendly) All commitments use Poseidon instead of SHA256. Poseidon operates over the BN254 scalar field, making it 10–100x more efficient in Wasm circuits. ```circom theme={null} include "poseidon.circom"; // from circomlib template Hash2() { signal input a; signal input b; signal output out; component h = Poseidon(2); h.inputs[0] <== a; h.inputs[1] <== b; out <== h.out; } ``` ### Merkle Tree Inclusion Proof All position commitments are stored in an on-chain Merkle tree. ZK proofs must prove membership in this tree without revealing which leaf. ```circom theme={null} template MerkleProof(depth) { signal input leaf; signal input path_elements[depth]; signal input path_indices[depth]; // 0 = left, 1 = right signal output root; component hashers[depth]; signal nodes[depth + 1]; nodes[0] <== leaf; for (var i = 0; i < depth; i++) { hashers[i] = Hash2(); // Select order based on path index hashers[i].a <== path_indices[i] == 0 ? nodes[i] : path_elements[i]; hashers[i].b <== path_indices[i] == 0 ? path_elements[i] : nodes[i]; nodes[i + 1] <== hashers[i].out; } root <== nodes[depth]; } ``` **Merkle tree depth:** Use depth = 20 → supports up to 2^20 = \~1 million simultaneous positions. Each additional depth level adds \~1–2 Poseidon hashes to the circuit. *** ## Circuit 1: Deposit ### Purpose Prove that a valid BTC deposit has been made via SPV and create a private commitment to the position. ### Note on separation The SPV verification (Bitcoin Merkle proof + header chain) is done in a **separate Soroban contract**, not in this Circom circuit. SPV is verified on-chain using the Rust SPV library. The Circom circuit only handles the ZK privacy layer - creating the commitment. ### Inputs/Outputs ```circom theme={null} template DepositCircuit() { // Private inputs signal private input collateral_satoshis; // BTC amount deposited (hidden) signal private input secret; // User's position secret (hidden) signal private input nonce; // Unique nonce (hidden) // Public inputs signal input btc_txid; // Bitcoin transaction ID (visible) signal input expected_satoshis; // Minimum deposit amount (visible, for validation) // Outputs (public) signal output commitment; // Poseidon(collateral, 0, secret, nonce) // Note: debt starts at 0 at deposit time } template DepositCircuit() { // Constraints // 1. Collateral matches the claimed amount collateral_satoshis >= expected_satoshis === 1; // 2. Commitment correctly encodes the position commitment <== Poseidon(4)([ collateral_satoshis, 0, // initial debt = 0 secret, nonce ]); } ``` **On-chain flow:** 1. Soroban SPV contract verifies BTC transaction → emits: `verified(txid, amount_satoshis, recipient_p2wsh_address)` 2. Writz Lend reads SPV verification result 3. User submits ZK deposit proof with `commitment` as public output 4. Writz Lend adds `commitment` to the Merkle tree 5. User stores `(collateral_satoshis, 0, secret, nonce, commitment_index)` privately ### Constraint count estimate Poseidon(4): \~240 constraints. Range check: \~32 constraints. **Total: \~280 constraints.** Very light circuit. *** ## Circuit 2: Borrow / Repay (State Transition) ### Purpose Prove that a position's debt is being correctly updated (borrowing more USDC or repaying) without revealing the actual amounts. ### Inputs/Outputs ```circom theme={null} template BorrowRepayCircuit(DEPTH) { // Private inputs (never revealed) signal private input collateral_satoshis; signal private input old_debt; signal private input new_debt; signal private input secret; signal private input nonce; signal private input merkle_path[DEPTH]; signal private input commitment_index; // Public inputs signal input old_merkle_root; signal input new_merkle_root; signal input delta_usdc; // positive = borrow, negative = repay (public) signal input btc_price; // current oracle price signal input min_collateral_ratio; // 150% = 15000 basis points signal input nullifier; // prevents replay of this state transition // Constraints // 1. Old commitment exists in the tree old_commitment <== Poseidon(4)([collateral_satoshis, old_debt, secret, nonce]); old_merkle_root === MerkleProof(DEPTH)(old_commitment, commitment_index, merkle_path); // 2. New debt is correctly computed new_debt === old_debt + delta_usdc; // 3. New debt does not exceed collateral ratio // (collateral_satoshis × btc_price / 1e8) × 10000 / new_debt >= min_collateral_ratio collateral_value_usd <== collateral_satoshis × btc_price \ 100_000_000; ratio <== collateral_value_usd × 10000 \ new_debt; ratio >= min_collateral_ratio === 1; // 4. New debt is non-negative new_debt >= 0 === 1; // 5. New commitment correctly encodes updated position new_commitment <== Poseidon(4)([collateral_satoshis, new_debt, secret, nonce]); // 6. New Merkle root reflects the commitment update new_merkle_root === UpdatedMerkleRoot(DEPTH)( old_commitment, new_commitment, commitment_index, merkle_path ); // 7. Nullifier prevents replay nullifier === Poseidon(2)([secret, commitment_index]); } ``` ### Constraint count estimate * Old commitment: \~240 constraints * Merkle proof (depth 20): \~20 × 240 = \~4,800 constraints * New commitment: \~240 constraints * Merkle root update: \~4,800 constraints * Range checks + arithmetic: \~200 constraints * Division for ratio: \~300 constraints (expensive in ZK) * **Total: \~10,500 constraints** For Groth16, \~10K constraints is a medium-sized circuit - proof generation should take under 5 seconds in WASM. *(This section is the original Phase 0 estimate, kept for historical context. The built circuit's real count, after an `is_borrow` soundness fix added a boolean constraint and two debt-direction range checks, is \~11,180 non-linear constraints - see `docs/how-it-works/privacy-edition.md` for the measured figure and `privacy/circuits/src/borrow_repay.circom` for the source of truth.)* *** ## Circuit 3: Liquidation ### Purpose Prove that a specific position (identified by its commitment) is undercollateralized, without revealing the collateral amount or position owner, enabling a liquidator to claim the collateral. > **Note (updated to match the shipped circuit):** the pseudocode below described an earlier design where `usdc_to_repay` was supplied as a separate **public input**, checked for equality against the private debt field. The circuit as implemented (`privacy/circuits/src/liquidation.circom`) is stronger: it has no `usdc_to_repay` input at all. `usdc_debt` is instead a circuit **output**, structurally bound to the private commitment (`usdc_debt <== debt_stroops`). This is a meaningful difference - see "Privacy tradeoff" below. ### Inputs/Outputs (as implemented) ```circom theme={null} template LiquidationCircuit(DEPTH) { // Private inputs signal input collateral_satoshis; signal input debt_stroops; signal input secret; signal input nonce; signal input path_elements[DEPTH]; signal input path_indices[DEPTH]; // Public inputs signal input merkle_root; signal input btc_price_stroops_per_btc; signal input liquidation_threshold_bp; // 12_000 = 120% // Public outputs signal output nullifier; // Poseidon(secret, nonce) signal output usdc_debt; // bound to debt_stroops - not caller-supplied // Constraints // 1. Commitment exists in tree commitment <== Poseidon(4)([collateral_satoshis, debt_stroops, secret, nonce]); merkle_root === MerkleTreeChecker(DEPTH)(commitment, path_elements, path_indices); // 2. Position IS undercollateralized (cross-multiplied, no division) // collateral_satoshis × price × 10_000 < debt × 100_000_000 × threshold_bp GreaterThan(128)(debt_stroops × 100_000_000 × liquidation_threshold_bp, collateral_satoshis × btc_price_stroops_per_btc × 10_000) === 1; // 3. Debt output is bound to the commitment's private debt field - a // keeper cannot claim an arbitrary amount while proving a different // commitment. usdc_debt <== debt_stroops; // 4. Nullifier nullifier <== Poseidon(2)([secret, nonce]); } ``` ### Privacy tradeoff in liquidation There's a fundamental tension in private liquidations: the liquidator (or the contract, on the liquidator's behalf) needs to know how much USDC to pay to execute the liquidation. This means the debt amount is published on-chain at liquidation time regardless of circuit design - that part of the tradeoff is inherent and cannot be engineered away while liquidation remains permissionless. What the circuit design *can* control is whether that published amount is **trustworthy** - i.e. whether a keeper (or anyone else constructing the liquidation call) could claim a different amount than what the position actually owes. The original pseudocode above modeled `usdc_to_repay` as a public input checked for equality against the private debt - functionally adequate, but it leaves the "declare the correct amount" burden on an external equality constraint, callable with any input. **The shipped circuit removes that input entirely**: `usdc_debt` is a circuit *output*, computed as `usdc_debt <== debt_stroops` directly from the private commitment. A malicious keeper cannot supply a different value - there is no signal for them to lie in. `privacy/contracts/commitment-tree/src/lib.rs` extracts `usdc_debt` from the proof's output signal, never from a caller-supplied parameter, so this guarantee is enforced on-chain as well as in the circuit. **Phase 1 status:** debt amount is revealed on-chain at liquidation time (unavoidable given a permissionless/keeper-based liquidator model), but the amount is provably correct - see `docs/how-it-works/privacy-edition.md`. **Phase 2 options (blocked on a decentralized keeper network):** 1. **Encrypted debt in commitment** - liquidator receives an encrypted hint containing the debt amount, decryptable only with a specific key. 2. **Committed-but-unrevealed liquidation bids** - multiple competing keepers commit to a bid without revealing it, settled without exposing the amount to losing bidders. Neither of these has a consumer today: a hidden-bid or encrypted-hint scheme only makes sense once there are multiple competing keepers to hide the amount *from*, and that decentralized keeper network is itself Phase 2 and not yet built. Designing this crypto now, before that network exists, would be speculative work against a documented, accepted, non-blocking tradeoff - revisit once that network exists. *** ## Trusted Setup Ceremony Groth16 requires a per-circuit trusted setup. The setup has two phases: **Phase 1 (Powers of Tau):** A multi-party ceremony generating universal SRS (Structured Reference String) parameters. Stellar Private Payments uses the Hermez Powers of Tau ceremony - Writz can reuse this. Already done. **Phase 2 (Circuit-specific):** A circuit-specific setup generating the proving key and verification key. This must be done separately for each of Writz's **four** circuits (the three named throughout this doc, plus `zero_debt`, which gates the cooperative Path A release endpoint and is fund-loss-equivalent in severity to the others). It is a one-time event per circuit version. **Ceremony requirements for production:** * Minimum 5 independent participants (more is better) * At least one participant must be trustworthy (the setup is secure if at least one person destroys their randomness) * Publicly verifiable transcript * Can be done by the Writz team + community members + security researchers This is a **pre-mainnet requirement**. The ceremony must be completed and the results audited before any mainnet deployment. **Tooling:** the coordinator/participant scripts, transcript format, verification, and on-chain rotation runbook implementing all of the above are in `privacy/circuits/scripts/ceremony/` - see that directory's `README.md` for the exact step-by-step process. A CI job (`circuits-ceremony-verify` in `.github/workflows/ci.yml`) mechanically checks the committed manifest's hashes and rejects any transcript with a "dev"-labeled participant before a rotation is accepted; it does not replace the human verification steps in the runbook. *** ## Browser-Side Proof Generation Proof generation runs on the client side via WebAssembly. Users generate their own ZK proofs in the browser before submitting to Soroban. **Proof generation time benchmarks (Groth16, snarkjs in WASM):** * Deposit circuit (\~280 constraints): \< 1 second * Borrow/Repay circuit (\~11,200 constraints): 3–8 seconds * Liquidation circuit (\~9,000 constraints): 2–6 seconds This is acceptable UX for DeFi operations (users already wait for Bitcoin confirmations). **Proof generation artifacts needed:** * `.wasm` file (circuit compiled to WebAssembly) * `.zkey` file (proving key from trusted setup) * Both served from Writz frontend / CDN *** ## Key Findings 1. **Three circuits needed:** Deposit (light), Borrow/Repay (medium), Liquidation (medium) 2. **Circom + Groth16 on BN254 is the right stack** - production-proven on Stellar 3. **Merkle tree depth 20** - supports 1M positions, adds \~4,800 constraints per circuit 4. **Division in ZK is expensive** - collateral ratio checks require range proofs; benchmark carefully 5. **Trusted setup is required** - one-time ceremony per circuit; pre-mainnet mandatory item 6. **Liquidation privacy is partially limited** - debt amount revealed in Phase 1; keeper-based approach mitigates 7. **Browser-side proof generation is viable** - 3–8 second proof time is acceptable for DeFi *** *Last updated: 2026-06-22* *Sources: [Circom Documentation](https://docs.circom.io) · [NethermindEth/stellar-private-payments](https://github.com/NethermindEth/stellar-private-payments) · [ShieldLend Architecture](https://github.com/cryptosingheth/shieldlend/blob/main/docs/architecture.md) · [RareSkills: Intro to ZK Circuits with Circom](https://rareskills.io/post/circom-intro)* # Research: Interest Rate Model Source: https://docs.writz.xyz/research/interest-rate-model **Author:** Research **Date:** 2026-06-22 **Status:** Complete *** ## Overview Writz Lend's interest rate model determines how much borrowers pay and how much lenders earn. A well-calibrated model keeps liquidity available, prevents extreme rate volatility, and ensures the protocol remains solvent under stress. This document analyzes production models from Aave v3, Compound v2, and Blend, then proposes Writz's specific parameters. *** ## The Utilization Rate All major lending protocols drive rates from a single variable: **utilization rate (U)**. ``` U = total_borrowed_USDC / total_supplied_USDC ``` * U = 0%: No one is borrowing. Lenders earn nothing. * U = 80%: Healthy target. Good yield for lenders, reasonable cost for borrowers. * U = 100%: All liquidity is borrowed. No withdrawals possible. Crisis state. The model's job is to automatically adjust rates so that U gravitates toward a target (usually 70–90%) through price signals. *** ## The Kinked (Two-Slope) Interest Rate Model Aave v3, Compound v2, and Blend all use a **piecewise linear model** - often called the "kinked" model - with two distinct slopes separated by an optimal utilization point. ``` Borrow Rate │ slope2 → ████│ ← steep rise │ ████ │ ██████ │ ██████ slope1 → ████│ ██████ │ ██████ │ ██████ base → █│███ └────────────────────────────────── Utilization 0% Uoptimal 100% (80%) ``` ### Formula ``` if U ≤ Uoptimal: borrow_rate = base_rate + (U / Uoptimal) × slope1 if U > Uoptimal: borrow_rate = base_rate + slope1 + ((U - Uoptimal) / (1 - Uoptimal)) × slope2 supply_rate = borrow_rate × U × (1 - protocol_fee) ``` ### Parameter comparison across protocols | Parameter | Aave v3 (WBTC) | Compound v2 | Blend (Stellar) | **Writz (proposed)** | | - | - | - | - | - | | Base rate | 0% | 2% | 0% | **0%** | | Uoptimal | 45% | 80% | 80% | **75%** | | Slope 1 (below optimal) | 7% | 15% | 8% | **8%** | | Slope 2 (above optimal) | 300% | 200% | 150% | **200%** | | Protocol fee | 10% | 20% | 10% | **15%** | ### Why Writz's parameters differ from standard **Uoptimal = 75% (not 80%):** BTC-collateralized loans carry higher liquidation risk than stablecoin loans. In a BTC price crash, multiple positions may need liquidation simultaneously, requiring liquidity to be available. A lower Uoptimal ensures more USDC is always available for withdrawals during a crisis. **Slope 2 = 200%:** An aggressive slope 2 is essential. At 100% utilization, USDC cannot be withdrawn. The steep slope 2 creates a powerful price signal to repay loans or supply more USDC before the system is illiquid. At U=90%, the borrow rate would be: 0% + 8% + ((90%-75%)/(25%)) × 200% = 8% + 120% = 128% APR. Nobody stays borrowed at 128% - the slope works. **Protocol fee = 15%:** Higher than Aave's 10%, justified by Writz's additional privacy infrastructure costs (ZK proof verification per transaction). The remaining 85% of interest goes to USDC lenders. *** ## Example Rate Scenarios | Utilization | Borrow Rate | Supply Rate (after 15% fee) | | - | - | - | | 0% | 0% | 0% | | 25% | 2.67% | 0.57% | | 50% | 5.33% | 2.27% | | 75% (optimal) | 8% | 5.1% | | 80% | 48% | 32.6% | | 90% | 128% | 97.9% | | 95% | 168% | 134.7% | The jump from 75% → 80% creates a powerful incentive (8% → 48%) that strongly discourages the pool from going above the optimal utilization point. *** ## Interest Accrual Mechanism ### Continuous accrual Interest accrues continuously on outstanding loans, calculated per Stellar ledger close (\~5 seconds). This avoids the "interest cliff" problem where large amounts accrue at discrete intervals. ### Implementation ```rust theme={null} // Per-ledger interest calculation let blocks_elapsed = current_ledger - last_update_ledger; let ledger_rate = annual_borrow_rate / (365 * 24 * 720); // 720 ledgers per hour let interest_accrued = outstanding_debt × ledger_rate × blocks_elapsed; debt = outstanding_debt + interest_accrued; ``` ### Compound interest Writz uses compound interest (interest on interest). This matches Aave/Compound behavior and simplifies the accounting model - the outstanding debt grows continuously, and the protocol fee is taken from the spread. *** ## Protocol Revenue Flow ``` USDC supplied by lenders: $1,000,000 Utilization (example): 75% USDC borrowed: $750,000 Annual borrow rate at 75% util: 8% Annual interest generated: $750,000 × 8% = $60,000 Distribution: ├── USDC lenders (85%): $51,000/year → 5.1% APY on supplied capital └── Writz Protocol (15%): $9,000/year → protocol revenue Protocol revenue breakdown: ├── Insurance fund (30%): $2,700 → on-chain safety reserve ├── Operations (50%): $4,500 → operational expenses └── Protocol treasury (20%): $1,800 → future development ``` *** ## Stress Test: BTC Price Crash Scenario **Scenario:** BTC drops 40% in 24 hours. Many positions approach liquidation threshold. 1. Liquidators activate - they need USDC to pay off loans 2. USDC demand spikes → U rises rapidly 3. Borrow rate jumps (e.g., from 8% to 50%+) 4. Existing borrowers are incentivized to repay immediately 5. USDC suppliers see high yields → new suppliers enter the pool 6. U stabilizes below the crisis threshold **Key risk:** If ALL positions become liquidatable simultaneously and the pool is at 75%+ utilization, there may not be enough free USDC for liquidators. **Mitigation:** The 75% Uoptimal (lower than Blend's 80%) ensures a larger free liquidity buffer during normal operations. Additionally, the liquidation bonus (BTC discount for liquidators) creates strong incentive for external liquidators to supply USDC even when the pool is stressed. *** ## Comparison: Blend Integration Option Rather than building a standalone lending contract, Writz could **use Blend as the underlying lending primitive** and add the BTC SPV + ZK privacy layer on top. **Pros:** * Reuse Blend's \$80M+ TVL and audited contracts * Faster time to market * Shared liquidity with the broader Stellar DeFi ecosystem **Cons:** * Blend controls collateral types and parameters - BTC support requires Blend to support it * Privacy layer integration with Blend's architecture would require significant modification * Less control over the economic model * Revenue sharing with Blend protocol **Recommendation:** Build independent Writz Lend for Phase 1. The ZK privacy requirement necessitates deep integration with the position management system - Blend's architecture makes this very difficult to retrofit. Use Blend as inspiration, not as the foundation. *** *Last updated: 2026-06-22* *Sources: [Aave Interest Rate Model - RareSkills](https://rareskills.io/post/aave-interest-rate-model) · [Aave v3 Rate Model Deep Dive - Medium](https://medium.com/@ancilartech/how-aave-calculates-interest-rates-a-deep-dive-into-defis-dynamic-rate-engine-23e75c5f1819) · [Blend Protocol - blend-capital/blend-contracts-v2](https://github.com/blend-capital/blend-contracts-v2)* # Research: Liquidation Mechanism Design Source: https://docs.writz.xyz/research/liquidation-mechanism **Author:** Research **Date:** 2026-06-22 **Status:** Complete *** ## Overview Liquidations are the safety mechanism that keeps Writz Lend solvent. When a user's BTC collateral drops in value and their loan becomes undercollateralized, a liquidation must occur quickly to protect the USDC lenders. The challenge for Writz: **how do you liquidate a private position?** The collateral amount and debt are hidden by ZK proofs. Liquidators can't see who is undercollateralized. This document designs the full liquidation system, including the novel ZK-private liquidation mechanism. *** ## Standard DeFi Liquidation (Public Protocols) In Aave, Compound, and Blend, liquidation works as follows: 1. Any position is publicly visible: collateral amount, debt, health factor 2. When health factor \< 1.0, anyone can call `liquidate(position_id)` 3. The liquidator pays some or all of the debt in USDC 4. The liquidator receives the BTC collateral at a discount (liquidation bonus) 5. The position is closed or partially reduced **Why this is easy in public DeFi:** Every bot can scan every position continuously. When a position becomes liquidatable, multiple liquidators compete (race condition), ensuring rapid liquidation. **Why this breaks for Writz:** Position data is hidden. Liquidators cannot scan positions. *** ## The Private Liquidation Problem Writz stores positions as ZK commitments: ``` commitment = Poseidon(collateral_satoshis, usdc_debt, secret, nonce) ``` On-chain, only the commitment hash is stored - not the collateral or debt amounts. A liquidation bot cannot determine which positions are undercollateralized by reading the chain. ### Solutions #### Option A: Privileged Keeper (Operator) A trusted Writz operator holds the decryption keys for all positions and monitors them privately. When a position becomes liquidatable, the keeper generates a ZK proof and initiates liquidation. **Pros:** Simplest to implement. Users trust the protocol operator. **Cons:** Centralization. If the keeper is offline, positions go unliquidated. If the keeper is malicious, they can selectively trigger or delay liquidations. **Verdict:** Acceptable for Phase 1 (alongside a timelock-based safety mechanism), but must be decentralized by Phase 2. #### Option B: User Self-Reporting When a position becomes liquidatable, the protocol emits no signal. Users must periodically submit ZK proofs proving their position is healthy. If a user fails to prove health within a window, the position is assumed liquidatable. **Pros:** Fully decentralized. No trusted party needed. **Cons:** Terrible UX. Users must monitor and act regularly or lose their collateral. Not viable for mainstream adoption. **Verdict:** Not recommended. #### Option C: ZK Proof of Undercollateralization (Recommended for Phase 2) A keeper (or any party with position knowledge) generates a ZK proof that a specific position is below the liquidation threshold - **without revealing the actual collateral amount or debt**. **ZK proof statement:** ``` I know (commitment_preimage = {collateral, debt, secret, nonce}) such that: 1. Poseidon(collateral, debt, secret, nonce) = commitment [valid commitment] 2. commitment is in the current Merkle tree [position exists] 3. BTC_price × collateral / debt < liquidation_threshold [undercollateralized] 4. nullifier = Poseidon(secret, commitment_index) [unique, prevents replay] ``` The verifier (Soroban contract) checks the ZK proof using the current oracle price. If valid, it triggers the liquidation of the commitment - releasing the BTC co-sign to the liquidator and burning the USDC debt - without ever revealing the collateral amount or who the position belongs to. **Pros:** Decentralized and privacy-preserving. Any party can liquidate if they can prove undercollateralization. **Cons:** The prover must know the position's preimage. This requires the keeper (or the user) to share the position details. In practice, the Writz keeper tracks all positions in encrypted off-chain storage. *** ## Recommended Liquidation Architecture ### Phase 1: Keeper + Emergency Timelock ``` Writz Keeper (off-chain) ├── Stores all position preimages in encrypted database ├── Monitors BTC/USD price continuously ├── When BTC price × collateral / debt < 120%: │ 1. Generates ZK proof of undercollateralization │ 2. Calls Writz Lend.liquidate(zk_proof, liquidator_address) │ 3. Protocol verifies ZK proof on-chain │ 4. Protocol co-signs BTC release to liquidator │ 5. USDC debt is burned └── Emergency fallback: if keeper offline >24 hours, positions can be liquidated by anyone with a valid ZK proof (open liquidation with proof submission) ``` **Emergency fallback mechanism - the two lending contracts differ here, by design:** 1. **`commitment-tree::liquidate`** - Writz's ZK liquidation path - needs no fallback at all. It is fully permissionless from the start: it requires only `keeper.require_auth()` (the caller authorizing their own USDC payment) plus a valid Groth16 undercollateralization proof, with no keeper-address check to fall back from. "Anyone who can submit a valid ZK proof" already describes its normal operating mode, not an emergency-only path. 2. **`writz-lend::liquidate`** - the plaintext (non-ZK) lending contract - restricts liquidation to a designated keeper by default, with a **time-based** fallback: `Config.keeper_stale_after_secs` (default 86,400 = 24h) and `ProtocolState.last_keeper_heartbeat`. If the designated keeper hasn't successfully liquidated or called `keeper_heartbeat` within that window, `liquidate` opens to any caller who can satisfy the (caller-independent) undercollateralization check. See `contracts/contracts/writz-lend/src/lib.rs`. ### Phase 2: Decentralized Keeper Network Multiple keeper nodes compete to submit liquidation proofs. The first valid proof wins. Keepers are incentivized by the liquidation bonus. A stake/bond mechanism ensures keepers don't collude to delay liquidations. *** ## Liquidation Parameters ### Collateralization ratios | Parameter | Value | Rationale | | - | - | - | | **Minimum collateral ratio** | 150% | User can borrow up to 66.7% of BTC value in USDC | | **Liquidation threshold** | 120% | Position liquidated if BTC value drops to 1.2× the debt | | **Liquidation buffer** | 30% | Distance from min ratio to liquidation: safety margin for BTC volatility | | **Liquidation bonus** | 10% | Liquidator receives BTC at 10% below market - their profit | | **Protocol liquidation fee** | 2% | Writz takes 2% of the liquidated collateral value | ### Example liquidation scenario ``` User deposits: 1 BTC at $100,000 → collateral value = $100,000 User borrows: $60,000 USDC (60% LTV, collateral ratio = 167%) Liquidation at: collateral ratio = 120% → BTC price = $72,000 At BTC = $72,000: Collateral value: $72,000 Debt: $60,000 + accrued interest (assume $60,500) Collateral ratio: 72,000 / 60,500 = 119% → LIQUIDATABLE Liquidation: Liquidator pays: $60,500 USDC (full debt) Liquidator gets: $60,500 × 1.10 = $66,550 worth of BTC (10% bonus) Protocol gets: $60,500 × 0.02 = $1,210 worth of BTC (2% fee) User gets back: $72,000 - $66,550 - $1,210 = $4,240 worth of BTC (residual) Liquidator profit: $66,550 - $60,500 = $6,050 (~10%) ``` ### Partial liquidations For large positions, full liquidation in one transaction may not be practical. Writz supports partial liquidations - a liquidator pays some of the debt and receives proportional collateral. **Minimum liquidation amount:** 10% of outstanding debt per liquidation call. This prevents dust liquidations that waste gas without meaningful risk reduction. *** ## Liquidation and the ZK Circuit The liquidation Circom circuit must prove: ```circom theme={null} template LiquidationProof() { // Private inputs (never revealed on-chain) signal private input collateral_satoshis; signal private input usdc_debt; signal private input secret; signal private input nonce; signal private input merkle_path[DEPTH]; signal private input commitment_index; // Public inputs (visible on-chain) signal input btc_price_usd; // from oracle signal input liquidation_threshold; // 120% = 12000 in basis points signal input merkle_root; // current commitment tree root signal input nullifier; // prevents replay // Constraints // 1. Commitment is correctly formed commitment <== Poseidon(collateral_satoshis, usdc_debt, secret, nonce); // 2. Commitment is in the Merkle tree merkle_root <== MerkleProof(commitment, commitment_index, merkle_path); // 3. Position is undercollateralized // collateral_value_usd = collateral_satoshis × btc_price_usd / 100_000_000 // ratio = collateral_value_usd × 10000 / usdc_debt (basis points) // ratio < liquidation_threshold ratio <== (collateral_satoshis × btc_price_usd / 100_000_000 × 10000) / usdc_debt; ratio < liquidation_threshold === 1; // 4. Nullifier is correctly derived (prevents double-liquidation) nullifier <== Poseidon(secret, commitment_index); } ``` **Key circuit challenge:** Division and comparison (`ratio < threshold`) are expensive in ZK circuits because they require range proofs. This will be one of the heavier operations in Writz's circuit design. Benchmark this specifically in Phase 1. *** ## Liquidation UX for Users Users should be notified well before liquidation: * **150% → 140%:** Warning notification (email, in-app) * **140% → 130%:** Urgent notification with one-click repay button * **130% → 120%:** Critical alert - liquidation imminent * **\< 120%:** Keeper initiates liquidation The Writz frontend should show users their health factor in real-time using their locally-stored position secret (the frontend knows the position details even though the chain doesn't). *** *Last updated: 2026-06-22* *Sources: [What is Health Factor in DeFi - Otomato](https://otomato.xyz/blog/what-is-health-factor-defi-lending) · [ZK Lending on Cardano - Catalyst](https://projectcatalyst.io/funds/13/cardano-open-developers/zero-knowledge-privacy-protocol-for-defi-lending-and-borrowing-on-cardano-open-source) · [Aave Liquidation Mechanism](https://docs.aave.com/faq/liquidations)* # Research: Market Landscape - BTCfi + Privacy on Stellar Source: https://docs.writz.xyz/research/market-landscape **Author:** Justin (Business Analyst) **Date:** 2026-06-22 **Status:** Complete - initial survey (updated 2026-08-04: Solv Protocol + Templar Protocol findings - see dated addendum in Competitive Analysis) *** ## BTCfi Market ### Growth trajectory Bitcoin DeFi has been one of the breakout trends of the 2024–2025 cycle: | Date | BTCfi TVL | | - | - | | January 2024 | \$304M | | December 2024 | \$7B+ | | Mid 2025 | \$8.6B+ | That is a **28x growth in 18 months**. The market is expanding fast and still early. ### What is driving growth * Bitcoin holders want to generate yield without selling BTC or giving up custody * Institutional interest in BTC-collateralized lending (cleaner regulatory profile than altcoins) * Maturing cross-chain infrastructure making BTC accessible on other chains * Bitcoin's "digital gold" narrative evolving toward "productive asset" ### Current BTCfi players | Protocol | Chain | Mechanism | Privacy | | - | - | - | - | | WBTC (BitGo) | Ethereum | Custodial bridge | None | | tBTC | Ethereum | Threshold signatures | None | | Aave (WBTC collateral) | Ethereum | Lending | None | | Compound (WBTC collateral) | Ethereum | Lending | None | | Stacks (sBTC) | Stacks L2 | Threshold bridge | None | | RSK (RBTC) | RSK sidechain | Federated peg | None | | Starknet (strkBTC) | Starknet | ZK-powered | Partial | | Interlay (iBTC) | Polkadot | Collateralized vaults | None | | Solv Protocol (SolvBTC) | Ethereum, BNB, Arbitrum, **Stellar (Soroban, live)**, +7 others | Custodial: 3rd-party custodians + FROST threshold-signature reserve network, NAV oracle | None (blacklist/pausable token, built for compliance not privacy) | | Templar Protocol | NEAR (native BTC lending, mainnet) - Soroban vault also live on Stellar today, but XLM-collateralized only | NEAR side: MPC/chain-signature Bitcoin light client, no wrapping. Stellar side: curator vault + Blend adapter (Upshift infra) | None | **Key observation (updated 2026-08-04):** Ethereum and Bitcoin L2/sidechains still dominate BTCfi by volume, but **the "None are on Stellar" claim from the original June survey is no longer accurate** and should not be reused as-is. Two Stellar-relevant players now exist: * **Solv Protocol** already runs an audited SolvBTC deployment on Soroban mainnet with real trading volume ($179K/day Q4 2025 → $372K/day Q1 2026 per Messari). It is fully custodial/wrapped, not privacy-preserving - it doesn't compete with Writz's trust model, but it kills the "we're the only BTCfi thing on Stellar" talking point. * **Templar Protocol** is the sharper risk: it already proved native-BTC lending *without wrapping* on NEAR (via Bitcoin light client + MPC chain signatures, \$4M pre-seed, audited, mainnet), and separately already has a live, Halborn-audited Soroban vault on Stellar (via Blend, currently XLM-collateral only) plus a cross-chain bridge (`omni-sdk`) that explicitly lists Stellar among its supported chains. They have not yet connected native BTC collateral to the Stellar vault - but all the components exist in their own GitHub org today, and their core `contracts` repo is under active daily development. Neither currently offers **ZK-private** BTC-collateralized borrowing on Stellar - that claim still holds. But the clean "no BTCfi exists on Stellar" framing needs to retire; the differentiator has to be privacy + trustless SPV specifically, not first-to-Stellar. *** ## Privacy Market ### Why privacy matters in DeFi Public blockchain DeFi has a fundamental problem: every position, every trade, every liquidation threshold is visible to anyone. This creates: * **Front-running:** Bots watch liquidation thresholds and exploit them * **Competitive intelligence leakage:** Institutional players don't want competitors seeing their positions * **Personal financial exposure:** Individuals don't want their net worth and borrowing behavior public * **Regulatory uncertainty:** Some jurisdictions treat DeFi activity differently based on public visibility ### Privacy in crypto - 2026 state Zero-knowledge proofs have moved from experimental to practical infrastructure: * Proof generation is orders of magnitude faster than 2022 - GPU/FPGA-accelerated provers produce basic proofs in milliseconds * ZK has gone from research tool to production infrastructure (Starknet, zkSync, Aztec, etc.) * The regulatory conversation has shifted toward "selective transparency" - private by default, auditable on request ### Stellar's privacy position Stellar launched **Protocol X-Ray** in January 2026, making it uniquely positioned: | Feature | Detail | | - | - | | **ZK proof verification** | Noir circuit proofs verifiable inside Soroban smart contracts | | **Stellar Private Payments** | Open-source framework: private deposits, transfers, and withdrawals using Groth16 ZK proofs | | **Compliance hooks** | Association Set Providers (ASPs) enable selective disclosure to regulators - "open by default, private when needed" | | **Protocol 24 roadmap** | Confidential assets are the next milestone - Stellar views privacy as a multi-year buildout | Stellar is the only major blockchain with ZK privacy infrastructure that is simultaneously **compliance-friendly**. This is critical for institutional adoption. *** ## Stellar Ecosystem ### Key metrics (2025–2026) * **USDC volume:** \$500M/month on Stellar - the dominant stablecoin, real usage not speculation * **Network operations:** Surpassed 1 billion network operations in Q3 2025 * **Soroban maturity:** Smart contracts moved from early experimentation to production-grade deployments * **RWA tokenization:** Hit \$3B target set by SDF * **Protocol 23 (Whisk, Sep 2025):** Parallel smart contract execution - significantly faster network ### DeFi protocols on Stellar | Protocol | Type | BTC support | Privacy | | - | - | - | - | | Blend | Lending (Aave-style) | No | No | | Stellar DEX | Native AMM | No native BTC | No | | Various AMMs | Soroban-based | No | No | | Solv Protocol (SolvBTC) | Custodial BTC yield token | Yes - wrapped/custodial only | No | | Templar Protocol (Soroban Vault) | Curated lending vault on Blend (via Upshift) | No - XLM/RWA collateral, not BTC (yet) | No | **Gap (revised 2026-08-04):** No protocol on Stellar handles **real, non-custodial BTC** (native UTXO, SPV-verified, no wrapping) as collateral. Solv fills the "some form of BTC exists on Stellar" gap with a fully custodial wrapped token. Templar has live Soroban infrastructure plus proven native-BTC-without-wrapping tech (on NEAR) but has not yet combined the two. The specific gap Writz occupies - trustless SPV-verified native BTC + ZK-private positions, on Stellar - is still open. ### USDC + Stellar = unique combination USDC is the world's most regulated and trusted stablecoin. On Stellar, USDC is natively issued by Circle - not bridged. This means: * No bridge risk on the USDC side * Stellar USDC is the same USDC that businesses already use for payments, remittances, and treasury * Users borrowing USDC on Writz are getting a real, liquid, institutionally recognized asset This combination - real BTC collateral + real USDC output - makes Writz Protocol's value proposition immediately understandable to mainstream financial players. *** ## Competitive Analysis ### Direct competitors to Writz Protocol Nobody is building exactly what Writz is building. The closest analogues are: **On privacy + DeFi:** Aztec Network (Ethereum), Penumbra (Cosmos) - but none handle BTC natively and none are on Stellar. **On BTCfi:** Stacks, RSK, Interlay - but none have ZK privacy and none are on Stellar. **On Stellar DeFi:** Blend - but no BTC support and no privacy. ### Indirect competitors | Competitor | Why they're indirect | Writz advantage | | - | - | - | | WBTC on Aave | Custodial bridge, no privacy, Ethereum fees | Trustless, private, Stellar low fees | | tBTC | Complex threshold bridge, no privacy | Simpler UX, ZK privacy | | Blend | On Stellar, but no BTC | Same ecosystem + BTC + privacy | | Stacks sBTC | Bitcoin-native, but no privacy, no Stellar | ZK privacy, USDC output | | Solv Protocol (SolvBTC) | Live on Stellar, real volume, but fully custodial + no privacy - targets USDC holders wanting BTC yield, not BTC holders wanting private loans | Trustless custody (user's own P2WSH), ZK-private positions, opposite flow direction | | Templar Protocol | Native-BTC-no-wrap tech proven on NEAR + live Soroban vault infra on Stellar already - closest thing to a credible fast-follower if they connect the two | Writz keeps BTC in the user's own script and verifies it with a light client on Stellar, where Templar relies on MPC threshold custody; the execution gap is smaller for Templar than for a from-scratch entrant | ### The competitive moat Writz's moat is **technical first-mover advantage in a specific niche**: 1. First Bitcoin SPV client on Soroban - takes 12–18 months to build and audit 2. First integration of Stellar's ZK privacy (Protocol X-Ray) with BTC collateral 3. Open SDK creates ecosystem lock-in - once Stellar wallets/protocols build on Writz SPV, switching is costly ### Update - 2026-08-04: Solv Protocol + Templar Protocol findings Deeper research (prompted by evaluating a potential Solv integration) surfaced two corrections to the picture above: **Solv Protocol** is live on Stellar Soroban mainnet (`solv-finance/SolvBTC-Stellar-Contract`, audited, real trading volume). It does not threaten Writz's moat directly - SolvBTC is custodial (third-party custodians + FROST threshold-signature reserve, NAV oracle, blacklist/pausable token) and serves the opposite customer flow (USDC holders wanting BTC-denominated yield, not BTC holders wanting private loans). Its relevance is narrative, not architectural: it retires the "nothing BTC-related exists on Stellar" talking point and means Writz has to lead with "trustless + private," not "first." **Templar Protocol** is a more serious signal. On NEAR, it already runs a mainnet, audited, \$4M-pre-seed-funded lending product that takes **native BTC as collateral with no wrapping**, using a Bitcoin light client plus MPC/chain-signature threshold custody (NEAR's chain-abstraction stack) instead of Writz's SPV-proof + P2WSH-covenant approach - architecturally different (MPC threshold trust vs. pure cryptographic verification) but marketed with similar "no custodian, no wrapping" language. Separately, Templar already has a **live, Halborn-audited Soroban vault on Stellar** (via a Blend lending-market adapter, part of Upshift's multi-chain vault rollout), currently limited to XLM collateral. Its own `omni-sdk` bridge already lists Stellar among its 10+ supported chains, and its core contracts repo shows same-day commit activity as of this writing. Templar has not yet routed native BTC collateral into its Stellar vault - but every component needed to do so (BTC light client, MPC custody, cross-chain bridge with Stellar support, audited Soroban vault) already exists in their GitHub org. This is the most credible fast-follower risk identified so far - closer and faster-moving than Babylon Labs (no Stellar presence at all) precisely because Templar's Stellar infrastructure is already shipped, audited, and live; only the BTC-collateral wiring is missing. **Corrected 2026-10-26:** this update originally called the combination of native-BTC verification without wrapping and ZK-private position sizes Writz's "unreplicated core claim". That no longer describes the product: Writz v1 has no privacy (positions are public on chain; privacy is a separate, later edition), and its cooperative release path uses a protocol co-signing key. The differentiator Writz states is native BTC collateral without custody or wrapping: the BTC stays in the user's own script with a time-locked exit to the user's key alone, and Stellar verifies it through an SPV light client. Babylon also keeps Bitcoin in the owner's own script with a time-lock exit, so Writz does not claim a stronger custody guarantee than Babylon; it differs by lending against that Bitcoin on Stellar. What this update found still holds: Templar shortens the window before someone else assembles the pieces. *** ## Market Sizing ### Total Addressable Market (TAM) **BTCfi TAM:** $8.6B TVL and growing. If Writz captures 5% of BTCfi TVL by 2028, that is $430M+ in TVL. Protocol revenues at 1–2% annualized on TVL = $4.3M–$8.6M/year from lending alone. **Privacy DeFi TAM:** The privacy DeFi category is emerging. Aztec raised $100M. Penumbra raised $23M. The market for private financial infrastructure is early but large. **LATAM remittances (future product):** \$150B/year market where Stellar already has distribution. BTC→USDC private remittances would be a natural extension after the core protocol is established. ### Serviceable Addressable Market (SAM) Near-term realistic targets: * Bitcoin holders on Stellar-adjacent ecosystems (existing Stellar users with BTC) * Privacy-conscious individuals in LATAM with BTC savings * Crypto-native companies needing ZK Proof of Reserve (post-FTX demand is structural) * Stellar DeFi protocols wanting BTC exposure *** ## Timing Assessment **Why now:** 1. Protocol X-Ray launched January 2026 - the ZK infrastructure is production-ready TODAY 2. BTCfi is in a growth phase - early but proven market 3. Stellar has real USDC liquidity - not a chicken-and-egg problem on the output side 4. Soroban has matured - Protocol 23 brought parallel execution; smart contracts are production-grade 5. The summa-tx Rust SPV library exists - no need to build cryptographic primitives from scratch **Window:** 12–18 months before a well-funded competitor could plausibly replicate the SPV + ZK combination on Stellar - holds for a from-scratch entrant. **Revised for Templar Protocol specifically (2026-08-04):** their Stellar Soroban vault infrastructure and cross-chain bridge are already live and audited, and their native-BTC-no-wrap lending tech is already proven on NEAR mainnet. They are missing (1) the BTC-to-Stellar routing and (2) ZK privacy - neither trivial, but both are integration/feature work on top of shipped infrastructure rather than from-scratch builds. Treat their effective window as materially shorter than 12–18 months; testnet-to-mainnet speed on Writz's side is a competitive variable against Templar specifically, not just a generic risk. *** *Last updated: 2026-08-04 (Solv Protocol + Templar Protocol competitive findings added - original survey dated 2026-06-22)* # Research: BTC/USD Price Oracle on Stellar Source: https://docs.writz.xyz/research/oracle-design **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) ### 1. Pyth Network - RECOMMENDED PRIMARY **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. ### 2. Reflector (external CEXs & DEXs instance) - RECOMMENDED SECONDARY **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. *** ## Recommended Oracle Architecture for Writz ### Combined-oracle design Combining oracles is still the right call - confirmed, not just recommended, now that both sources are known to work: ``` Price sources: ├── Pyth BTC/USD (primary) → price_1 - real cross-exchange price, small per-pull fee, needs a Hermes update submitted per call └── Reflector external-prices BTC (secondary) → price_2 - confirmed live on testnet, free (Pulse tier), 5-min resolution Aggregation: price = median(price_1, price_2) # placeholder until a 3rd source (DIA, once fixed) is added Validity checks: - Each price must be < 60 minutes old (Pyth: since last pull; Reflector: since resolution()-aligned update) - If prices deviate >5% from each other → treat as a manipulation signal, pause liquidations rather than average blindly - If either source is unreachable → pause liquidations (not new borrows) until both are healthy again ``` ### 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: ```rust theme={null} // Reflector (SEP-40-shaped) - a straightforward cross-contract call fn get_reflector_btc_price(env: &Env, reflector_address: &Address) -> PriceData { let oracle: ReflectorClient = ReflectorClient::new(env, reflector_address); let asset = Asset::Other(Symbol::new(env, "BTC")); oracle.lastprice(&asset).unwrap_or_else(|| panic!("Reflector price unavailable")) } // Pyth - requires the caller (relayer/keeper) to supply a fresh signed Hermes // update as part of the same call; the contract only verifies it, it doesn't // store or serve a price on its own. fn verify_pyth_update(env: &Env, pyth_address: &Address, hermes_payload: Bytes) -> Bytes { let verifier: PythVerifierClient = PythVerifierClient::new(env, pyth_address); verifier.verify_update(&hermes_payload) .unwrap_or_else(|_| panic!("Pyth update verification failed")) // Caller still needs to parse the verified bytes into (price, exponent, timestamp) // per Pyth's wire format - not shown here. } ``` ### Liquidation price safety For liquidations specifically, use a **conservative price** (slightly lower than current) to avoid liquidating based on a momentary price spike: ``` liquidation_price = min( current_price, price_5_minutes_ago // smoothed lookback ) ``` 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 | Decision | Choice | Rationale | | - | - | - | | Primary oracle | Pyth | Real cross-exchange BTC/USD, industry standard; verification contract confirmed live on testnet, though price availability itself needs a Hermes-payload test (see "Before wiring this in") | | Secondary oracle | Reflector, external-prices instance | Confirmed live on testnet by direct call: real `BTC` ticker, live updating price, free (Pulse tier, 5-min resolution) | | Parked | DIA | Documented testnet contract address doesn't exist on-chain; needs a working address from DIA directly before it can be a candidate again | | Explicitly avoided | RedStone; SolvBTC/BTCLN as "BTC"; Reflector's Stellar-assets instance for BTC | RedStone isn't on Stellar at all; SolvBTC/BTCLN are wrapped BTC, reintroducing custodian risk; Reflector's *other* instance only has SAC-token prices, not the external BTC ticker | | Aggregation | Median of 2 confirmed-working sources, with a deviation check standing in for the missing 3rd-source tiebreaker | Both sources now confirmed live - no need to wait on a listing request before wiring in a real median | | Staleness threshold | 60 minutes, adjusted per provider cadence | Reflector's `resolution()` (300s) and Pyth's pull-on-demand model don't update the same way | | Liquidation smoothing | min(current, 5-min lookback) | Prevents front-running and price spike exploits | | Interface standard | SEP-40 for Reflector; Pyth uses its own verifier pattern, not SEP-40 | Reflector is provider-agnostic within SEP-40; Pyth's integration code is provider-specific regardless | *** *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.* # Research: Stellar ZK Infrastructure - Protocol 25, 26 & 27 Source: https://docs.writz.xyz/research/protocol-x-ray-capabilities **Author:** Justin (Business Analyst) + Technical Research **Date:** 2026-06-22 **Status:** Complete **Phase:** 0.2 - Stellar Protocol X-Ray Deep Dive *** ## Executive Summary Stellar has shipped the most complete compliance-friendly ZK infrastructure of any major blockchain ecosystem in 2026. Three consecutive protocol upgrades (X-Ray/P25, Yardstick/P26, and the forthcoming Zipper/P27) have progressively built out cryptographic primitives, reduced proof verification costs, and introduced smart account features that directly benefit Writz Protocol. **Bottom line for Writz:** * ZK position privacy is **technically feasible today** using Circom + Groth16 (the same stack as Stellar Private Payments) * Protocol 26 (live May 2026) significantly reduces ZK verification cost on-chain * Protocol 27 (July 8, 2026) introduces delegated authentication - directly useful for Writz's protocol co-signing mechanism * A complete open-source reference implementation exists (NethermindEth/stellar-private-payments) that Writz can learn from directly *** ## Protocol 25 - X-Ray (Mainnet: January 22, 2026) ### What it is Protocol 25, nicknamed **X-Ray**, is Stellar's foundational ZK upgrade. It embedded elliptic curve cryptography and ZK-friendly hashing natively into Soroban's host environment, making zero-knowledge proof verification on-chain practical for the first time. ### Technical primitives added #### CAP-0074: BN254 Elliptic Curve Host Functions Added native host functions for elliptic curve operations on **BN254** - the most widely used curve in the ZK ecosystem today (the same curve used by Ethereum's EIP-196/EIP-197 precompiles, Starknet, ZK Email, and most Circom-based applications). Operations exposed: * `bn254_add` - point addition on BN254 * `bn254_scalar_mul` - scalar multiplication on BN254 * `bn254_pairing` - bilinear pairing (the core operation for Groth16 verification) **Why BN254 specifically:** BN254 (also known as alt\_bn128) is the curve that powers Groth16 proofs, which is the proving system behind Circom, the most mature ZK circuit development tool. By targeting BN254, Stellar immediately enabled the entire Circom ecosystem. > Note: BLS12-381 was added earlier (CAP-0059). BN254 was prioritized for Protocol 25 because of its widespread adoption in production ZK tooling, despite BLS12-381 offering slightly better security properties. #### CAP-0075: Poseidon and Poseidon2 Hash Functions Added the **Poseidon** and **Poseidon2** hash function primitives as native host functions. **Why Poseidon matters for ZK:** Traditional cryptographic hash functions (SHA-256, Keccak) are computationally expensive inside ZK circuits because they use operations (bitwise, modular arithmetic in large primes) that don't translate efficiently to constraint systems. Poseidon was designed specifically to be ZK-friendly - it uses field arithmetic that maps cleanly to ZK constraints, resulting in circuits that are 10-100x smaller than SHA-256-based alternatives. For Writz specifically, Poseidon is the hash function used to build: * Position commitment trees (hiding collateral amounts) * Merkle trees for nullifier sets (preventing double-spending) * The commitment `commitment = Poseidon(amount, secret, nonce)` ### What became possible with Protocol 25 With BN254 + Poseidon on Soroban: * **Groth16 proof verification** - verify a SNARK proof in a single Soroban transaction * **Circom circuit compatibility** - any circuit built with Circom can be verified on Stellar * **RISC Zero zkVM proofs** - arbitrary Rust program execution can be proven and verified on-chain * **Privacy pools** - the Stellar Private Payments framework (shipped shortly after P25) ### Cost baseline established A Groth16 SNARK verification requires approximately **40 million Soroban instructions** - roughly half of the testnet's maximum instruction budget per transaction. This is feasible but tight, and Protocol 26 was specifically designed to reduce this cost. *** ## Protocol 26 - Yardstick (Mainnet: May 6, 2026) ### What it is Protocol 26, nicknamed **Yardstick**, brought precision and optimization to Stellar: a series of targeted improvements to Soroban's computation capabilities, ZK cryptography, and ledger management. ### ZK-specific additions: CAP-0080 **Nine new BN254 host functions** that move heavy ZK arithmetic from Wasm (expensive, slow) into the host layer (cheap, fast): | Function | Purpose | | - | - | | `bn254_msm` | Multi-scalar multiplication - the heaviest operation in Groth16 verification | | `bn254_field_add` | Scalar-field addition | | `bn254_field_sub` | Scalar-field subtraction | | `bn254_field_mul` | Scalar-field multiplication | | `bn254_field_pow` | Scalar-field exponentiation | | `bn254_field_inv` | Scalar-field inverse | | `bn254_g1_is_on_curve` | Curve membership check for G1 points | | `bn254_g2_is_on_curve` | Curve membership check for G2 points | | `bls12_381_is_on_curve` | Curve membership check for BLS12-381 points | **Why MSM matters:** Multi-scalar multiplication is the computationally dominant operation in Groth16 verification. Moving it from Wasm (where it had to be implemented in pure Rust and counted instruction-by-instruction) into a host function (where it's executed natively at hardware speed and billed as a fixed cost) reduces verification cost dramatically. **Impact on Noir:** The Interstellar project (Ratherlabs) is building a Noir → BLS12-381 proving backend for Soroban. Protocol 26's new field arithmetic host functions make Noir proof verification significantly cheaper once that pipeline is complete. ### Other Protocol 26 features relevant to Writz #### CAP-0077: Quorum Freeze (Ledger Entry Freezing) The first consensus-driven on-chain mechanism to freeze compromised ledger entries. A validator quorum can freeze a specific key or contract without a full protocol upgrade. **Writz relevance:** If a critical vulnerability is discovered in the Writz SPV contract or Writz Lend, the network now has a mechanism to freeze the affected contract while a fix is deployed. This is a meaningful security improvement for DeFi protocols on Stellar. #### CAP-0082: Checked 256-bit Integer Arithmetic 256-bit integer operations in Soroban now **trap on overflow** rather than silently wrapping around. Any arithmetic that would exceed the 256-bit boundary aborts the transaction with an error. **Writz relevance:** Critical for Writz Lend's financial calculations. Overflow in lending calculations (interest accumulation, collateral ratios, liquidation thresholds) is a classic DeFi exploit vector. Protocol 26 makes these errors explicit and transaction-aborting rather than silent. #### SAC Improvements The Stellar Asset Contract (SAC) can now create unlimited trustlines for G-accounts directly and auto-create account entries when sending XLM to new addresses. **Writz relevance:** Simplifies USDC pool management - Writz Lend can create trustlines for lenders programmatically without pre-existing account setup. *** ## Protocol 27 - Zipper (Mainnet vote: July 8, 2026) ### Timeline | Date | Event | | - | - | | June 4, 2026 | Upgrade guide published by SDF | | June 5, 2026 | Stellar Core + SDK releases | | June 18, 2026 | Testnet upgrade | | July 8, 2026 | Mainnet validator vote | ### What it is Protocol 27, nicknamed **Zipper**, makes **authentication delegation** a first-class feature on Stellar. One account can officially authorize another to act on its behalf, unlocking modular multisig, social recovery, and programmable signing policies. ### CAP-0071-01: Authentication Delegation Two new host functions: **`delegate_account_auth(address)`** Called inside a custom account's `__check_auth` function to delegate authentication to a specified address. When an account calls this, the delegated address can approve transactions on behalf of the delegating account. **`get_delegated_signers_for_current_auth_check()`** Returns the list of addresses currently delegated for the active auth check, enabling contracts to inspect and reason about the delegation chain. **New credential type:** `SOROBAN_CREDENTIALS_ADDRESS_WITH_DELEGATES` Bundles all delegated signers and their signatures into a single authorization entry, eliminating the need for separate authorization entries per delegated signer and reducing transaction size. **Key properties:** * Delegation can be **nested recursively** - delegated signers can themselves have delegated signers * Any account (including smart contract accounts) can serve as a delegate * Enables: social recovery, modular multisig, time-locked signing, session keys ### CAP-0071-02: Address-Bound Credentials Introduces `SOROBAN_CREDENTIALS_ADDRESS_V2` - a new credential type with address-bound signature payloads. AddressV2 will **replace V1 in Protocol 28**, giving developers until the next upgrade to migrate. ### Breaking change: SDK consolidation `@stellar/stellar-base` is being consolidated into `@stellar/stellar-sdk`. Anyone importing `stellar-base` directly must update imports to `@stellar/stellar-sdk`. ### Writz relevance - Protocol 27 is highly significant **Protocol co-signing key architecture:** The core mechanism in Writz requires a protocol-held co-signing key to authorize BTC release when loans are repaid. Protocol 27's auth delegation enables a much more robust design: ``` Writz Protocol Master Account │ ├── delegate_account_auth(Operator Set A) ← daily operations ├── delegate_account_auth(Operator Set B) ← emergency recovery └── delegate_account_auth(TimeLock Contract) ← automatic release after maturity ``` Instead of a single protocol private key (single point of failure), Writz can distribute the co-signing authority across multiple delegated accounts with different roles and thresholds. **Smart account users:** Protocol 27 also enables end users to use smart contract accounts with delegated signers for their Writz positions - enabling things like: * Multi-sig controlled positions (2-of-3 family members must approve borrowing) * Session key access (temporary delegation to a frontend app) * Dead-man's switch recovery (delegate to a backup address if main key is lost) *** ## Quantum Preparedness Roadmap (Announced June 9, 2026) The SDF published a three-stage quantum preparedness plan that will affect Writz's long-term key management strategy. | Stage | Timeline | What happens | | - | - | - | | **Stage 1** | 2026 | ML-DSA-44 and ML-DSA-65 (NIST post-quantum standards) added as native Soroban host functions | | **Stage 2** | 2027 | Opt-in quantum-safe signer types for existing accounts | | **Stage 3** | Flexible (post-2029) | Ed25519 deprecated when quantum threat is real | **Writz relevance:** Stellar separates account identity from signing keys, meaning Writz's protocol keys can be migrated to post-quantum signatures (Stage 2, 2027) without changing the contract addresses or user positions. This is a meaningful long-term security advantage over EVM-based DeFi, where key migration requires contract redeployment. *** ## Stellar Private Payments - Reference Implementation **Repository:** [NethermindEth/stellar-private-payments](https://github.com/NethermindEth/stellar-private-payments) **Docs:** [nethermindeth.github.io/stellar-private-payments](https://nethermindeth.github.io/stellar-private-payments/) **Released:** February 2026 (shortly after Protocol 25 mainnet) **Built by:** Nethermind ZK Engineering Team ### Architecture The most complete open-source reference for ZK-private asset management on Stellar. Directly applicable to Writz's position privacy design. **Model:** UTXO-based commitments (similar to Zcash shielded pool / Tornado Cash) ``` Deposit flow: User sends token to pool contract → Creates a commitment: Poseidon(amount, secret, nonce) → Commitment added to on-chain Merkle tree → User stores (amount, secret, nonce) privately Transfer flow: User generates ZK proof: - "I know a (amount, secret, nonce) s.t. commitment is in the Merkle tree" - "I am spending this commitment to create new output commitments" - "Inputs = Outputs (no money created or destroyed)" - "I haven't spent this commitment before (nullifier not in nullifier set)" Proof verified on-chain by Groth16 verifier contract Nullifier recorded to prevent double-spend New output commitments created ``` ### Technical stack | Component | Technology | | - | - | | ZK circuits | Circom | | Proving system | Groth16 | | Elliptic curve | BN254 | | ZK-friendly hash | Poseidon | | Proof generation | WebAssembly (browser-side) | | On-chain verifier | Soroban smart contract | | Compliance | ASP membership/non-membership Merkle trees | ### Compliance system (ASPs) Association Set Providers (ASPs) maintain two Merkle trees: * **Allow list:** addresses permitted to interact with the pool * **Block list:** addresses flagged for suspicious activity Users can be required to prove they are on the allow list AND not on the block list, without revealing their identity. Operators can update these lists without accessing individual transaction data. ### What Writz takes from this directly | Stellar Private Payments feature | Writz adaptation | | - | - | | Commitment = Poseidon(amount, secret, nonce) | Writz Lend position = Poseidon(collateral\_btc, usdc\_debt, secret, nonce) | | Merkle tree of commitments | Merkle tree of active lending positions | | Nullifier set (prevent double-spend) | Nullifier set (prevent double-repay / double-liquidation) | | Circom circuit for transfer | Circom circuit for borrow/repay/liquidation proofs | | Groth16 verifier on Soroban | Same - directly reusable | | ASP compliance | Same - Writz can use ASPs for regulatory compliance | | Browser-side WASM proving | Same - user generates ZK proof in browser before submitting | *** ## ZK Tooling Landscape on Stellar ### Proven / Production-ready today | Tool | Status | Use case | | - | - | - | | **Circom + Groth16** | ✓ Production | Write circuits in Circom → generate Groth16 proofs → verify on Soroban via BN254 host functions | | **RISC Zero** | ✓ Production | Prove arbitrary Rust program execution → verify on Soroban | | **Stellar Private Payments** | ✓ Open source | Reference implementation of private asset pool on Soroban | | **Groth16 verifier contract** | ✓ Example code | `stellar/soroban-examples/groth16_verifier` - ready to use | ### Emerging / In development | Tool | Status | Use case | | - | - | - | | **Interstellar (Ratherlabs)** | ○ In development | Noir → BLS12-381 proving backend for Soroban; write ZK programs in Noir | | **NoirLang direct** | ○ Possible with P26 | P26's new BN254 host functions make Noir verification cheaper when Interstellar ships | ### Writz recommendation: Use Circom + Groth16 For Writz's v1 privacy layer, use **Circom + Groth16 on BN254** - the same stack as Stellar Private Payments. It is: * The only fully production-tested ZK stack on Stellar today * Well-documented with working examples in the Stellar ecosystem * Immediately compatible with Protocol 25 + 26 host functions * Has browser-side WASM proving libraries ready Noir + Soroban (via Interstellar) is worth tracking for v2 once it's production-ready - Noir is a more developer-friendly ZK language than Circom. *** ## Protocol Timeline Summary ``` 2025 Sep │ Protocol 23 (Whisk) - Parallel Soroban execution 2026 Jan │ Protocol 25 (X-Ray) - BN254 + Poseidon → ZK on Soroban │ ↳ Stellar Private Payments open-sourced (Feb 2026) │ May │ Protocol 26 (Yardstick) - BN254 MSM + scalar field arithmetic │ → ZK verification cost significantly reduced │ → Checked 256-bit arithmetic (overflow protection) │ → Quorum freeze (emergency contract freezing) │ Jul │ Protocol 27 (Zipper) - Auth delegation [mainnet vote Jul 8] │ → Protocol co-signing key can be distributed/delegated │ → Smart account users with delegated signers │ → SDK: stellar-base merges into stellar-sdk │ 2027 │ Quantum Stage 2 - Opt-in post-quantum signers ``` *** ## Capability Assessment for Writz Protocol | Writz requirement | Stellar capability | Protocol | Verdict | | - | - | - | - | | ZK proof of position privacy | Groth16 via BN254 (Circom) | P25 live | ✓ Feasible today | | ZK-friendly hashing (commitments) | Poseidon host functions | P25 live | ✓ Feasible today | | Affordable proof verification | BN254 MSM + scalar arithmetic | P26 live | ✓ Cost acceptable | | Overflow-safe lending math | Checked 256-bit arithmetic | P26 live | ✓ Built-in | | Protocol co-sign key distribution | Auth delegation | P27 Jul 8 | ✓ Imminent | | Emergency contract freeze | Quorum freeze | P26 live | ✓ Available | | Reference ZK pool implementation | Stellar Private Payments | P25 era | ✓ Open source | | Browser-side proof generation | WASM Groth16 prover | P25 era | ✓ Available | | Post-quantum key migration | ML-DSA via Soroban host | 2026 Stage 1 | ○ Coming | | Noir-based circuits | Interstellar (in dev) | P26+ | ○ Not yet | *** ## Critical Findings for Writz Architecture ### 1. Use Circom + Groth16, not Noir (for now) The Stellar Private Payments project by Nethermind uses Circom + Groth16 and is live today. Writz should use the same stack for v1 - it's the only production-ready ZK path on Stellar. Noir can be evaluated for v2. ### 2. Protocol 27 changes the co-signing key design The Writz architecture doc assumed a single protocol co-signing key. Protocol 27 (shipping July 8) enables distributing this across multiple delegated signers with different roles. **Update the architecture before building the key management system.** ### 3. The Stellar Private Payments codebase is the direct reference [NethermindEth/stellar-private-payments](https://github.com/NethermindEth/stellar-private-payments) implements exactly what Writz needs for position privacy: Poseidon commitments, Merkle trees, nullifier sets, Groth16 verification on Soroban, and browser-side WASM proving. Study this codebase before writing any Writz circuit code. ### 4. The 40M instruction limit requires careful circuit design A Groth16 verification costs \~40M instructions, roughly half the per-transaction budget. Complex circuits with many constraints will approach this limit. Protocol 26's MSM host functions help. Circuit complexity (number of constraints) should be minimized in the Writz Circom design. ### 5. Protocol 26's overflow protection is free safety Use `checked_add`, `checked_mul` etc. throughout the Writz Lend contract. Overflow in collateral ratio calculations is a classic exploit - Protocol 26 makes this a guaranteed transaction abort rather than silent corruption. ### 6. The compliance layer (ASPs) is built, not custom Stellar Private Payments already implements the ASP membership/block-list system. Writz should reuse or integrate this rather than building custom compliance infrastructure. *** ## Recommended Next Steps 1. **Clone and run** [NethermindEth/stellar-private-payments](https://github.com/NethermindEth/stellar-private-payments) locally - understand the complete ZK flow from circuit to Soroban contract 2. **Study** the `groth16_verifier` example in [stellar/soroban-examples](https://github.com/stellar/soroban-examples/tree/main/groth16_verifier) - this is the on-chain verifier Writz will use 3. **Design Writz's Circom circuit** for position commitments before writing Soroban code: * Inputs: `collateral_satoshis`, `usdc_debt`, `secret`, `nonce` * Commitment: `Poseidon(collateral_satoshis, usdc_debt, secret, nonce)` * Constraints: collateral ratio ≥ 150%, no double-spend (nullifier check), valid Merkle inclusion 4. **Wait for Protocol 27 (July 8)** before finalizing the protocol co-signing key architecture - delegated auth changes the design significantly 5. **Track Interstellar** (Ratherlabs Noir backend) for potential adoption in v2 *** *Last updated: 2026-06-22* *Sources:* * [Announcing Stellar X-Ray, Protocol 25 - stellar.org](https://stellar.org/blog/developers/announcing-stellar-x-ray-protocol-25) * [Yardstick, Stellar Protocol 26 - stellar.org](https://stellar.org/blog/foundation-news/yardstick-stellar-protocol-26) * [Protocol 26 Upgrade Guide - stellar.org](https://stellar.org/blog/foundation-news/stellar-yardstick-protocol-26-upgrade-guide) * [Stellar Zipper Protocol 27 Upgrade Guide - stellar.org](https://stellar.org/blog/foundation-news/stellar-zipper-protocol-27-upgrade-guide) * [ZK Proofs on Stellar - developers.stellar.org](https://developers.stellar.org/docs/build/apps/zk) * [NethermindEth/stellar-private-payments - GitHub](https://github.com/NethermindEth/stellar-private-payments) * [Stellar Private Payments Docs - nethermindeth.github.io](https://nethermindeth.github.io/stellar-private-payments/) * [CAP-0074: BN254 Host Functions - GitHub](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0075.md) * [Interstellar: Noir + BLS12-381 for Soroban - Ratherlabs](https://github.com/orgs/noir-lang/discussions/8654) * [Stellar Quantum Preparedness Plan - stellar.org](https://stellar.org/blog/foundation-news/introducing-the-quantum-preparedness-plan) * [How Stellar Enables ZK Proof Verification with Protocol 25 - bitrue.com](https://www.bitrue.com/blog/stellar-zk-proof-verification) * [Prototyping Privacy Pools on Stellar - stellar.org](https://stellar.org/blog/ecosystem/prototyping-privacy-pools-on-stellar) # Research: Regulatory Landscape for Private DeFi Source: https://docs.writz.xyz/research/regulatory-landscape **Author:** Research **Date:** 2026-06-22 **Status:** Complete *** ## Overview Privacy-preserving DeFi operates at the intersection of two regulatory priorities that often conflict: financial privacy rights and AML/KYC compliance. This document maps the 2026 regulatory landscape and defines how Writz Protocol positions itself to remain legally defensible while preserving meaningful privacy. *** ## The Regulatory Environment (2026) ### FATF and AML pressure The Financial Action Task Force (FATF) - the global standard-setter for AML - has continued pushing for jurisdiction-level enforcement of its guidelines on DeFi and "anonymity-enhancing technologies." In 2026: * FATF urged jurisdictions to strengthen enforcement against anonymous crypto transactions * The Travel Rule (sharing sender/receiver info for transactions above thresholds) is increasingly extending to DeFi access pathways * Mixers and pure anonymity tools (Tornado Cash precedent) face the most regulatory risk **Key distinction for Writz:** FATF's guidance distinguishes between protocols that are *de facto* controlled by an identifiable entity vs. truly decentralized protocols. Writz in Phase 1 is a team-operated protocol - it has an identifiable controlling entity and regulatory accountability. ### U.S.: GENIUS Act (July 2025) The U.S. GENIUS Act brought payment stablecoins (including USDC) under the Bank Secrecy Act, mandating: * Customer due diligence (CDD) * Transaction monitoring * Suspicious activity reporting (SAR) * OFAC screening **Impact on Writz:** USDC itself is now formally regulated under BSA. However, the obligation falls on the **issuer (Circle)** and regulated **intermediaries** - not necessarily on a smart contract protocol. Writz should not position itself as a USDC money service. The protocol is a smart contract; users interact with it directly. ### Market bifurcation trend 2026 regulatory analysis shows a clear market split: * **"Permissioned DeFi":** Fully compliant, KYC-heavy, used by banks and institutional players * **"Pure DeFi":** Privacy-first, permissionless, regulatory gray area Writz's "compliance-friendly privacy" model targets the middle ground: **private by default, auditable by design** - enabled by Stellar's ASP system. *** ## Writz's Regulatory Strategy: Selective Transparency Writz does not hide from regulation. It uses ZK proofs to provide **privacy without opacity** - the two are not the same. ### The ASP System (Stellar Private Payments) Stellar's Association Set Providers (ASPs) enable compliance without sacrificing privacy: **How it works:** 1. ASPs maintain two Merkle trees: an **allow list** (verified users) and a **block list** (sanctioned addresses) 2. Users prove (via ZK) that they are on the allow list AND not on the block list - without revealing their identity 3. Protocol operators can enforce compliance requirements without seeing individual transactions 4. Regulators can access specific transaction records through a defined legal process **For Writz:** * Phase 1: No ASP required (low TVL, low regulatory risk) * Phase 2: Integrate with an accredited ASP for users above certain deposit thresholds (\$10,000+) * Enterprise Proof of Reserve: Always ASP-gated (institutional customers have their own compliance needs) **Integration plan - status: not yet researched, flagged as an open Phase 2 task.** This document names the ASP system as a requirement but has not identified a specific accredited ASP provider, integration cost, or technical scope. Before this becomes a Phase 2 exit criterion, resolve: * **Provider:** which accredited ASP(s) are actually live/available for Soroban integration at Phase 2 time - this needs a fresh lookup, not an assumption, since ASP tooling on Stellar is new and the available provider list may change between now and Phase 2 * **Technical scope:** integrating the allow-list/block-list ZK-membership check into the deposit/borrow flow - likely an additional circuit constraint or a separate proof the user submits alongside the existing deposit/borrow proof; needs a design pass once a provider's interface is known * **Cost:** no estimate exists yet. Do not publish a specific dollar figure for this until a provider quote or comparable integration's public cost is found - an invented number here would be exactly the kind of unverified claim this audit is trying to remove * **Owner and target date:** unassigned ### What ZK privacy does and doesn't hide | Visible on-chain | Hidden on-chain | | - | - | | That a deposit occurred | How much BTC was deposited | | That a loan was taken | The loan amount | | That a liquidation happened | Who was liquidated and for how much | | Protocol TVL (aggregate) | Individual position sizes | | USDC pool utilization (aggregate) | Who is borrowing | This is materially different from Tornado Cash, which hid the *existence* of transactions. Writz transactions are visible - only the amounts are hidden. This is similar to how traditional banking works: your bank knows about your transactions, but your neighbors don't. *** ## Tornado Cash Precedent - What Writz Is Not The U.S. Treasury's OFAC sanction of Tornado Cash (2022) and subsequent legal cases established important precedents. Key differentiators: | Tornado Cash | Writz Protocol | | - | - | | No compliance hooks | ASP compliance system built-in | | Anonymous team | Identified founding team | | No KYC option | ASP allows selective KYC gating | | Mixing purpose (obscure source of funds) | Collateralized lending (funds origin is on Bitcoin, verifiable) | | No identifiable controlling entity | Team-operated protocol with legal entity | | Sanctioned wallets could use it freely | ASP block-list can exclude OFAC-sanctioned addresses | The most important distinction: **Writz is a collateralized lending protocol, not a mixer.** All BTC collateral is verifiable on the Bitcoin blockchain. The privacy is in the *position size on Stellar*, not in the origin of funds. *** ## Jurisdictional Considerations ### United States * Highest regulatory risk for DeFi teams * Consider operating through a non-US entity (BVI, Cayman Islands, Zug/Switzerland) * Protocol smart contracts are deployed on Stellar and are not under any jurisdiction * Team entity should have legal counsel familiar with DeFi/crypto regulation before mainnet ### European Union (MiCA) * Markets in Crypto-Assets regulation (MiCA) provides a framework for crypto asset service providers * Privacy protocols in a gray area, but "privacy by default, auditable on request" aligns with GDPR principles * EU's "right to privacy" in financial matters provides some legal cover for privacy-preserving design ### Switzerland (Zug "Crypto Valley") * Most favorable regulatory environment for DeFi protocols * DeFi protocols generally not classified as banks or payment service providers * FINMA guidance treats protocols differently from service providers * **Recommended jurisdiction for Writz's legal entity** ### El Salvador and LATAM * Bitcoin is legal tender in El Salvador - BTC-based DeFi is explicitly supported * LATAM regulators generally less prescriptive about DeFi than US/EU * Large potential user base (BTC adoption driven by inflation concerns) *** ## Practical Compliance Steps ### Before mainnet 1. **Legal entity formation** - Establish a legal entity in a favorable jurisdiction (Switzerland recommended) 2. **Legal counsel** - Retain a crypto-specialized law firm familiar with DeFi regulation 3. **Terms of Service** - Publish ToS that clearly states: * Writz is a smart contract protocol, not a financial institution * Users are responsible for their own regulatory compliance * Services are not available to OFAC-sanctioned jurisdictions 4. **OFAC screening** - Implement OFAC address screening in the frontend (not the contract) - block known sanctioned addresses from using the UI 5. **Privacy policy** - GDPR-compliant privacy policy for any user data collected by the frontend ### Ongoing 6. **ASP integration** (Phase 2) - When TVL exceeds \$1M, integrate ASP allow-list 7. **Regulatory engagement** - Proactively engage Stellar Foundation's policy team and relevant regulators to position Writz as a responsible privacy protocol 8. **Transaction monitoring** - For the enterprise Proof of Reserve product, implement Chainalysis or TRM Labs screening *** ## Key Regulatory Risks and Mitigations | Risk | Likelihood | Mitigation | | - | - | - | | US enforcement action against team | Medium | Non-US legal entity; decentralize governance progressively | | OFAC sanction for privacy protocol | Low | ASP block-list, no mixing functionality, collateralized (traceable) funds | | EU MiCA classification as regulated service | Low-Medium | Protocol is a smart contract; team provides tooling, not services | | Regulatory pressure on USDC use in private DeFi | Low | Circle compliance built into USDC; Writz uses native Circle USDC | | AML obligation attached to Writz as entity | Medium | Legal counsel + ToS + OFAC frontend screening reduces risk | *** ## Positioning Statement Writz Protocol provides **financial privacy as a feature, not financial opacity as a product**. Users can prove their creditworthiness, prove their compliance, and prove their solvency - all without revealing their private financial positions to the public. This is analogous to how traditional banking provides privacy from neighbors while maintaining regulatory accountability. The protocol is not designed to help users evade taxes, launder money, or circumvent sanctions. It is designed to prevent the structural disadvantages of fully-public blockchains: front-running, competitive intelligence leakage, and personal financial exposure. *** *Last updated: 2026-06-22* *Sources: [DeFi Compliance 2026 Guide - Blockchain Council](https://www.blockchain-council.org/cryptocurrency/defi-and-wallet-compliance-kyc-aml-travel-rule-self-custody/) · [FATF DeFi Guidelines](https://www.fatf-gafi.org/) · [Grant Thornton Crypto Compliance 2026](https://www.grantthornton.com/insights/articles/banking/2026/crypto-compliance-in-2026) · [Clarity Act Overview](https://www.zyphe.com/resources/glossary/clarity-act)* # Research: Relayer Incentive Design Source: https://docs.writz.xyz/research/relayer-incentive-design **Author:** Research **Date:** 2026-06-22 **Status:** Complete *** ## Overview > **Updated 2026-10-26.** This June 2026 design assumed stateless SPV, with no header chain on Stellar. That is no longer the design: `bitcoin-spv` is an on-chain header light client, so headers must be submitted continuously, as in BTC Relay, and relayer liveness is on the critical path. Liveness is engineered rather than assumed: planned permissionless header submission on mainnet, several independent relayers on different providers and Bitcoin sources, and an on-chain freshness rule so that loans depending on the Bitcoin tip are refused when it is too old (see [SPV vs Wrapped Bitcoin](/security/spv-vs-wrapped)). The sections below are kept as the original research; where they describe the stateless design, read them as superseded. The original design assumed **stateless SPV** (no on-chain header chain), which would have made the header relay problem simpler than BTC Relay's. Users still need a reliable source of Bitcoin block headers and Merkle proofs to construct their SPV proofs. This document designs the relayer service and its incentive model. *** ## The Stateless SPV Relayer Difference (superseded) ### BTC Relay's failure (stateful approach) BTC Relay required relayers to continuously submit every Bitcoin block header to Ethereum. When fee revenue dropped, relayers stopped. No headers = system dead. ### The original approach (stateless, superseded) The original design did not store Bitcoin block headers on Stellar. Instead: * Users provide the headers themselves at verification time * The Soroban contract verifies the provided headers on-demand * No continuous relay is required **Implication:** The relayer is not a protocol-critical component - it's a **convenience service**. A user can fetch their own headers from any Bitcoin full node or public API. If the Writz relayer goes down, users can use alternative sources. This fundamentally changes the risk profile from BTC Relay. *** ## What the Relayer Actually Does When a user wants to prove a Bitcoin deposit to Soroban, they need: 1. The raw Bitcoin transaction (containing their P2WSH output) 2. The Merkle proof for that transaction within its block 3. The block header for that block 4. A chain of N block headers connecting that block to a recent, trusted checkpoint The relayer is a service that: * Maintains a connection to a Bitcoin full node * Given a txid, fetches and packages all of the above into a ready-to-use SPV proof bundle * Returns the bundle to the user's browser for submission to Soroban *** ## Relayer Architecture Options ### Option A: Writz-Operated Centralized Relayer A single Bitcoin full node + REST API operated by Writz. **Endpoint:** ``` GET /spv-proof/{txid} Response: { "raw_tx": "0200000001...", "merkle_proof": ["a1b2c3...", "d4e5f6...", ...], "tx_index": 42, "block_headers": ["00000020...", "00000020...", ...], // 6 headers "block_height": 845123 } ``` **Pros:** Simple, fast, low cost, full control over reliability. **Cons:** Centralization. If Writz's server is down, users can't easily generate proofs without technical knowledge. **Mitigation:** Open the header fetching to any Bitcoin full node. Users who know what they're doing can query any public Bitcoin node (mempool.space API, Blockstream Esplora, their own node). The Writz relayer is a convenience, not a dependency. ### Option B: Decentralized Relayer Network Multiple independent operators run relayer nodes. Users can query any of them. Operators are paid per proof request. **Pros:** No single point of failure. Censorship resistant. **Cons:** Adds complexity. Overkill for Phase 1 where user volume is small. **Recommendation:** Start with Option A (Phase 1), with a public API that any third party can independently implement. Design the API spec openly so competitors or community members can run alternative relayers. Progressively decentralize as the protocol grows. ### Option C: Pure User Self-Service No Writz-operated relayer. The frontend guides users to fetch their own proof from public APIs. **Pros:** Maximum decentralization. No ongoing infrastructure cost. **Cons:** Very poor UX. Most users cannot fetch and format Merkle proofs manually. **Verdict:** Suitable as a fallback mode, not as the primary experience. *** ## Relayer Incentive Model (for Decentralized Phase 2) When Writz launches a decentralized relayer network, relayers need economic incentives to operate reliably. ### Fee model **Per-proof fee:** Users pay a small fee (e.g., 1,000 satoshis in USDC equivalent \~\$1) when requesting a proof bundle. This fee is: * Charged in USDC on Stellar (simplest for users) * Collected by the Writz Lend contract as part of the deposit transaction * Distributed to the relayer that provided the proof bundle **Collateral requirement:** Relayers post a bond (e.g., \$500 in USDC) when registering. If they provide invalid data that causes a failed transaction, the bond is partially slashed and distributed to the affected user. **Reputation system:** Relayers accumulate a success score. High-reputation relayers get preferential routing from the Writz frontend. ### Liveness guarantee Superseded: the original design argued that stateless SPV needs no 24/7 relayer availability. With the header light client it does: headers have to keep arriving, or the contract's view of Bitcoin goes stale. Redundant relayers and the planned on-chain freshness rule address this; a stale tip yields fewer confirmations, never false ones. **Minimum viable liveness:** A relayer that responds within 60 seconds and has >99% uptime over 30-day windows can participate. *** ## Public Data Sources (User Fallback) If the Writz relayer is unavailable, users can construct their own SPV proof from public sources: | Source | What it provides | How to use | | - | - | - | | **Blockstream Esplora API** | Raw transaction, Merkle proof, block header | `GET https://blockstream.info/api/tx/{txid}/merkle-proof` | | **mempool.space API** | Same as Blockstream | `GET https://mempool.space/api/tx/{txid}/merkle-proof` | | **Bitcoin Core RPC** | Full node access, all data | `bitcoin-cli gettxout`, `getblock`, `gettxoutproof` | Writz's frontend should include a "Manual proof submission" mode that walks technical users through fetching their own data from these sources. *** ## Alternative: zkRelay (Phase 2+ Consideration) Research found **zkRelay** - a ZK-SNARK-based relay that batches Bitcoin header validation off-chain. Instead of submitting 6 raw headers, a prover submits one ZK proof that 6 (or 504) headers are all valid. **Cost reduction:** 187× cheaper than per-header verification (achieved in Ethereum research). **For Writz:** If the raw block header chain verification becomes a cost concern (especially for headers with very high difficulty requiring large PoW numbers), zkRelay-style header batching could significantly reduce the Soroban transaction cost of the SPV verification step. **Not for Phase 1.** zkRelay adds circuit complexity and a trusted setup. Start with raw header verification; switch to zkRelay batching if needed in Phase 2. *** ## Summary | Decision | Choice | Rationale | | - | - | - | | Relayer type (Phase 1) | Writz-operated API | Simple, fast to ship, adequate for early users | | Relayer type (Phase 2) | Decentralized with fee incentives | As user volume grows, decentralize for resilience | | Fallback | User self-service from public APIs | Eliminates complete dependency on Writz relayer | | Fee model | \$1 per proof request (paid in USDC) | Fair compensation, low burden on user | | zkRelay adoption | Phase 2+ if needed | Only if header verification becomes a cost bottleneck | | Liveness guarantee | Critical path (originally: convenience service) | The header light client needs headers submitted continuously; redundant relayers and a freshness rule are planned | *** *Last updated: 2026-06-22* *Sources: [zkRelay paper](https://eprint.iacr.org/2020/433) · [BTC Relay GitHub](https://github.com/ethereum/btcrelay) · [Blockstream Esplora API](https://github.com/Blockstream/esplora) · [TrustBlink: zkSNARK relay](https://link.springer.com/chapter/10.1007/978-981-95-3543-9_7)* # Research: Security Audit Strategy Source: https://docs.writz.xyz/research/security-audit-strategy **Author:** Research **Date:** 2026-06-22 **Status:** Complete *** ## Overview Security is Writz Protocol's most critical non-negotiable. A single exploit could drain all BTC collateral and destroy user trust permanently. This document maps the audit landscape for Soroban, documents common vulnerabilities, and defines Writz's security strategy. *** ## The Soroban Security Audit Bank **The most important finding in this research: Writz may qualify for FREE security audits through the SDF.** The Stellar Development Foundation operates the **Soroban Security Audit Bank** - a program that provides up to \$1M in security audit credits distributed across 20–30 high-priority Soroban projects. ### Program details | Parameter | Value | | - | - | | Total budget | Up to \$1M in audit credits | | Number of projects | 20–30 | | Co-payment required | 5% of audit cost (refundable if vulnerabilities are remediated within 20 days) | | Participating firms | OtterSec, Veridise, Runtime Verification, CoinFabrik, QuarksLab, Coinspect | | Eligibility pathway | Build an SCF-funded project OR demonstrate sufficient ecosystem impact | | Follow-up audits | Free at $10M TVL and $100M TVL milestones | ### How to qualify The primary pathway is through the **Stellar Community Fund (SCF)**. If Writz receives an SCF grant, it automatically qualifies for Audit Bank access. This aligns the grant strategy with the security strategy - SCF funding unlocks both capital AND audit support. *** ## Approved Audit Firms (Official SDF Audit Bank List) The following firms are officially pre-approved by the SDF for the Soroban Security Audit Bank. Source: SCF handbook. | Firm | Specialization | Writz relevance | | - | - | - | | **Veridise** | Smart contract + ZK circuit audits, advanced vulnerability detection tooling | ✓ Primary - only approved firm with explicit ZK circuit capability | | **OtterSec** | \$36B+ TVL secured, 120+ protocols, Soroban track record | ✓ Primary - Soroban smart contract audit | | **Zellic** | Blockchain + cryptography security, world-class white-hat team | ✓ Strong option for Soroban contracts | | **Certora** | Formal verification via mathematical reasoning of code | Good for Writz Lend financial math formal proofs | | **Runtime Verification** | Formal methods and runtime verification, deep design review | Good for SPV verification correctness proofs | | **Spearbit + Cantina** | Network of top researchers, competitive audit platform | Good for broad coverage | | **Oak Security** | 600+ audits with zero exploits, blinded parallel review | Good for unbiased second opinion | | **Halborn** | Web3 + enterprise security assessments | Option if others unavailable | | **ChainSecurity** | Complex smart contract infrastructure, founded 2017 | Option for deep protocol review | | **Code4rena** | Competitive platform, 100+ researchers per audit | Good for community-wide coverage | **Writz's target firms:** * **Veridise** - Circom ZK circuit audit (5 circuits: deposit, borrow/repay, liquidation, zero-debt release, Merkle insertion) * **OtterSec or Zellic** - Soroban smart contracts (SPV client + Writz Lend + USDC pool) *** ## Common Soroban Vulnerabilities Based on CertiK's research and the audit bank's findings across 40+ audited projects: ### 1. Unbounded Storage Growth (HIGH SEVERITY) **What it is:** Storing ever-growing collections (arrays, maps) in `instance` storage causes the storage entry to grow unboundedly. Reading or writing large instance entries can spike costs dramatically and eventually DoS the contract. **Example in a lending context:** ```rust theme={null} // DANGEROUS: All position commitments in one instance entry #[contracttype] pub struct PoolState { pub commitments: Vec>, // grows forever! pub nullifiers: Vec>, // grows forever! } ``` **Writz mitigation:** * Each commitment stored as a separate `persistent` ledger entry keyed by commitment hash * Each nullifier stored as a separate `persistent` ledger entry keyed by nullifier hash * Pool-level state (TVL, utilization) stored in `instance` storage (fixed size) * Never use `Vec` in instance storage for data that grows with users ### 2. Type Safety in Host Value Conversion (MEDIUM SEVERITY) **What it is:** Soroban converts container inputs into raw host values without guaranteed round-trip type safety. Storing a value as one type and retrieving as another can halt execution or corrupt logic. **Writz mitigation:** * Define all storage types explicitly with `#[contracttype]` derive macro * Use newtype wrappers for semantic distinction (e.g., `CommitmentHash(BytesN<32>)` vs `NullifierHash(BytesN<32>)`) * Write round-trip tests for every storage type: store → retrieve → assert equality ### 3. Integer Overflow in Financial Math (HIGH SEVERITY) **What it is:** Before Protocol 26, overflow in 256-bit arithmetic would silently wrap around, causing wildly incorrect financial calculations. Protocol 26 introduces checked arithmetic that traps on overflow. **Writz mitigation:** * Target Protocol 26+ from day one - use checked arithmetic everywhere * Use `checked_add`, `checked_mul`, `checked_div` for all financial calculations * Fuzz test interest accrual calculations with extreme inputs (very high interest rates, very long time periods, max uint values) * Write explicit overflow tests at boundary conditions ### 4. Reentrancy via Cross-Contract Calls (HIGH SEVERITY) **What it is:** When Writz's Writz Lend contract calls an external contract (USDC token transfer, oracle, etc.), that external contract could call back into Writz before the first call completes - potentially double-spending or corrupting state. **Writz mitigation:** * Follow checks-effects-interactions pattern: update all state BEFORE making external calls * Use reentrancy guards on critical functions (`liquidate`, `repay`, `borrow`) * Minimize external calls within sensitive functions ### 5. Oracle Manipulation (HIGH SEVERITY) **What it is:** If the BTC price oracle can be manipulated (even temporarily), an attacker can trigger artificial liquidations or borrow more USDC than their BTC is worth. **Writz mitigation:** * Multi-oracle median (Pyth + Reflector - see `docs/research/oracle-design.md` for why RedStone was dropped and DIA was parked) * Staleness check: reject prices >60 minutes old * Price deviation check: reject if sources disagree by >5% * Liquidation smoothing: use min(current, 5-min-ago price) for liquidations ### 6. ZK Proof Soundness (CRITICAL) **What it is:** A bug in the Circom circuit could allow an attacker to construct a valid-looking proof that proves a false statement - e.g., proving they have more collateral than they do. **Writz mitigation:** * Engage ZK-specialized auditors (Veridise, Trail of Bits' ZK team) separately from Soroban contract auditors * Conduct formal verification of critical circuit constraints * Run the trusted setup ceremony transparently with multiple parties * Test circuits against known attack patterns (under-constrained circuits, malicious witness generation) ### 7. Timing Attacks on Liquidations (MEDIUM SEVERITY) **What it is:** If liquidations can only happen at specific times or by specific parties, an attacker can front-run or delay liquidations to profit. **Writz mitigation:** * Liquidation is open to anyone once the ZK proof is valid * No minimum delay between liquidation proof submission and execution * Keeper bond ensures keepers don't collude to delay *** ## Security Testing Strategy ### Pre-audit (internal) **Unit tests:** Every contract function tested in isolation with both valid and invalid inputs. Minimum 90% function coverage. **Integration tests:** Cross-contract call flows tested end-to-end (SPV → Writz Lend → USDC pool). **Fuzz testing:** Use `cargo-fuzz` on: * Interest rate calculations (extreme utilization, very large/small amounts) * Collateral ratio calculations (zero debt, max BTC, extreme prices) * SPV verification (malformed headers, invalid Merkle proofs, reorg scenarios) * ZK proof submission (invalid proofs, replayed proofs, malformed inputs) **Invariant testing:** Define protocol invariants and test that no sequence of operations can violate them: * `total_usdc_borrowed ≤ total_usdc_supplied × Umax` * `sum(commitment_collateral) = total_btc_locked` * `insurance_fund ≥ 0` (never negative) ### Audit sequence ``` Phase 1 (before testnet with real funds): ├── Internal security review ├── Fuzz testing campaign (2 weeks) └── Bug bounty (Immunefi, $5K max per bug) Phase 2 (before mainnet): ├── OtterSec audit of Soroban contracts (4–6 weeks) ├── Veridise ZK circuit audit (3–4 weeks, parallel) └── Remediation and re-audit of critical/high findings Phase 3 ($10M TVL milestone): └── Second full audit (different firm per Audit Bank program) Ongoing: └── Bug bounty program (permanent, Immunefi) ``` ### Bug bounty tiers | Severity | Reward | | - | - | | Critical (funds at risk) | Up to \$50,000 | | High (significant impact) | $10,000–$25,000 | | Medium (limited impact) | $1,000–$5,000 | | Low/Informational | $100–$500 | *** ## Security Checklist Before Mainnet * [ ] All unit tests passing (≥90% coverage) * [ ] Fuzz tests run for minimum 48 hours with no crashes * [ ] External audit completed (OtterSec or equivalent) * [ ] All critical and high findings remediated * [ ] ZK circuit audit completed (Veridise or equivalent) * [ ] Trusted setup ceremony completed and transcript published * [ ] Bug bounty program live on Immunefi * [ ] TVL cap set at \$50K for first 30 days * [ ] Insurance fund seeded with $5K minimum (= 10% of the $50K launch TVL cap, consistent with the 10%-of-TVL target in `docs/research/tokenomics-fee-model.md` - see that doc for the ramp as TVL scales past the launch cap) * [ ] Emergency pause mechanism tested * [ ] Incident response runbook written and team trained *** *Last updated: 2026-06-22* *Sources: [Soroban Audit Bank - stellar.org](https://stellar.org/grants-and-funding/soroban-audit-bank) · [CertiK: Soroban Contract State Management](https://www.certik.com/blog/soroban-contract-state-management) · [OtterSec](https://osec.io/) · [Veridise Soroban Audits](https://veridise.com/audits/soroban/)* # Research: Soroban Compute Cost & Resource Limits Source: https://docs.writz.xyz/research/soroban-compute-benchmarks **Author:** Research **Date:** 2026-06-22 **Status:** Complete **Roadmap task:** Phase 0.3 *** ## Overview Before writing a single line of Writz contract code, we need to know whether the core operations - Bitcoin SPV verification and ZK proof verification - are feasible within Soroban's resource constraints. This document establishes the cost baseline. *** ## Soroban Resource Model Soroban fees are determined by **six resource dimensions**, each metered independently: | Resource | What it measures | Fee type | | - | - | - | | **CPU Instructions** | Total Wasm + host instructions executed | Non-refundable | | **Ledger Entry Reads** | Number of ledger keys read | Non-refundable | | **Ledger Entry Writes** | Number of ledger keys written | Non-refundable | | **Read Bytes** | Total bytes read from ledger | Non-refundable | | **Write Bytes** | Total bytes written to ledger | Non-refundable | | **Transaction Size** | Serialized transaction size | Non-refundable | | **Events & Return Value** | Size of emitted events | Refundable | | **Ledger Space Rent** | Storage rent for persistent entries | Refundable | **Key insight:** Only Events and Ledger Space Rent are refundable. CPU instructions are always paid, even if the transaction fails after the first instruction. This means simulation before submission (`simulateTransaction`) is critical for estimating costs accurately. *** ## CPU Instruction Limits ### Per-transaction budget (Protocol 23+, parallel execution era) Based on current Stellar documentation and Protocol 26 mainnet parameters: | Parameter | Value | | - | - | | Max instructions per transaction | \~100 million (approximate mainnet) | | Typical Groth16 SNARK verification | \~40 million instructions | | Protocol 26 MSM host function | Significantly cheaper (host-layer, not Wasm) | | Simulation margin of error | ±20% + 3M minimum floor | **The 40M instruction figure for Groth16 verification comes directly from Stellar's own documentation** and is approximately 40% of the per-transaction maximum - feasible but not trivial. ### Why Protocol 26 matters for cost Before Protocol 26, multi-scalar multiplication (MSM) - the dominant computation in Groth16 verification - had to run in Wasm and was counted instruction-by-instruction. After Protocol 26, `bn254_msm` is a host function: it executes at native speed and is billed as a fixed cost at the host layer, dramatically reducing effective instruction consumption for ZK verification. **Practical implication for Writz:** ZK position proofs should become significantly cheaper after Protocol 26. The 40M instruction figure is likely a pre-Protocol-26 measurement. Real benchmarking post-Protocol-26 is a required Phase 1 task. *** ## Cost Benchmarks - Current Data ### Average transaction costs on Soroban (Protocol 25+ era) From analysis of 220 community protocol transactions: * **Average minimum resource fee:** \~215,000 stroops (0.0215 XLM) * \*\*At current XLM price (\~$0.40):** ~$0.0086 per transaction **For Writz specifically:** | Operation | Estimated instruction cost | Estimated fee (XLM) | | - | - | - | | Groth16 ZK proof verification | \~40M instructions | \~0.02–0.05 XLM | | Bitcoin Merkle proof verification | \~5–15M instructions (estimate) | \~0.005–0.015 XLM | | Lending position update (read/write) | \~1–3M instructions | \~0.001–0.003 XLM | | Full deposit flow (SPV + ZK + state) | \~50–60M instructions | \~0.025–0.06 XLM | **At $0.40 XLM, a full deposit costs approximately $0.01–0.024.** This is acceptable for DeFi - substantially cheaper than Ethereum, where comparable operations cost \$5–50+. *** ## Resource Constraints for Writz Operations ### 1. SPV Verification (Bitcoin Merkle proof) Bitcoin SPV verification requires: * `SHA256d` (double-SHA256) for each level of the Merkle tree: typically 12–13 levels for a block with \~1000–4000 transactions * Block header PoW check: `SHA256d(header) < target` - two SHA256d calls * Header chain validation: `prev_block_hash` check for each header in the chain **Wasm SHA256 cost estimate:** Each SHA256d is \~10,000–20,000 instructions in Wasm. For a 13-level Merkle proof: \~130,000–260,000 instructions for the proof itself. Header chain (6 headers): \~120,000–240,000 additional instructions. **Total SPV estimate:** 5–15M instructions - well within budget, even combined with ZK verification in the same transaction. **Important:** Soroban does NOT have a native SHA256d host function. Bitcoin uses double-SHA256 (`SHA256(SHA256(data))`), not single SHA256. The circuit must implement this in Wasm. This adds cost vs. SHA256, but is manageable. ### 2. ZK Proof Verification (Groth16 / Circom) The Groth16 verifier requires: * Bilinear pairing check (the most expensive operation - `bn254_pairing` host function after P25) * Multi-scalar multiplication (MSM) - `bn254_msm` host function after P26 * Field arithmetic operations - `bn254_field_*` host functions after P26 With Protocol 26's host functions, the bulk of the ZK verification cost moves off the Wasm instruction counter. The remaining Wasm work is mostly data marshaling and memory operations. **Post-P26 estimate:** Groth16 verification could be 40–60% cheaper than the pre-P26 40M baseline. Targeting \~20–25M instructions post-P26. ### 3. Combined transaction (deposit = SPV + ZK + state write) ``` SPV verification: ~10M instructions ZK proof verify: ~25M instructions (post-P26) State reads/writes: ~2M instructions Total: ~37M instructions ``` This fits comfortably within a 100M instruction budget with headroom for circuit complexity growth. ### 4. Transaction size constraints A Soroban transaction's physical size affects fees. Key sizes to manage: * Bitcoin block headers: 80 bytes each × 6 headers = 480 bytes * Merkle proof: \~13 × 32 bytes = 416 bytes * Raw Bitcoin transaction: varies (200–1,000 bytes typically) * Groth16 proof: 192 bytes (fixed for Groth16) * ZK public inputs: depends on circuit, typically 32–128 bytes **Total transaction payload estimate:** \~1.5–2.5 KB - within Soroban's limits. *** ## Ledger Storage Considerations ### What Writz stores on-chain | Data | Storage type | Size | Notes | | - | - | - | - | | Position commitments (Merkle tree) | Persistent | 32 bytes per commitment | Grows with users | | Nullifier set | Persistent | 32 bytes per nullifier | Grows with operations | | USDC pool state | Instance | \~200 bytes | Small, fixed size | | Protocol config | Instance | \~100 bytes | Minimal | ### CertiK's warning: unbounded storage growth CertiK specifically flags **unbounded storage growth** as a critical Soroban vulnerability class. If the commitment Merkle tree or nullifier set is stored in `instance` storage, they grow indefinitely and can cause DoS by exceeding storage limits or making reads prohibitively expensive. **Writz design requirement:** Store individual commitments and nullifiers in separate `persistent` ledger entries keyed by their hash. Never store growing collections in a single `instance` storage entry. Each entry is a fixed-size 32-byte hash - cost is predictable and bounded per user. *** ## Cost Simulation Strategy Before deploying to testnet, Writz must benchmark all critical operations using `soroban-rpc simulateTransaction`. The simulation workflow: ``` 1. Write Soroban contract (Rust) 2. Compile to WASM 3. Deploy to Stellar testnet 4. Call simulateTransaction with real inputs: - Real Bitcoin transaction + Merkle proof + 6 block headers - Real Groth16 proof (generated by Circom/snarkjs) 5. Record actual instruction counts, not estimates 6. Verify the 20% simulation margin is acceptable 7. Measure at different Bitcoin block sizes (100 tx vs 4000 tx → different Merkle depths) ``` *** ## Key Findings 1. **SPV verification is feasible** (\~5–15M instructions) - well within budget 2. **ZK Groth16 verification is feasible** (\~25–40M instructions post-P26) - within budget 3. **Combined deposit transaction is feasible** (\~37–55M instructions) - fits in one transaction 4. **Cost is DeFi-acceptable** (\~$0.01–0.06 per operation vs $5–50 on Ethereum) 5. **Unbounded storage growth is the main storage risk** - use per-entry persistent storage, never growing instance collections 6. **Real benchmarking required in Phase 1** - these are estimates; `simulateTransaction` on testnet with real proofs is mandatory before mainnet *** ## Phase 1 Benchmarking Checklist * [ ] Benchmark SHA256d implementation in Soroban Wasm * [ ] Benchmark Merkle proof verification at 3 depths: log2(100), log2(1000), log2(4000) transactions * [ ] Benchmark block header chain validation for 3, 6, 10 headers * [ ] Benchmark Groth16 verifier using Protocol 26 BN254 host functions * [ ] Benchmark full deposit flow (SPV + ZK + state writes) end-to-end * [ ] Measure XLM cost in USD at current price * [ ] Identify instruction-count bottlenecks and optimize *** *Last updated: 2026-06-22* *Sources: [Soroban Fees & Metering - Stellar Docs](https://soroban.stellar.org/docs/soroban-internals/fees-and-metering) · [CertiK: Soroban Contract State Management](https://www.certik.com/blog/soroban-contract-state-management) · [How Much Do Soroban Fees Cost - CheesecakeLabs](https://cheesecakelabs.com/blog/how-much-do-soroban-fees-cost/) · [Stellar ZK Proofs Docs](https://developers.stellar.org/docs/build/apps/zk)* # Research: Bitcoin SPV Implementations on Other Chains Source: https://docs.writz.xyz/research/spv-implementations **Author:** Justin (Business Analyst) **Date:** 2026-06-22 **Status:** Complete - initial survey **Relevance:** Directly informs Writz Protocol's core technical approach > **Updated 2026-10-26.** Two points of this June 2026 survey are superseded. Writz did not adopt the stateless approach it recommends: `bitcoin-spv` is an on-chain header light client, because a stateless design lets a caller present a header chain it mined privately (see [How SPV Verification Works](/how-it-works/spv-verification) and [SPV vs Wrapped Bitcoin](/security/spv-vs-wrapped)). And Interlay/interBTC is no longer live: the project has wound down and its repository is archived. *** ## Overview Bitcoin SPV (Simplified Payment Verification) allows a system to verify that a Bitcoin transaction occurred without running a full Bitcoin node. It works by checking a Merkle proof against a block header - a computationally cheap operation that can be performed inside a smart contract. This document surveys every meaningful attempt to implement Bitcoin SPV verification on another blockchain, extracting lessons that directly apply to building the SPV client on Stellar's Soroban. *** ## 1. BTC Relay - Ethereum (2016) **Status:** Deprecated / inactive **Chain:** Ethereum (Solidity) **Repository:** [github.com/ethereum/btcrelay](https://github.com/ethereum/btcrelay) ### What it was BTC Relay was the first implementation of a Bitcoin light client on another blockchain. It stored Bitcoin block headers in an Ethereum smart contract, building a mini Bitcoin chain on Ethereum. Other contracts could then verify that a specific BTC transaction was included in a confirmed block. ### How it worked 1. **Relayers** submitted Bitcoin block headers to the Ethereum contract 2. The contract maintained a chain of verified headers 3. Any Ethereum application could query the contract to verify a BTC transaction using a Merkle proof 4. Relayers were compensated with fees when their submitted headers were used for verification ### Why it failed | Problem | Detail | | - | - | | **Relayer incentive failure** | When usage dropped, fees dried up. Relayers had no reason to keep submitting headers. The system became stale and stopped tracking Bitcoin. | | **Gas cost at scale** | Storing every Bitcoin block header on Ethereum was expensive. Each header is 80 bytes, but Ethereum storage costs made this economically unfeasible at high throughput. | | **No liveness guarantee** | There was no mechanism to ensure headers would always be submitted. The system had a single point of liveness failure. | | **Orphan chain handling** | Bitcoin occasionally produces orphan blocks (temporary forks). The contract needed to handle reorgs, which added significant complexity. | ### What worked * The **cryptographic verification was correct** - Merkle proof validation worked as designed * The **SPV approach was sound** - the concept proved valid even if the implementation had issues * It demonstrated that cross-chain Bitcoin verification was technically possible ### Key lesson for Writz > The cryptographic core is not the problem. **Relayer incentive design is the #1 existential risk.** Writz must build economic incentives for header relaying directly into the protocol fee structure from day one. *** ## 2. summa-tx/bitcoin-spv - Multi-chain (2019–present) **Status:** Actively maintained (last update: 2024) **Chains:** EVM (Ethereum, Celo), Cosmos SDK, others **Repository:** [github.com/summa-tx/bitcoin-spv](https://github.com/summa-tx/bitcoin-spv) ### What it is A low-level toolkit for Bitcoin SPV proof verification. Unlike BTC Relay, it does NOT maintain a chain of block headers on-chain. Instead, it verifies a **specific slice of headers** provided at call time - a "stateless" approach. Available implementations: * **Solidity** - for EVM chains * **Rust** - directly applicable to Soroban * **Go** - for Cosmos SDK chains * **Python** - for tooling * **JavaScript/ES6** - for off-chain tooling ### The stateless SPV insight Traditional SPV (BTC Relay) is *stateful*: it stores and tracks the full Bitcoin header chain on-chain. This is expensive and creates liveness dependency. Stateless SPV shifts the storage burden off-chain. The caller provides: 1. A set of Bitcoin block headers proving sufficient proof-of-work 2. A Merkle inclusion proof for their specific transaction The contract verifies everything in a single call without storing anything permanently. **Tradeoff:** Stateless SPV is cheaper and simpler but requires the caller to provide valid headers. It assumes a reasonably honest environment (the Bitcoin network consensus provides this guarantee implicitly). ### What this means for Writz This is the **most directly relevant existing work**. The Rust library (`bitcoin-spv` in Rust) is written in the same language as Soroban contracts. It provides: * `validateHeader()` - verifies a Bitcoin block header is valid (correct PoW, correct format) * `validateHeaderChain()` - verifies a sequence of headers forms a valid chain * `prove()` - verifies a transaction is included in a block via Merkle proof * `extractTxOutputValue()` - extracts the output value from a Bitcoin transaction **Recommended action:** Evaluate the Rust implementation of `summa-tx/bitcoin-spv` as the starting point for the Soroban SPV contract, rather than implementing from scratch. *** ## 3. Interlay / interBTC - Polkadot (2021–2026) **Status:** Wound down in 2026; the repository is archived (checked 2026-10-26). Previously deployed on Polkadot (Interlay) and Kusama (Kintsugi). Not to be cited as a live system. **Chain:** Substrate/Polkadot (Rust) **Repository:** [github.com/interlay/interbtc](https://github.com/interlay/interbtc) **Spec:** [github.com/interlay/interbtc-spec](https://github.com/interlay/interbtc-spec) ### What it is The most complete production implementation of trustless BTC DeFi on another chain. interBTC is a 1:1 Bitcoin-backed asset secured by a network of decentralized vault operators who lock collateral (DOT/KSM) as insurance. ### Architecture ``` Bitcoin Network Polkadot/Substrate ─────────────── ────────────────────────── User BTC Wallet BTC-Relay pallet (SPV) │ │ │ BTC → Vault address │ Verifies Bitcoin tx └────────────────────────────►│ via Merkle proof ▼ Issue pallet (mints iBTC) │ ▼ Vault operators (collateralized) If vault steals BTC → collateral slashed → user repaid ``` ### Key innovations **Collateralized vault system:** Vault operators lock overcollateralized DOT/KSM. If they steal the BTC, their collateral is automatically slashed and distributed to the user. This solves the "what if the custodian steals the BTC?" problem without requiring trustlessness at the cryptographic level. **BTC-Relay pallet:** The SPV verification component is implemented as a Substrate pallet (module) in Rust. It maintains a chain of Bitcoin block headers on-chain and provides inclusion proof verification. Similar to BTC Relay but in a more efficient Rust/Substrate environment. **Relayer incentives:** Anyone can submit Bitcoin headers and earn fees. The system is permissionless for relayers. ### Challenges * Polkadot ecosystem adoption has been slower than expected * Vault operator bootstrapping is a chicken-and-egg problem * Collateralization requirements can be capital-inefficient for vault operators ### Key lessons for Writz | Lesson | Application | | - | - | | The vault collateral model solves theft risk | Writz can consider a similar model: relayer/vault operators stake USDC or XLM as insurance | | Header chain maintenance in Rust is feasible | The BTC-Relay pallet in Rust is a direct reference for the Soroban implementation | | Permissionless relayers with fee incentives work | Design the relayer fee model into the Writz protocol from the start | | Polkadot's Rust environment is closest to Soroban | The interBTC Rust code is the most transferable reference for Writz | *** ## 4. Solana BTC SPV - Experimental (2020–2021) **Status:** Minimal maintenance **Chain:** Solana **Packages:** `solana-btc-spv-program` (v1.2.32), `solana-btc-spv-api` (v0.19.1) **Reference:** [lib.rs/crates/solana-btc-spv-program](https://lib.rs/crates/solana-btc-spv-program) ### What it was An experimental implementation of Bitcoin SPV verification as a Solana on-chain program. There was also an open GitHub issue on the main Solana repository proposing a "marketplace for Bitcoin transaction proofs" where users could request and providers could supply SPV proofs. ### Why it didn't progress * Never achieved production readiness or meaningful adoption * Solana's programming model at the time made on-chain Bitcoin header verification expensive in terms of compute units * The ecosystem prioritized other infrastructure ### Key lesson for Writz Solana attempted this and stalled - not because the idea was wrong, but because the ecosystem incentives and developer focus were elsewhere. Stellar's situation is different: the SDF actively wants BTC to enter the ecosystem, and Soroban's fee model is more favorable. *** ## 5. Succinct Labs SP1 + BitVM - Bitcoin-Native ZK (2024–2026) **Status:** Active / cutting-edge **Direction:** ZK proofs ON Bitcoin (reverse direction) **References:** * [blog.succinct.xyz/bitcoin-sp1](https://blog.succinct.xyz/bitcoin-sp1/) * [zkBitcoin on zksecurity.xyz](https://blog.zksecurity.xyz/posts/zkbitcoin/) ### What it is SP1 is a zkVM (zero-knowledge virtual machine) that can prove the execution of arbitrary Rust programs. It is optimized for Bitcoin. Combined with **BitVM** - a computing paradigm that allows Bitcoin to verify ZK proofs without changing Bitcoin's consensus rules - it enables a new class of trust-minimized Bitcoin bridges. ### How it works BitVM uses an optimistic challenge-response model: 1. A prover claims "I executed program X with result Y" 2. Anyone can challenge this claim on Bitcoin 3. If the claim is false, the challenger wins the prover's bond 4. If no challenge succeeds, the claim is accepted This allows Bitcoin to effectively verify ZK proofs by making fraud economically unviable rather than cryptographically impossible. ### Why this matters for Writz (long-term) This is the **long-term convergence point** of ZK and Bitcoin. Today, Writz uses an SPV client on Soroban to verify Bitcoin state from Stellar's side. In the future, BitVM + SP1 could enable the reverse: Bitcoin verifying state from Stellar, enabling fully trustless bidirectional verification. ### Current limitations * BitVM is still experimental - real-world deployments are limited * Proof generation for complex programs can take minutes * The challenge-response model adds latency and requires capital at risk * Not ready for production DeFi today ### Key lesson for Writz This is the **Phase 3 vision** for Writz Protocol - not the starting point. Track actively. When BitVM matures to production readiness, Writz should be the first protocol on Stellar to integrate it. *** ## Comparative Summary | Project | Chain | Approach | Status | Rust Code? | Lessons Weight | | - | - | - | - | - | - | | **BTC Relay** | Ethereum | Stateful headers on-chain | Deprecated | No (Solidity) | High - what NOT to do | | **summa-tx/bitcoin-spv** | Multi-chain | Stateless SPV toolkit | Active | ✓ Yes | Very high - direct reference | | **Interlay/interBTC** | Polkadot | SPV + collateralized vaults | Wound down, archived | ✓ Yes | Very high - closest to Writz | | **Solana BTC SPV** | Solana | Experimental program | Minimal | ✓ Yes | Medium - shows pitfalls | | **SP1 + BitVM** | Bitcoin-native | ZK proofs on Bitcoin | Cutting-edge | ✓ Yes | Medium - long-term vision | *** ## Critical Findings for Writz Protocol ### 1. Start with stateless SPV (summa-tx approach) - superseded Original recommendation: don't build a stateful header chain, because it is expensive and creates relayer dependency; let the caller provide headers at verification time. Writz later moved to an on-chain header light client, because a stateless contract cannot tell an honest header chain from one the caller mined privately. The relayer dependency this finding warned about is real and is handled by redundant relayers and an on-chain freshness rule (planned). ### 2. The Rust ecosystem is already there Both `summa-tx/bitcoin-spv` (Rust) and `interbtc` (Rust/Substrate) are written in Rust. Soroban contracts are written in Rust. This is a significant advantage - the hardest parts of the implementation have precedent in the same language. ### 3. Relayer incentives must be designed from day 0 Every failed implementation either ignored relayer incentives or added them as an afterthought. With an on-chain header light client, relayers are on the critical path: someone has to keep submitting headers, or the contract's view of Bitcoin goes stale. A small fee mechanism for header provision should be designed into the protocol. ### 4. Bitcoin confirmation time is a UX problem, not a technical one 6 confirmations ≈ 60 minutes. A tiered confirmation option was evaluated in `docs/research/bitcoin-locking-script.md` - fast lane (3 confirmations, capped deposit size) vs. standard lane (6 confirmations) - but was not adopted for the current spec. Writz requires a flat 6 confirmations for all deposit sizes; see `docs/products/writz-lend.md` and `docs/how-it-works/spv-verification.md` for the adopted policy. ### 5. The SPV math is solved - focus engineering effort on economics The cryptographic verification (SHA256d, Merkle proofs, header validation) is well-understood and implemented. The unsolved problems are: incentive design, reorg handling, UX around Bitcoin's slow finality, and the BTC locking mechanism on the Bitcoin side. *** ## Recommended Next Steps 1. **Fork and study** the Rust implementation of `summa-tx/bitcoin-spv` - map every function to what Writz needs 2. **Read the interBTC specification** (`interlay/interbtc-spec`) - it's the most complete formal specification of a trustless BTC bridge and directly applicable 3. **Prototype stateless SPV verification** in a Soroban contract on testnet - target: verify a single mainnet Bitcoin transaction 4. **Design the P2WSH locking script** - the Bitcoin-side of the mechanism that prevents users from moving BTC after depositing *** *Last updated: 2026-06-22* *Sources: [BTC Relay](https://github.com/ethereum/btcrelay) · [summa-tx/bitcoin-spv](https://github.com/summa-tx/bitcoin-spv) · [interlay/interbtc](https://github.com/interlay/interbtc) · [Succinct Labs SP1](https://blog.succinct.xyz/bitcoin-sp1/) · [SoK: Blockchain Light Clients](https://fc22.ifca.ai/preproceedings/176.pdf)* # Research: Tokenomics & Fee Model Source: https://docs.writz.xyz/research/tokenomics-fee-model **Author:** Research **Date:** 2026-06-22 **Status:** Complete *** ## Overview Writz Protocol's economic sustainability depends on a well-designed fee model. This document defines all revenue streams, the protocol fee structure, treasury allocation, and the governance token strategy - informed by the 2025–2026 shift in DeFi toward real-yield tokenomics. *** ## Industry Context: The Real-Yield Shift (2025–2026) The DeFi industry has fundamentally changed its tokenomics approach: **Old model (2020–2023):** Emit governance tokens as liquidity mining rewards. Users farm tokens, dump them, APYs collapse, protocol dies. **New model (2024–2026):** * Uniswap: 17% of swap fees auto-buy and burn UNI tokens * Aave's "Aave Will Win": protocol revenue directly tied to AAVE token through buyback mechanism * Compound, Curve: fee revenue distributed to stakers/veTokens **The paradigm:** Governance tokens must be backed by real protocol revenue, not emissions. Writz will design tokenomics from day one around real cash flows. *** ## Revenue Streams ### Primary: Lending Spread (Writz Lend) The difference between the borrow rate (what borrowers pay) and the supply rate (what lenders earn). ``` At 75% utilization (Uoptimal): Borrow rate: 8% APR Supply rate: 6.8% APR (= 8% × 75% × 85%) Protocol spread: 1.2% APR on borrowed amount ``` At $1M TVL with 75% utilization ($750K borrowed): \~\$9,000/year in spread revenue. ### Secondary: SPV Verification API Other Stellar protocols pay to use Writz's Bitcoin SPV client. **Pricing model:** * Per-verification fee: $0.10–$0.50 per proof verification * Monthly subscription: $500–$5,000/month for high-volume protocols Early adopter pricing is aggressive - the goal is ecosystem adoption, not maximizing API revenue in Year 1. > These two streams belong to planned features, not products, and no code exists for them. Figures for them come from the June 2026 model and are not a forecast. See [Vision](/roadmap/vision#planned-for-later). ### Tertiary: Swap Fees (planned feature) Basis points on BTC/USDC swaps. Target: 0.3% per swap (comparable to Uniswap v3). At $10M monthly swap volume: $30,000/month in swap fees. ### Quaternary: ZK Proof of Reserve (planned feature) Enterprise customers pay for private, verifiable BTC reserve attestations. **Pricing model:** * Starter: \$500/month - up to 5 attestations * Professional: \$2,000/month - unlimited attestations, custom reporting * Enterprise: \$10,000+/month - SLA, dedicated support, compliance documentation Target: 10 paying enterprise customers in Year 1 = $20,000–$100,000/year. ### Liquidation Fees 2% of liquidated collateral value goes to the protocol. At 10 liquidations/month averaging $20,000 each: $4,000/month. *** ## Fee Distribution All protocol revenue flows into a distribution contract that routes funds to: ``` 100% of Protocol Revenue ├── 30% → Insurance Fund (on-chain safety reserve) ├── 30% → Token Buyback & Burn (reduces supply, supports token price) ├── 25% → Operations Treasury (salaries, infrastructure, audits) └── 15% → Ecosystem Grants (developer grants, integrations) ``` **Insurance Fund:** Accumulates until it reaches 10% of TVL. After that, excess flows to buyback instead. This ensures the protocol can cover bad debt without relying on tokenomics. **Buyback & Burn:** Protocol buys WRTZ tokens from the open market and burns them. This creates deflationary pressure tied directly to protocol usage - more borrowers = more revenue = more buybacks = less token supply. *** ## Governance Token: WRTZ > **Scope status:** This is a problem-and-hypothesis exploration, not a committed design. No contract, governance mechanism, or legal analysis of WRTZ's status as a security in target jurisdictions exists today. Token launch is explicitly gated on post-\$5M TVL (see "When to launch the token" below), so there is no urgency to finalize this - treat the distribution table and utility design as directional, not final, until closer to that milestone and until legal counsel has reviewed the security-classification question. ### Design principles * Total supply: **100,000,000 WRTZ** (fixed, no inflation) * 100% backed by real protocol revenue (no liquidity mining emissions) * Governance rights over protocol parameters * Revenue sharing via buyback/burn (not direct dividends - cleaner tax treatment) ### Distribution | Allocation | % | Amount | Vesting | | - | - | - | - | | Team | 20% | 20M | 4 years, 1-year cliff | | Investors (seed) | 15% | 15M | 2 years, 6-month cliff | | Ecosystem/grants | 20% | 20M | 3 years, monthly release | | Community/DAO treasury | 25% | 25M | Governed by DAO | | Protocol-owned liquidity | 10% | 10M | Used to seed USDC pools | | Public launch | 10% | 10M | IDO / fair launch | **No pre-mine for team beyond the 20% with vesting.** The community treasury (25%) is controlled by WRTZ governance from day one. ### Token utility 1. **Governance:** Vote on protocol parameters (interest rate curves, collateral ratios, fee splits, new features) 2. **Fee capture:** Buyback/burn mechanism means holding WRTZ benefits from protocol growth 3. **Staking for enhanced yields:** WRTZ stakers receive 10% boost on USDC lending yields (creates demand for staking) 4. **Liquidation priority:** WRTZ stakers have first access to liquidation opportunities (creates demand from keeper operators) ### When to launch the token **Not in Phase 1 or Phase 2.** Token launches before product-market fit destroy communities and set unrealistic expectations. **Token launch criteria:** * \$5M TVL sustained for 60+ days * 500+ active users * At least one completed external audit * Clear governance use cases ready to deploy Expected timeline: Q2–Q3 2027. *** ## Financial Projections (June 2026 model, not a forecast) These tables include revenue from planned features that have no code. ### Year 1 (2027, post-launch) | Revenue Stream | Monthly | Annual | | - | - | - | | Lending spread (@ \$2M TVL, 75% util) | \$1,500 | \$18,000 | | Dark Swap fees (\$500K/month volume) | \$1,500 | \$18,000 | | SPV API | \$500 | \$6,000 | | Proof of Reserve (5 customers) | \$5,000 | \$60,000 | | Liquidation fees | \$1,000 | \$12,000 | | **Total** | **\$9,500/month** | **\$114,000/year** | ### Year 2 (2028) | Revenue Stream | Monthly | Annual | | - | - | - | | Lending spread (@ \$20M TVL, 75% util) | \$15,000 | \$180,000 | | Dark Swap fees (\$5M/month volume) | \$15,000 | \$180,000 | | SPV API | \$5,000 | \$60,000 | | Proof of Reserve (25 customers) | \$25,000 | \$300,000 | | Liquidation fees | \$5,000 | \$60,000 | | **Total** | **\$65,000/month** | **\$780,000/year** | These are conservative estimates. At BTCfi's current 28× annual TVL growth rate, the upside scenario significantly exceeds these numbers. *** ## Key Decisions | Decision | Choice | Rationale | | - | - | - | | Token model | Real-yield, buyback/burn | 2026 industry standard; no inflationary emissions | | Token supply | 100M fixed | Simple, no inflation | | Token launch timing | Post \$5M TVL | Product-market fit first | | Protocol fee % | 15% of interest spread (`PROTOCOL_FEE_BP = 1_500` in contract) | Higher than Aave (10%) due to ZK infrastructure costs | | Mainnet bootstrap fee waiver (option, not decided) | One option under evaluation: a reduced or 0% protocol fee for an initial period after mainnet launch | Would attract early liquidity; the 15% fee is a contract constant, so any waiver is a launch-time parameter change. Not a commitment | | Insurance fund target | 10% of TVL | Industry standard; covers typical bad debt scenarios | | Revenue distribution | 30/30/25/15 | Balanced between safety, token health, operations, growth | *** ### Insurance Fund Ramp The $5K launch seed (`docs/research/security-audit-strategy.md` pre-mainnet checklist) is exactly 10% of the $50K launch TVL cap, so it starts on-target. As the TVL cap is raised, the fund needs to keep pace: | TVL cap | 10% target | Funding source | | - | - | - | | \$50K (launch) | \$5K | Seeded directly at launch | | \$250K (post 30-day clean operation) | \$25K | Must accrue from protocol fee revenue (30% slice) between launch and the cap raise | | \$5M+ (Phase 3 scale) | \$500K+ | Ongoing accrual from protocol fee revenue | **Open gap:** the fund is meant to grow from the 30% "safety" slice of protocol fee revenue, but if the bootstrap fee waiver above is adopted, the protocol fee would be 0% for that period. In that case the fund cannot accrue past its initial $5K seed during the exact window when TVL is scaling from $50K toward \$250K, unless a separate top-up is planned. This needs an explicit answer before the 30-day TVL cap raise is exercised - either delay the cap raise until the fund has organically caught up, or fund the gap from another source. *** *Last updated: 2026-06-22* *Sources: [DeFi Protocol Revenue Rankings - DefiLlama](https://defillama.com/revenue) · [Aave Interest Rate Model](https://rareskills.io/post/aave-interest-rate-model) · [DeFi 2.0 Lending Protocols](https://1bitup.com/blog/defi-lending-protocols)* # Vision Source: https://docs.writz.xyz/roadmap/vision **In 2028, any Bitcoin holder in the world will be able to borrow USDC against real BTC, in minutes, without giving up custody - from their phone.** That is the Writz vision. No custodian. No bridge. No wrapped BTC. A transaction that starts with Bitcoin in your wallet and ends with USDC in your account, enforced entirely by math. *** ## Why This Vision Is Worth Building Bitcoin is the world's largest crypto asset by market cap. Over 900 million people own it. The vast majority of them - 77% by most estimates - have never used it in DeFi. Not because they don't want to. Because every option today requires one of two concessions: give your BTC to a custodian or signer set (WBTC, tBTC), or bridge it onto another chain as a wrapped token. Writz removes both. The BTC stays on Bitcoin - secured by Bitcoin Script, accessible only by the user. The loan happens on Stellar - where native USDC lives - and the deposit is verified there by an on-chain Bitcoin light client, not by a bridge or an oracle. If Writz disappears, a timelock in the Bitcoin script lets the user take the BTC back alone. This is not an incremental improvement on existing BTCfi. It is a different category. *** ## The Two-Layer Moat Writz's competitive position is not built on branding or funding. It is built on compounding technical depth. ### Layer 1 - Time Building a Bitcoin SPV client on Soroban from scratch and getting it audited takes 12–18 months. A well-funded team starting today arrives in late 2027 - by which time Writz will have: * Over a year of mainnet operation with no security incidents * Real TVL, organic user growth, and verifiable on-chain metrics * An open-source SPV SDK integrated into the Stellar ecosystem The first-mover in a technical niche this specific tends to hold the position. The cost of switching from an established, audited protocol to an unproven one is too high for rational users. ### Layer 2 - Ecosystem Lock-in The Writz Bitcoin SPV SDK is free infrastructure for the Stellar ecosystem. Every Stellar wallet that integrates it, every protocol that builds on it, every developer who ships a product using it: these are switching costs that accumulate invisibly. By the time there are 5 integrations, Writz is load-bearing infrastructure for Stellar's Bitcoin-awareness. By 10 integrations, Writz is the canonical answer to "how does Bitcoin work on Stellar?" Replacing it requires not just building a better contract - it requires convincing every integration partner to migrate simultaneously. *** ## Writz in 2028 - The Full Picture ### \$100M+ TVL in Writz Lend Bitcoin holders trust Writz because: * The smart contracts have passed external audits * Over a year of mainnet operation with zero critical incidents * TVL grew progressively from $50K → $250K → $1M → $5M → $20M → $100M The protocol generates real revenue: at $100M TVL and a 5% average borrow rate, with a 15% protocol fee, annual revenue is approximately $750,000 - enough to fund ongoing development, audits, and the insurance fund. ### The WRTZ Token - Real Yield, No Speculation The WRTZ token launches at \$5M TVL (Q2–Q3 2027), with a community-first structure: * Fair IDO / Liquidity Bootstrapping Pool - no VC cliff dumps at steep discounts * Real-yield mechanics: protocol revenue is used to buy and burn WRTZ from the open market * Governance: WRTZ holders vote on TVL cap increases, fee adjustments, new product launches, and insurance fund payouts This is not a token launched to raise money. It is a token launched to give ownership to the community that built the TVL. ### Stellar Ecosystem Infrastructure The Writz Bitcoin SPV SDK is integrated into: * Xverse (BTC wallet with a "Writz" button in the DeFi section) * Freighter (Stellar wallet with BTC collateral option) * Lobstr (BTC yield product in the savings section) * 7+ Stellar DeFi protocols using the SDK for Bitcoin verification Writz has proposed and is co-authoring a Stellar SEP (Standards Evolution Proposal) for Bitcoin SPV on Stellar - establishing the verification interface as a standard across the ecosystem. ### Institutional Presence Writz has 5+ institutional deposits of \$100K+ each - crypto funds, family offices, and mining companies using the protocol for treasury management. Institutions that need confidential positions are the audience of the separate, opt-in [privacy edition](/how-it-works/privacy-edition), still in progress, which is not part of the Writz protocol. *** ## Planned for later Writz has one product: Writz Lend. The SPV SDK is the open infrastructure behind it. The features below are directions we want to explore after Writz Lend is live on mainnet. They are not products. None has a date, and none is promised. * **Privacy.** A separate, opt-in edition where a borrower proves they hold enough collateral without showing the amount. The design and testnet contracts exist. See [Privacy edition](/how-it-works/privacy-edition). * **ZK proof of reserve.** A way for a company to prove it holds Bitcoin without revealing its addresses or the exact amount. No circuit exists. * **Private conversion.** Turning Bitcoin into USDC directly, without Writz ever holding the Bitcoin. Nothing is built, and no design meets that condition yet. * **Automatic yield.** Putting borrowed USDC to work without adding risk the borrower did not choose. Nothing is built. *** ## The Bigger Picture Writz is not just a product. It is the protocol that connects the world's largest crypto asset to the world's most compliance-friendly blockchain. Bitcoin is the foundation. Stellar is the network. The house is where the financial future lives. What starts as a lending protocol becomes infrastructure. The SPV SDK becomes the standard. The USDC loans become a gateway for BTC holders who have never touched DeFi. That is the 2028 vision. The first step is a single Bitcoin transaction, verified by a Soroban contract, followed by the first USDC loan issued against real BTC collateral on mainnet. That step is months away. # Audits Source: https://docs.writz.xyz/security/audits **Current status: Pre-audit. Mainnet will not launch before audit completion.** Writz will not launch on mainnet with real funds until an independent security audit of the Soroban smart contracts (`bitcoin-spv`, `writz-lend`) is complete. The ZK circuits belong to the separate privacy edition (in progress) and get their own audit before that edition launches. *** ## Audit Strategy Writz uses the **Stellar Foundation's Audit Bank** program, which subsidizes the cost of smart contract audits for qualifying Soroban projects. This is a separate program from the SCF Build Award - the Audit Bank covers up to 100% of audit costs for projects that meet the readiness criteria. ### Audit Readiness Criteria (Writz's current status) | Criterion | Status | | - | - | | SCF Build Award received | Pending | | Contracts deployed on testnet with passing tests | ✓ Done - 263/263 tests | | STRIDE threat model completed | v0.1 draft complete - [`docs/security/stride-threat-model.md`](/security/stride-threat-model). Full revision planned before the audit | | Self-service security scan completed | Pending - Phase 2 task | | Integration tests covering all flows | ✓ Done - `scripts/deploy/e2e_local.mjs`, run in CI on a local network | | Dataflow diagram produced | Pending - Phase 2 task | *** ## Target Audit Firms ### ZK Circuits - Veridise > The circuits belong to the separate privacy edition (in progress), not to Writz v1. Their audit is that edition's audit and is not on the v1 mainnet path. Veridise specializes in ZK circuit security. They have audited Tornado Cash, Aztec, and multiple Groth16-based systems. ZK circuit bugs (underconstrained circuits, soundness issues) are a category that requires specialized expertise - general smart contract auditors do not have this depth. **Scope:** All three Circom circuits (`deposit.circom`, `borrow_repay.circom`, `liquidation.circom`), the shared Merkle components (`merkle.circom`), and the trusted setup ceremony procedure. **Target timeline:** with the privacy edition, after Writz v1 is live on mainnet. No date. ### Soroban Contracts - OtterSec or Zellic For the Soroban contracts, Writz will engage either OtterSec or Zellic - both are Audit Bank approved firms with Soroban-specific experience. **Scope:** All four Soroban contracts, focusing on: * Storage growth vulnerabilities (the #1 Soroban vulnerability class) * Cross-contract call security * Integer overflow/underflow in interest calculations * Access control: admin-only functions, keeper permissions * Oracle manipulation vectors * Re-entrancy (less common in Soroban but must be checked) **Target timeline:** before mainnet. *** ## Audit Bank Process 1. Submit Audit Bank intake form (provided by SCF upon Build Award approval) 2. Readiness review by SDF security expert (\< 4 weeks) 3. Audit scheduled with approved firm 4. Audit completed; findings categorized as Critical, High, Medium, Low, Informational 5. All Critical/High/Medium findings remediated within 20 business days (required for Audit Bank subsidy) 6. Audit report published publicly 7. Co-payment: 5% of audit cost (refunded if remediation deadline met) *** ## Post-Audit Audits (Growth and Scale) Stellar's Audit Bank provides additional free audits at TVL milestones: | Program | Trigger | Cost to Writz | | - | - | - | | Growth Audit | > \$10M TVL | Free (no co-payment) | | Scale Audit | > \$100M TVL | Free (no co-payment) | These audits are re-assessments as the codebase and TVL grow. Writz intends to apply for both as the protocol scales. *** ## Current Security Posture Before the formal Audit Bank engagement, Writz applies the following: **Automated tooling:** * `cargo clippy` with warnings treated as errors, apart from a short documented allow-list of pre-existing lints * `cargo audit` - dependency vulnerability scanning of every Cargo workspace against the RustSec database, on manifest or lockfile changes and weekly (`.github/workflows/dependency-audit.yml`); any ignored advisory is listed in the workflow with its reason * Custom constraint analysis for ZK circuits (circom-specific tooling) **Test coverage:** * 623 tests covering happy paths and error cases * End-to-end integration tests covering the full deposit→borrow→repay→liquidate cycle * ZK circuit tests verifying proof acceptance for valid inputs and rejection for invalid inputs * Bitcoin transaction tests verifying witness construction against real Bitcoin script interpretation **Known limitations before audit:** * No formal verification of ZK circuits (Veridise will perform this) * No penetration testing of the relayer API * Oracle integration is stubbed in the current testnet deployment (real oracle integration in Phase 2) *** ## Audit History | Date | Scope | Firm | Findings | Report | | - | - | - | - | - | | - | - | - | No audits yet | - | This table will be updated as audits are completed. All published audit reports will be linked here. *** **Next:** [Bug Bounty →](/security/bug-bounty) # Bug Bounty Source: https://docs.writz.xyz/security/bug-bounty **Responsible disclosure program for Writz Protocol.** > ### \[WARNING] Not yet active - no cash rewards are payable today > > Writz runs on **testnet only** and holds no user funds. The paid bug bounty > starts at mainnet launch (targeted Q2 to Q3 2027); the reward amounts below are the > **planned** bands for that program, not offers we can honour right now. > > **Please still report findings.** During testnet we acknowledge, fix, and > credit them, and a finding reported now is honoured at mainnet launch under > the bands below. What we cannot do today is pay on receipt - we would rather > say so plainly than have you assume otherwise. If you find a security vulnerability in Writz Protocol, we want to hear from you before it becomes a problem. *** ## Scope The following are in-scope for the bug bounty program: **Soroban contracts (high priority):** * `bitcoin-spv` - SPV verification, SHA256d implementation, Merkle proofs * `writz-lend` - Lending mechanics, interest accrual, liquidation * `commitment-tree` - privacy edition (in progress, testnet only): ZK lending logic, nullifier set, Merkle tree * `zk-verifier` - privacy edition (in progress, testnet only): Groth16 verification, verification key management **ZK circuits:** * `deposit.circom` - Soundness issues, underconstrained signals * `borrow_repay.circom` - Collateral ratio enforcement, state transition * `liquidation.circom` - Undercollateralization proof, usdc\_debt binding * `merkle.circom` - Poseidon Merkle tree components **Relayer service:** * SPV proof assembly correctness * API authentication and rate limiting * Data integrity issues **Out of scope:** * Theoretical attacks that require physical access to infrastructure * Social engineering attacks * Denial-of-service attacks (network or application layer) * Issues in third-party dependencies (report to the dependency maintainer) * Issues in Stellar or Bitcoin protocols themselves * Findings already documented in known limitations or audit reports *** ## Severity Levels and Rewards (planned - from mainnet launch) Rewards will be paid in USDC on Stellar once the program is active. Amounts are guidelines - actual rewards depend on impact and quality of the report. **None of these are payable during the testnet phase**; see the notice at the top. | Severity | Description | Reward | | - | - | - | | **Critical** | Theft of user funds, ZK circuit soundness bypass, unauthorized BTC access | Up to \$50,000 | | **High** | Significant fund loss risk, privacy leak of positions in the privacy edition contracts, oracle manipulation | Up to \$15,000 | | **Medium** | Denial of service for a specific user, minor fund loss risk, incorrect calculations | Up to \$5,000 | | **Low** | Minor issues, incorrect error handling, non-exploitable edge cases | Up to \$1,000 | | **Informational** | Best-practice improvements, documentation issues | Recognition only | **Critical finding examples:** * A ZK circuit that accepts a proof where the loan-to-value constraint is not enforced * A Soroban contract bug that allows withdrawing more USDC than was deposited * A Bitcoin SPV verification bypass that accepts a fabricated transaction as valid *** ## How to Report **Open a private GitHub security advisory:** [github.com/WritzProtocol/writz/security/advisories/new](https://github.com/WritzProtocol/writz/security/advisories/new) Only you and the maintainers can see it, and it gives us a thread to coordinate the fix and the reward in. See [`SECURITY.md`](https://github.com/WritzProtocol/writz/blob/main/SECURITY.md) for the full policy. > **There is no security mailbox yet.** Earlier versions of this page pointed at > `security@writz.io`, a domain with no DNS at all - mail to it bounced, and at > least one researcher's disclosure was lost that way. The project has since > moved to `writz.xyz`, which is registered but has no MX record yet, so email > there does not arrive either. Use the private advisory link above; this note > goes away once a mailbox is live. **Include in your report:** 1. A description of the vulnerability 2. The affected component (contract name, circuit name, function name) 3. Step-by-step reproduction instructions 4. Proof of concept code or test case (if available) 5. Your assessment of impact and severity 6. Your Stellar wallet address for the reward payment *** ## Disclosure Process 1. **You submit** a [private advisory](https://github.com/WritzProtocol/writz/security/advisories/new) 2. **We acknowledge** within 48 hours 3. **We assess** the report and confirm the severity within 7 business days 4. **We fix** the issue; Critical/High findings are patched within 14 days 5. **We pay** the reward upon fix deployment 6. **We publish** a post-mortem (for Critical/High findings) after the fix is live and users are safe 7. **You can disclose** publicly after the fix is live and 30 days have passed - we will coordinate with you We do not pursue legal action against researchers who follow this responsible disclosure process. *** ## Hall of Fame Researchers who responsibly disclose valid vulnerabilities will be acknowledged here (with permission). | Researcher | Finding | Date | | - | - | - | | - | - | - | *** ## Active Period The bug bounty program is active from mainnet launch (Q2 to Q3 2027). During the testnet phase, reports are accepted and recognized, but cash rewards are not yet active - the protocol does not hold user funds until mainnet. If you find a critical issue during the testnet phase, report it anyway. We will honor the reward when mainnet launches. # Security Model Source: https://docs.writz.xyz/security/security-model **What Writz protects, what it cannot protect, and what happens when things go wrong.** No protocol is risk-free. This page explains Writz's security model honestly: what the system protects, what the trust assumptions are, and what happens in every relevant failure scenario. *** ## Security Properties Writz v1 (Writz Lend on `bitcoin-spv` and `writz-lend`) is designed to provide two security properties to users: **1. Custody:** Your BTC cannot be taken by Writz, by an attacker, or by anyone - unless your Bitcoin wallet private key is compromised. The P2WSH script enforces this at the Bitcoin Script level. **2. Recoverability:** Even if Writz shuts down permanently, you can recover your BTC after the time-lock expires using only your Bitcoin wallet. No dependence on Writz's co-signing service after the lock expires. **Not a property of v1: privacy.** Writz Lend positions are plaintext. Your Stellar address, the Bitcoin deposit it is linked to, your collateral, debt and health ratio are all public on chain. Privacy is being designed as a separate, opt-in edition with its own contracts, pool, audit and trusted-setup ceremony, in progress and on testnet only; see `docs/how-it-works/privacy-edition.md`. Sections below that cover `commitment-tree`, `zk-verifier`, the circuits or the ceremony describe that edition, not v1. *** ## Trust Assumptions Every security system has trust assumptions. Writz's are: | Component | Trust assumption | Risk if violated | | - | - | - | | Bitcoin Script | Bitcoin's consensus rules are enforced correctly | Catastrophic - but this is foundational Bitcoin security | | Soroban contracts | Smart contract code runs as written | Critical - mitigated by audits and open-source code | | ZK circuits (privacy edition only) | Groth16 proofs are sound (no witness forgery) | Critical for that edition - mitigated by its own audit; not a v1 assumption | | Trusted setup ceremony (privacy edition only) | ≥1 participant destroyed their randomness honestly | Critical for that edition - mitigated by a multi-party ceremony; not on the v1 mainnet path | | Protocol co-signing key | The protocol co-signing key is not compromised | High - it cannot move Bitcoin without the user's key; a 2-of-3 protocol side in the script is planned, with a published signing policy | | Oracle price feed | BTC/USD price is not manipulated beyond staleness threshold | High - mitigated by staleness checks today (single Reflector source); a second source for a median is planned | | User's private keys | User's Bitcoin and Stellar keys are not compromised | Critical - user responsibility | | All of the above | No large-scale quantum computer capable of running Shor's algorithm exists | Catastrophic if it does, but this breaks Bitcoin's ECDSA, Stellar's Ed25519, and Groth16's BN254 pairing curve simultaneously. Not a Writz-specific risk, and not one Writz can mitigate unilaterally | Every trust assumption above except the ZK circuit's soundness (Groth16 pairing checks) and the Bitcoin/Stellar signature schemes themselves rests on elliptic curve discrete log hardness. A cryptographically relevant quantum computer would break secp256k1 (Bitcoin), Ed25519 (Stellar), and BN254 (the trusted setup ceremony and every Groth16 proof) at once. This is not something Writz can fix by itself: the protocol's BTC custody depends on Bitcoin's own signature scheme remaining secure, and its USDC settlement depends on Stellar's. Migrating Writz's ZK layer to a post-quantum-friendly proving system (for example, a hash-based STARK) would not change that, and would give up Stellar's native BN254 pairing verification (Protocol X-Ray) that makes on-chain ZK verification affordable in the first place. No BTCfi competitor surveyed in `docs/research/market-landscape.md` is post-quantum either, for the same structural reason. This is tracked as an acknowledged, industry-wide limitation, not a roadmap item. *** ## Component-Level Security Mechanisms The properties above are enforced by specific mechanisms in `bitcoin-spv`, `writz-lend` and the relayer. Subsections marked `commitment-tree`, ZK or circuit belong to the privacy edition (in progress, testnet only), which `writz-lend` never calls; they are kept here because that edition's contracts are deployed on testnet and in scope for security reports. This section documents how each one works and what its residual trade-offs are. ### Bitcoin SPV verification `bitcoin-spv` is a header-chain light client, and `verify_transaction` is the sole gate a Bitcoin deposit must pass before Writz recognizes it. A caller no longer supplies a header chain: it proves a transaction against block headers the contract already stored, so a chain mined privately can never be presented. How this compares with wrapped Bitcoin and bridges, and which of their risks a light client still shares, is set out in [SPV vs Wrapped Bitcoin](/security/spv-vs-wrapped). * **Anchored to a checkpoint.** `set_checkpoint` (admin-gated, callable exactly once) stores a real block's height, hash, `bits`, timestamp and the start time of its difficulty period. Every header accepted by `submit_headers` must descend from a stored header, so it must descend from that block. A header whose `prev_block_hash` is not stored is rejected with `UnknownParent`. Because the checkpoint can only be set once, the admin cannot later swap the trusted chain. * **Proof-of-work and exact difficulty.** Each header must satisfy `SHA256d(header) < target(header.bits)` (`difficulty.rs::validate_proof_of_work`, replicating Bitcoin Core's `arith_uint256::SetCompact`), and must carry exactly the `bits` Bitcoin's rules require at its height: unchanged inside a 2016-block period, and at each boundary the retargeted value (`parent_target * clamp(timespan, 1/4, 4x) / 2 weeks`, capped at the network's proof-of-work limit, compact-encoded like `GetCompact`). This replaces the earlier 64x "difficulty band", so an attacker must expend the network's real difficulty, not a 64x discount. Testnet3/testnet4's minimum-difficulty exception is not modelled; only mainnet and signet are supported. * **Most-work chain.** Headers accumulate chainwork, and the contract tracks the tip with the most work and a height-to-hash index. A transaction is only provable in a block on that chain, buried under at least `min_confirmations` blocks. A heavier fork re-points the index, and a transaction that existed only in the orphaned branch stops being provable. * **Not-in-the-future timestamps.** Headers more than two hours ahead of ledger time are rejected. * **No inner-node forgery.** `verify_transaction` rejects a 64-byte `raw_tx`, the size of a Merkle inner-node preimage, so an inner node can never be proven as a transaction. * **Permissionless ingestion, optional allowlist.** Anyone can call `submit_headers`; the relayer does so routinely. **On signet, restrict it with `set_submitter`:** signet blocks are authenticated by a block signature that this contract does not verify, and signet's proof-of-work is trivially cheap, so on signet the header source must be a trusted submitter. On mainnet, proof-of-work at real difficulty is the security and submission can stay open. * **Admin governance.** The checkpoint admin can no longer rewrite the trusted chain after the fact, but it still chooses the checkpoint and the submitter, so the `bitcoin-spv` (and `zk-verifier`) admin accounts are Stellar accounts intended to be configured as 2-of-3 multisigs via native `SetOptions` before mainnet - no contract code is involved, since `caller.require_auth()` already validates against whatever signing thresholds the admin account carries. The contract is configured by its constructor at deployment. Until `set_checkpoint` has been called, `verify_transaction` and `submit_headers` fail closed with `CheckpointNotSet`. **This same requirement extends to `writz-lend` and `commitment-tree`'s admin accounts, not just `bitcoin-spv`/`zk-verifier`.** Their admins control functions with equal blast radius: `commitment-tree`'s admin gates `insert_commitment` (when a deposit enters the Merkle tree; the resulting root is proven, #211) and, as of the config setters added alongside this document, `set_oracle`/`set_spv_contract`/`set_zk_verifier` - an admin who can silently repoint any of those can redirect the entire lending flow's trust assumptions. `writz-lend`'s admin gates the equivalent setters plus `set_keeper`/`set_relayer`/`set_keeper_stale_window`, which govern who can liquidate and who can trigger BTC release. None of the four contracts enforce multisig in contract logic - all four depend entirely on the underlying Stellar account being configured as a multisig externally. **Before mainnet, all four admin accounts need the same 2-of-3 (or stricter) multisig configuration, not just two of them.** The contract already is a stateful light client: it accumulates its own validated chainwork across calls and follows the most-work chain, as described above. What it still trusts is its starting point, the checkpoint, which is set once and is public, so anyone can compare it with a Bitcoin node. Planned hardening (proof-depth binding, median-time-past, an on-chain freshness rule, redundant relayers and an independent monitor) and how this model compares with wrapped Bitcoin and bridges are set out in [SPV vs Wrapped Bitcoin](/security/spv-vs-wrapped). ### Deposit script binding (`writz-lend`) An SPV proof shows a Bitcoin transaction is real and confirmed; it does not show who controls its outputs. `writz-lend::deposit` therefore never trusts the caller's scriptPubKey. It rebuilds the Writz redeem script from three values - the protocol co-signing key fixed in the contract's config, the depositor's key and the timelock - hashes it, and requires the deposit output to be exactly that P2WSH. An output the depositor can spend alone, or someone else's UTXO, cannot match, so it can never be registered as collateral. (`bitcoin-script`'s `buildRedeemScript` and the contract's `script.rs` are kept byte-identical; the contract tests pin them against reference vectors from the TypeScript builder.) The timelock must also fall 1,008 to 105,000 blocks (about 7 days to 2 years) above the block that confirmed the deposit, so the CLTV escape hatch is never an instant exit for a freshly-deposited position. The protocol key is fixed for the contract's lifetime; rotating it means deploying a new contract. ### Collateral binding (`commitment-tree`, privacy edition) A ZK deposit must lock its BTC under the same Writz script `writz-lend` uses: a P2WSH built from the protocol's co-signing key, the depositor's own key and a CLTV timelock. `commitment-tree::deposit` takes `user_pubkey` and `timelock_height`, rebuilds that script on-chain from them and `Config.protocol_pubkey` (`spv-types::script`, shared by both lending contracts), and counts only the output of `raw_tx` that pays it. The timelock must lie 1,008..=105,000 blocks above the block that confirmed the deposit. A transaction that pays anything else - a script the protocol cannot co-sign, or someone else's Writz output claimed with a different key - fails with `VaultOutputNotFound`. The proof's public `actual_satoshis` signal must equal that output's value, and `deposit.circom` constrains `collateral_satoshis === actual_satoshis`, so the hidden collateral committed into the position is transitively bound to what the transaction really locked (GHSA-2hjj-x5wr-4p68, GHSA-xp6j-g2rw-h5g6, GHSA-mg4x-cr23-4x3v). Why a per-user script rather than one shared vault script (#177): a shared script cannot hold a per-user timelock exit or require the user's own signature for release, so the protocol alone would hold every ZK depositor's BTC. The per-user script costs the ZK path no privacy it actually had: `DepositEvent` already publishes the depositor, the Bitcoin txid and the commitment, and the txid identifies the output on Bitcoin. What the ZK path hides is which leaf later borrows, repays or is liquidated - and those proofs never reference the script. `DepositEvent` also carries `user_pubkey` and `timelock_height`, so the timelock exit can be rebuilt from chain data alone. The trade-off is the one `writz-lend` already makes (GHSA-5rxp): once the timelock passes, a borrower can reclaim the BTC while still owing USDC, and the protocol cannot seize it. `commitment-tree` therefore carries the same exposure cap, `max_total_borrowed` (50,000 USDC by default, admin-set). Two gaps specific to the ZK path are closed by #212: * **A borrow must leave time before the timelock.** The commitment now carries the position's CLTV height, `Poseidon(collateral, debt, liq_key, nonce, timelock_height)`, bound at deposit to the height the contract checked against the Bitcoin output. A borrow proves `timelock_height >= min_timelock_height`, a public bound, and the contract requires that bound to be at least `bitcoin-spv`'s tip plus `borrow_timelock_margin_blocks` (4,320 blocks, about 30 days, admin-set from 144 up to 105,000). It also refuses a tip whose header is older than `max_tip_age_secs` (6 hours by default), so a stalled header relay cannot let a borrow measure its margin from a stale height. The bound reveals the current tip, not the position's own timelock. Repay has no bound, so a matured position can always repay. * **A third party can liquidate a ZK position.** A position has two keys: `secret`, which borrow, repay and release prove knowledge of, and `liq_key = Poseidon(secret)`, which is enough for the liquidation circuit and nothing else. Every borrow and repay proof encrypts the new position's `liq_key`, nonce and amounts to the keeper's BabyJubJub key inside the circuit (`KeeperNote` in `privacy/circuits/src/position.circom`), and the contract requires that key to be the configured one (`KeeperKeyMismatch`). Because the encryption is a constraint of the proof, a borrower cannot publish a note the keeper fails to open. The relayer's ZK keeper (`relayer/src/zk-keeper/`) opens each note from the `borrow`/`repay` event, tracks the position's health and liquidates it below the threshold. The keeper key can close a position but never borrow against it or release its BTC. What is not done: there is no default at maturity. A position whose owner neither repays nor is liquidated can still reclaim its BTC once the timelock passes, which is the bounded exposure above. The margin guarantees that at least 30 days separate the last borrow from that point. ### Deposit key ownership (both lenders) Rebuilding the Writz script proves an output is locked under the protocol's co-signing key, not that the Stellar caller owns it. The user key appears in the deposit transaction's own input witness and the timelock is guessable, so anyone watching Bitcoin could register someone else's deposit as their own position and borrow against it (GHSA-862g-739q-x6h8). Both `deposit` functions now take `key_sig`: a standard Bitcoin signed message (BIP-137, a wallet's ECDSA "sign message") by `user_pubkey` over ```text theme={null} Writz deposit contract: depositor: txid: ``` The contract rebuilds that text, recovers the signer with `secp256k1_recover`, and requires it to be `user_pubkey` (`spv-types::key_proof`). Naming the contract and the depositor means a signature cannot be replayed into the other lender, another deployment, or another account. The app asks the connected Bitcoin wallet for this signature right before the deposit transaction. ### Borrow recipient binding (`commitment-tree`, privacy edition) `commitment-tree::borrow` authenticates the caller-supplied `borrower` address via `require_auth`, but authentication alone doesn't tie *that specific address* to the ZK proof being redeemed. Before this fix, a party who obtained a pending borrow transaction's proof and public signals (e.g. observing it before it confirms) could resubmit the identical proof with a different address they control and be included first - the contract would pay them, spend the position's nullifier, and the original caller's transaction would then fail. `borrow_repay.circom`'s public `recipient_lo`/`recipient_hi` signals - the sha256 digest of the recipient's Stellar strkey address, split into two 128-bit halves - close this the same way `deposit.circom`'s `btc_txid_lo/hi` bind a proof to its Bitcoin transaction: by being part of the public statement Groth16 verifies, not by any in-circuit constraint. `commitment-tree::borrow` independently recomputes `sha256(borrower.to_string())` from the address it authenticated and rejects a mismatch, so a copied proof can only be redeemed by the address it was generated for. `repay` shares the same circuit but pulls funds *from* the caller rather than paying an arbitrary address, so it has no equivalent recipient-redirection risk and doesn't check these signals. ### Interest accrual (`writz-lend`) Interest used to be charged at the borrow rate in force when a position was next touched, applied to the whole period since its last update. A borrower could supply a large amount right before repaying, crash utilization and the rate, and reprice months of debt at almost nothing, then withdraw (GHSA-c9gg-c476-9vc8, GHSA-frj5-5wmv-wvm3). `writz-lend` now keeps a global borrow index that grows at the rate in force over each interval and is brought up to date before anything changes utilization: supply, withdraw, borrow, repay and liquidate. A position's debt scales with the index since it was last settled, so a past interval is always priced at the rate that applied during it. ### Deposit nullifier reuse (`commitment-tree`, privacy edition) `deposit` checked that a nullifier had not been spent but never recorded it, so a second deposit could reuse one and leave a leaf that could never be spent (GHSA-frj5-5wmv-wvm3, low: self-harm only). Deposit nullifiers are now recorded and a reuse fails with `DuplicateNullifier`. ### Merkle proof integrity (duplicate-leaf ambiguity) Bitcoin's block Merkle tree duplicates the last transaction when a block has an odd transaction count - a well-known quirk (the class of issue behind CVE-2012-2459) that lets the same txid produce a valid-looking inclusion proof at two adjacent indices. Rather than special-casing the Merkle verifier (which would incorrectly reject the *legitimate* proof for the last transaction in any odd-count block - covered by the `merkle_seven_tx_odd_count` test), both `writz-lend::deposit` and `commitment-tree::deposit` reject any deposit whose Bitcoin txid has already been recorded, independent of which Merkle index was supplied. This closes the double-collateralization path at the layer that actually needs to enforce uniqueness - the position/commitment store - rather than in the Merkle math itself. ### Relayer authorization (`/insert-commitment`, `/update-leaf`) The relayer holds the `commitment-tree` admin key and exposes two HTTP endpoints the frontend calls directly from the browser during deposit/borrow/repay - which used to mean anyone with network access to the relayer could trigger them too (GHSA-prw2-j3jx-43qh, GHSA-wqp4-3573-552v, GHSA-ffx5-7x4m-939g). `POST /insert-commitment` now requires `authTxXdr`: a throwaway transaction (never submitted) whose source account is the depositor, signed by them, containing a single `manageData("writz-insert-commitment", commitment)` operation. The relayer verifies the signature against the claimed depositor, then independently looks up the real `DepositEvent` for that commitment on-chain and rejects unless its `depositor` field matches - so an attacker can sign with their own key all they want, but can't claim someone else's pending deposit, and can't fabricate a deposit that never happened. `POST /update-leaf` needs no identity check at all: it validates the proposed write against ground truth instead, by recomputing what the Merkle root would become and comparing it against the real on-chain root via `get_merkle_root()`. Only a write that reproduces a root the contract has actually reached is persisted - a real borrow/repay always produces exactly the update this check demands, and nothing else can. `commitment-tree::insert_commitment` used to store whatever root the admin passed, so the relayer's honest computation was the only thing keeping the tree correct; a compromised admin key or a relayer bug could install a root holding invented leaves or dropping honest ones. It now takes a Groth16 proof from the `insert` circuit (`privacy/circuits/src/insert.circom`, built on `MerkleTreeUpdater`) instead of a root (#211). The proof shows the new root is the stored root with the commitment written into a leaf that was empty, and the contract pins that leaf to its own `NextLeafIndex` counter, requires the commitment to be a pending deposit, and requires the proof's old root to be the stored one. The admin still decides when a commitment is inserted, but no longer what the root becomes. The relayer generates the proof from public data (`relayer/src/insert-prover.ts`). The old `createDemoPosition` dashboard shortcut, which inserted a commitment with no deposit behind it, is removed. ### Leaf store liveness (relayer) The contract stores only the Merkle root, so the relayer's leaf store is the only copy of the tree, and it used to advance only when a client called `/update-leaf` after a borrow or repay. One missing call, a closed tab or a borrower who never sends it, left the store behind the chain and `/merkle-path` answered 409 for everyone (GHSA-vwvv-3255-783f). `BorrowEvent` now carries `new_commitment` like `RepayEvent`, and the relayer's tree watcher follows `insert_leaf`, `borrow` and `repay` events from a persisted cursor, applying each to the store after checking it reproduces the event's root. Which leaf a borrow or repay changed is found by matching the root, so it is still not published. `/update-leaf` is now only a faster path to the same state. The cursor advances one event at a time, so a restart replays at most the event that was in flight, and that event always reapplies cleanly (#286). An event the store cannot explain holds the cursor and shows up as `treeSync.ok: false` on `/health`. `bun run rebuild-tree` then rebuilds the store from chain and checks it against the on-chain root before swapping it in (#265). ### Cooperative release binding (`/api/cosign`) `/api/cosign` verifies a `zero_debt` Groth16 proof and separately trusts a caller-supplied `commitment` string and PSBT - with nothing tying the three together. A valid zero-debt proof about the caller's own debt-free position B could authorize releasing a *different*, still-indebted position A's BTC: the proof itself carried no position identifier (its only public signal was `merkle_root`), and the endpoint never checked that the claimed `commitment` was the one the proof was about, or that the PSBT's UTXO was the one that commitment actually deposited (GHSA-6jmp-wf3x-3vxh, GHSA-9j8g-prh5-jhhj). `zero_debt.circom` now publishes `commitment` (`Poseidon(collateral, 0, liq_key, nonce, timelock_height)`) as a circuit output alongside `merkle_root` - this reveals nothing new, since the commitment is already public on-chain as the Merkle leaf itself (visible in `DepositEvent`/`InsertLeafEvent`). `/api/cosign` (via `lib/cosign/binding.ts`'s `verifyReleaseBinding`) now checks, in order: the proof's public commitment matches the one the request claims to release; the proof's public root matches the current on-chain root; a `DepositEvent` exists on-chain for that commitment; and the PSBT's input 0 spends exactly the txid that event recorded. Each link - proof↔commitment, commitment↔real-deposit, commitment↔PSBT - is independently verified against on-chain ground truth, not against another caller-supplied value. A repaid position's zero-debt leaf also has to stop being borrowable once its BTC leaves: otherwise a fully repaid and released position could borrow again against no collateral (GHSA-w4rp-v54x-2cv3, GHSA-hcjf-8vjc-2hfv). `zero_debt.circom` publishes the leaf's nullifier, `commitment-tree::mark_released` spends it, and `/api/cosign` refuses to co-sign until that is recorded on-chain, so `borrow` on the released leaf fails with `NullifierAlreadySpent` (#205, deployed in the 2026-10-11 testnet redeploy). ### Contract initialization (constructors, all four contracts) All four contracts (`bitcoin-spv`, `writz-lend`, and the privacy edition's `commitment-tree` and `zk-verifier`) take their admin and configuration in a `__constructor` that runs atomically as part of deployment, so there is no separate initialization transaction for anyone to front-run (GHSA-422m-f73x-fh58). Earlier versions used a separate `initialize` call, which left a window in which another account could install itself as admin; the constructors replaced it (#199) and shipped with the coordinated redeploy (#177). ### SegWit transaction handling For SegWit deposits, `verify_transaction` computes the txid as `SHA256d` of the caller-supplied raw transaction bytes, which must be the non-witness serialization (Bitcoin's block Merkle tree is built over non-witness txids, not `wtxid`). The contract has no way to verify on-chain that witness data was stripped correctly - if it wasn't, the computed hash won't match the Merkle root and the deposit simply fails with `MerkleProofInvalid`. This is a liveness concern only (a failed proof can be retried with correctly stripped bytes); no funds are ever at risk from a stripping mistake. The relayer's transaction-stripping logic carries dedicated test coverage for legacy, P2WPKH, P2WSH, and P2TR shapes, including multi-input transactions with distinct per-input witness stacks. ### Emergency recovery timelock (CLTV / nSequence) The P2WSH cooperative-release path requires the protocol's co-signature; the fallback path uses `OP_CHECKLOCKTIMEVERIFY` so a user can always recover BTC unilaterally once the timelock expires. CLTV has a sharp edge: if the spending input's `nSequence` is `0xFFFFFFFF` (Bitcoin's "final" value, and many wallets' default), the Script interpreter fails the CLTV check immediately regardless of whether the timelock has actually expired, with no clear error message. Writz's own tooling (`buildEmergencyTransaction`/`finalizePathB` in `bitcoin-script`) hardcodes `nSequence = 0xFFFFFFFE` and gives `SpendParams` no caller-settable sequence field for this path, so it cannot regress. Users constructing this recovery transaction by hand with a third-party wallet must set this themselves - see [Manual Emergency Recovery](/how-it-works/manual-emergency-recovery). ### Protocol co-signing key The relayer's repay watcher co-signs a Bitcoin release in response to a `repay_full` event recorded on Stellar, and publishes the half-signed transaction on `writz-lend` so it can be read even if Writz is down. The protocol key cannot move Bitcoin on its own: the cooperative path also requires the user's signature, and the time-lock path needs only the user's key. The planned design replaces the single protocol key with a 2-of-3 protocol side written into the P2WSH script, three keys held separately, so Bitcoin enforces the threshold natively. The signing policy, what the protocol signers can and cannot sign, will be published. ### Trusted setup ceremony (privacy edition) The ceremony is not on the v1 mainnet path. Groth16 requires a per-circuit trusted setup. `privacy/circuits/scripts/ceremony/` implements the full production runbook: fetching and checksum-verifying the Hermez Phase-1 ptau (`00_fetch_ptau.sh`, deliberately requiring a human-pinned, PR-reviewed checksum rather than a hardcoded one), the coordinator's per-circuit setup (`01_new_zkey.sh`), interactive participant contribution with no scripted entropy (`02_contribute.sh`), independent transcript verification (`03_verify_transcript.sh`), and final key export plus manifest generation (`04_export.js`). A CI job re-verifies the committed manifest's hashes and IC lengths on every PR touching `privacy/circuits/keys/**`, and rejects any transcript containing a dev-labeled participant. Running the actual multi-party ceremony - recruiting independent participants and executing the runbook publicly - is an operational milestone for the privacy edition before that edition launches, not something further engineering work can complete on its own. ### Liquidation debt disclosure (privacy edition) In the privacy edition's design, collateral is hidden by the ZK commitment scheme (debt amounts still appear in borrow and repay events and USDC transfers). Liquidation is the one operation where this breaks down: a keeper must know how much USDC to repay, so `usdc_debt` is necessarily published on-chain at liquidation time. What the circuit guarantees is that this published amount is *provably correct* - `liquidation.circom` binds it directly to the private commitment (`usdc_debt <== debt_stroops`) as a circuit output, so there is no caller-supplied field a keeper could misdeclare. A future keeper network where amounts are learned through encrypted off-chain channels, or committed-but-not-revealed bidding, only becomes worth building once multiple competing keepers exist for such a scheme to protect against - see the keeper model below. ### Cross-chain repayment automation Repayment isn't atomic across Stellar and Bitcoin: a user repays USDC on Soroban, and only afterward does the protocol co-sign the Bitcoin release. For `writz-lend`'s plaintext flow, a relayer service (`relayer/src/repay-watcher/`) closes the gap between those two steps automatically - it polls Soroban RPC for repayment events from a cursor persisted in SQLite (never "from now" on restart, so no event that fires during an outage is silently skipped), reconstructs and co-signs the release PSBT, and publishes it on-chain for the user to broadcast. For `commitment-tree`'s ZK flow, this can't be fully automated with the current design - cosigning requires the user's own private witness (collateral, secret, nonce), which the backend does not and should not custody outside of the keeper model described below. Those positions rely on manual cosign submission or the CLTV timelock as the safety valve. In both flows, the bound on how long a user can be "stranded" (repaid on Stellar, BTC still locked) is `loan_duration + a fixed recovery buffer`, after which the timelock path is always available regardless of protocol availability. ### Keeper model and liquidation permissionlessness In the privacy edition, `commitment-tree::liquidate` requires only the caller's own authorization and a valid Groth16 proof of undercollateralization - there is no keeper allowlist to bypass, so it is permissionless by construction. Producing that proof takes the position's `liq_key`, which only its owner and the holder of the keeper key can read (#212). `writz-lend::liquidate` (plaintext positions, no ZK privacy at stake) additionally supports a stale-keeper fallback: the designated keeper may always liquidate an undercollateralized position, and after a configurable window (default 24h) with no keeper activity, any caller may liquidate a position that is actually below the threshold instead. Liquidation safety never depends on caller identity - only on whether the position is actually undercollateralized - so this fallback improves liveness without weakening the collateralization check itself. **This is a deliberate asymmetry between the two contracts, not an inconsistency to reconcile.** `commitment-tree` doesn't need a keeper-preference mechanism because its positions are private by design - there's no plaintext health-ratio feed a keeper could act on faster than anyone else, so "anyone with a valid proof" is already the natural permission model. `writz-lend`'s positions are plaintext, so a keeper-preference-with-stale-fallback pattern makes sense there: a designated keeper gets first opportunity as an operational convenience (predictable liquidation ops, no race with third parties under normal conditions), while the stale fallback guarantees the position doesn't depend on that keeper's uptime for safety. Neither contract's approach is more "correct" than the other - they fit the privacy model each contract actually has. The keeper does need position preimages to monitor health ratios in the first place, which means Writz-as-keeper-operator can see position details that no on-chain observer or third-party liquidator can. Since #212 it gets them from the keeper notes every borrow and repay encrypts to its key, not from the user, and what it gets (`liq_key`) cannot move funds. This is an accepted architectural trade-off for the current phase (privacy-against-observers, not privacy-against-the-operator) rather than a bug - decentralizing it requires a keeper network with its own coordination/staking design, tracked as future work. **`commitment-tree::liquidate` is currently unreachable through any legitimate on-chain sequence of calls.** `borrow`'s circuit enforces a minimum 150% collateral ratio at the moment of borrowing; `liquidate` requires the position's ratio to be below the 120% threshold; and the price used for both checks is the same fixed Phase 1 oracle stub. Unlike `writz-lend`, `commitment-tree` has no interest-accrual mechanism that could push a healthy position's debt up over time - ZK debt only ever changes via an explicit `borrow`/`repay` proof, never automatically. With the price fixed and no accrual, there is no way for a position that started at ≥150% to ever drop below 120% today. This isn't a bug in `liquidate` itself (its proof verification, threshold check, and nullifier-spend logic are all tested - see `commitment-tree/src/test.rs::liquidate_undercollateralized_position`, which constructs the precondition directly rather than through `deposit`+`borrow`) - it's a product gap: liquidation only becomes reachable once either a real (movable) oracle price or a ZK-compatible accrual mechanism exists. Track this as a Phase 2 dependency alongside the oracle integration itself. *** ## What Writz Can Do * Co-sign BTC release transactions when a loan is fully repaid * Refuse to co-sign if a loan is outstanding (keeping BTC locked) * Operate the keeper service that monitors for undercollateralized positions * Manually pause new deposits/borrows/USDC supply via `set_paused` (admin-gated, both lending contracts) - an emergency brake on new exposure, not a freeze; `repay`/`withdraw_supply`/`liquidate`/BTC release all keep working during a pause so users can always exit. This is distinct from the line below. * **Not yet implemented:** an automatic pause of borrowing and liquidation when oracle prices go stale. Today `writz-lend` calls Reflector live and a stale or unavailable price makes the calling operation fail (`OraclePriceStale`/`OraclePriceUnavailable`), which blocks it without a global pause. Today's `set_paused` is manual and admin-triggered, not automatic. * Update the oracle contract address (admin function, no user impact) - `set_oracle` on both lending contracts ## What Writz Cannot Do * Move BTC unilaterally - no valid Bitcoin transaction can be constructed without the user's signature * Block the emergency time-lock recovery - Condition B in the P2WSH script requires no Writz involvement * Censor a specific user's repayment - any user can call `repay` on the Stellar contract *** ## Failure Scenarios ### "Writz stops operating" **During an active loan:** * The user waits for the time-lock to expire (loan duration + 7 days) * The user broadcasts the Condition B Bitcoin transaction using only their own key * The user keeps the USDC they borrowed; Writz cannot recover it **No active loans:** * No BTC is locked; nothing is at risk **Impact on USDC lenders:** * If borrowers do not repay before Writz shuts down, and liquidations cannot occur, USDC lenders may face losses * This risk is mitigated by conservative TVL caps, insurance fund accumulation, and progressive decentralization ### "The protocol co-signing key is compromised" An attacker with the protocol co-signing key cannot: * Move BTC from any P2WSH address (user signature still required for Condition A) * Change any position's recorded state on Stellar (positions are plaintext and public, but only `writz-lend`'s own logic can modify them) An attacker could: * Refuse to co-sign repayment releases (effectively locking users out of Condition A) - users fall back to Condition B time-lock * Generate invalid co-signatures for transactions where loans haven't been repaid - invalid because the Stellar contract checks repayment before the event is emitted **Planned mitigation:** the single co-signing key is replaced by a 2-of-3 protocol side written into the P2WSH script, three keys held separately. Bitcoin enforces the threshold natively, so co-signing a release requires compromising two independent keys at once. The signing policy will be published. ### "The oracle reports a wrong price" If the BTC/USD price is manipulated downward: * Healthy positions could appear undercollateralized and be liquidated (their debt is cleared in USDC; no BTC moves in a `writz-lend` liquidation) **Mitigations:** * Planned: a median of two independent oracles (Reflector plus Pyth, with a deviation guard - corrected 2026-09-16, then re-verified on-chain; RedStone dropped, DIA parked, see `docs/research/oracle-design.md`) - manipulating the median requires moving both **Interim status (2026-09-17):** only Reflector is wired into `writz-lend` today - see `contracts/contracts/writz-lend/src/oracle.rs`. There is no median yet; a single misbehaving or unavailable Reflector feed currently blocks (via `OraclePriceStale`/`OraclePriceUnavailable`) rather than silently mispricing, but it cannot yet be cross-checked against a second source the way the target design requires. Do not treat this as the finished manipulation-resistance story until Pyth is also wired. * Staleness check: price data older than 60 minutes is rejected; liquidations are paused (aligned with `docs/research/oracle-design.md` - Pyth is a pull oracle, not a continuous push feed, so the threshold accounts for time between pulls, not just publish latency) * Progressive TVL caps limit the maximum exposure during early operation ### "A bug exists in the Soroban contracts" **Before audit:** The protocol operates on testnet only with no real funds. **After audit, pre-mainnet:** All findings are remediated before mainnet launch. The external audit before v1 mainnet covers the Soroban contracts (`bitcoin-spv`, `writz-lend`); the privacy edition's circuits and contracts get their own audit before that edition launches. **Post-mainnet:** A whitehat bug bounty program is active with up to \$50,000 in rewards for critical findings. TVL caps limit damage during early operation. An on-chain insurance fund (seeded from protocol fees) provides backstop coverage. ### "A user loses their position data (privacy edition only)" This scenario does not apply to Writz Lend: `writz-lend` positions are recorded on chain and keyed by the Bitcoin txid, so there is nothing local to lose. In the privacy edition, a user's ZK position data (commitment secret, nonce) exists only in their local browser storage. If this data is lost: * The user cannot generate ZK proofs to borrow, repay, or close their position * The Writz team cannot recover the data - it was never stored on-chain * The user must wait for the time-lock to expire and recover BTC via Condition B * Any USDC already borrowed is kept by the user; the loan cannot be formally repaid without the ZK secret **Mitigation:** The Writz app will prompt users to export and securely back up their position data. This is analogous to backing up a seed phrase - the user is responsible. ### "A ZK circuit has a soundness bug" (privacy edition only) This does not apply to Writz Lend, which uses no circuits. A soundness bug means an attacker can generate a valid-looking proof for a false statement - for example, proving they have a 150% collateral ratio when they don't. **Impact:** An attacker could borrow USDC without adequate collateral. If undetected, they could drain the USDC pool. **Mitigations:** * A specialist ZK circuit audit before the privacy edition launches * Formal verification tools (circom-mutator, custom constraint analysis) * TVL caps limit maximum exposure * Progressive rollout: low TVL cap raised only after extended clean operation *** ## The Insurance Fund Starting from mainnet launch, 5% of all protocol fee revenue is automatically routed to an on-chain insurance fund. This fund is: * Controlled by a multi-sig (Writz team + community representatives) * Used only to compensate users for verified smart contract exploits * Not used for operational expenses or token liquidity As the fund grows and protocol governance decentralizes, the insurance fund allocation and payout criteria will be managed by WRTZ token holders. *** **Next:** [SPV vs Wrapped Bitcoin →](/security/spv-vs-wrapped) # SPV vs Wrapped Bitcoin Source: https://docs.writz.xyz/security/spv-vs-wrapped Where an SPV light client differs from wrapped Bitcoin and bridges, and where it shares their risks. **Wrapped Bitcoin, bridges and SPV: what actually differs.** Writz never wraps Bitcoin. Your BTC stays on Bitcoin, in a script that only your key can spend alone after the time-lock, and Stellar learns about it through an SPV light client: a Soroban contract (`bitcoin-spv`) that checks Bitcoin block headers and Merkle proofs. People sometimes ask whether an SPV light client is "just another bridge". It is not, but it is not free of trust either. The tables below say exactly where the difference lies. Items marked **planned** are not built yet. They stay marked until they ship. The mechanisms that are live today are described in the [Security Model](/security/security-model) and in [How SPV Verification Works](/how-it-works/spv-verification). *** ## Who holds the Bitcoin, and who can move it | | Custodial wrapped BTC | Multisig or federated bridge | Bitcoin staking with slashing (Babylon-style) | Writz (SPV light client) | | - | - | - | - | - | | Where the BTC sits | With a custodian | In an address controlled by a group of signers | In the staker's own script | In the user's own script, one output per position | | Who can move it | The custodian | Any quorum of the signers | Only the script's paths; slashing needs proof of misbehaviour | Only the script's paths: user and protocol together, or the user alone after the time-lock | | Can the operator take the BTC | Yes | Yes, with a quorum | No | No | | Is there a pooled reserve to steal | Yes | Yes | No | No | | What represents the BTC on the other chain | A token, transferable | A token, transferable | A staking position | A loan position; no Bitcoin token is minted | | If the operator disappears | Token holders hold an unbacked claim | Token holders hold an unbacked claim | The staker unbonds through the script | The user spends the time-lock path with their own key | ## Trust assumptions | Assumption | Custodial wrapped BTC | Multisig or federated bridge | Writz (SPV light client) | | - | - | - | - | | Honest custodian or signers | Required | Required (k of n) | Not required for custody | | Bitcoin proof of work | Not used | Not used | Required: headers must carry real work, and the most-work chain wins | | Data delivery | The custodian attests | Signers watch Bitcoin | At least one honest relayer submits headers in time. Relayers cannot forge mainnet headers, only delay them | | Verifier code is correct | Minting logic | Bridge verifier (where Wormhole and Nomad failed) | Light client: headers, difficulty, chainwork, Merkle proofs. Open source, with no upgrade entry point, tested against real Bitcoin headers | | Starting point | None | None | A checkpoint block, set once and never changed afterwards, publicly checkable against any Bitcoin node | | Price oracle | For lending use | For lending use | For lending use (the same as any lending protocol) | On testnet the light client follows Bitcoin signet. Signet blocks are authenticated by a block signature the contract does not check, and signet proof of work is cheap, so on testnet header submission is restricted to the Writz relayer. The proof-of-work argument on this page applies to Bitcoin mainnet. ## What happens when each part fails | Failure | Custodial wrapped BTC | Multisig or federated bridge | Writz (SPV light client) | | - | - | - | - | | Keys stolen | All reserves at risk | All reserves at risk once a quorum is stolen | No key can spend a user's script alone. The protocol co-signing key can only refuse to co-sign, and the user still exits after the time-lock | | Verifier bug | Unbacked tokens minted, redeemable for real BTC | Unbacked tokens minted, redeemable for real BTC | Collateral that does not exist could be credited, so lenders' USDC is at risk, bounded by the global borrow cap (a per-borrower cap is planned). Users' BTC is not touched | | Relayers stop | Not applicable | Peg-ins and peg-outs stop | The light client's view of Bitcoin stops advancing. New loans should stop until it catches up (planned: an on-chain freshness rule). Existing BTC stays where it is | | Deep Bitcoin reorg | The custodian absorbs or socialises it | The bridge may have minted against reorged deposits | A deposit confirmed by fewer blocks than the reorg could be undone. Writz plans 6 confirmations on mainnet; the deepest mainnet reorgs on record are from 2010 and 2013 | | Operator shuts down | Redemption depends on the custodian | Redemption depends on the signers | Users recover their BTC with their own key after the time-lock | ## Where SPV really does share risk with a bridge | Shared risk | Why | How Writz addresses it | | - | - | - | | Verification code can have bugs | A light client is code that decides what counts as a confirmed Bitcoin payment | A small, open-source verifier with no upgrade path, so code cannot be swapped under users (a fix ships as a new deployment). Planned: an independent audit before mainnet, and hardening of proof-depth binding and median-time-past | | Someone must deliver data | Headers have to be submitted to Stellar | Planned: permissionless submission on mainnet, several independent relayers, and a monitor that compares the on-chain tip with several Bitcoin sources and pauses new loans if it falls behind | | Privileged roles | Whoever configures the verifier is trusted for what that role can do | No role can rewrite stored headers or the checkpoint once it is set. The admin can still choose who may submit headers and which lending contracts may consume deposits, and the lending contract's admin can point it at a different verifier or pause new loans. Planned: these admin accounts move to a 2-of-3 multisig before mainnet | | Starting point | The checkpoint is trusted once | It is public, and anyone can compare it with a Bitcoin node | ## What it costs to attack each model | Model | To forge a deposit or mint | Notes | | - | - | - | | Custodial wrapped BTC | Compromise or coerce one custodian | One organisation | | Multisig or federated bridge | Compromise k of n signers, or find a verifier bug | Ronin: 5 of 9 keys. Harmony Horizon: two keys | | Writz (SPV light client), relayers live | Out-mine the Bitcoin network | Majority hash power | | Writz (SPV light client), relayers stalled, mainnet with open submission | Mine the required confirmations privately at full difficulty | About six blocks of mainnet work, worth roughly 18.8 BTC in forgone rewards at the time of writing (October 2026). This is why new loans must stop when the light client falls behind, and why exposure caps stay far below that figure | ## Track record | System | Model | What happened | | - | - | - | | [Ronin bridge, 2022](https://home.treasury.gov/news/press-releases/jy0768) | Multisig of validators | About 625M USD taken with 5 of 9 validator keys | | [Wormhole, 2022](https://kudelskisecurity.com/research/quick-analysis-of-the-wormhole-attack) | Guardian signatures | 120,000 wETH minted through a signature-verification bypass | | [Nomad, 2022](https://immunefi.com/blog/bug-fix-reviews/hack-analysis-nomad-bridge-august-2022/) | Optimistic verifier | About 190M USD after an upgrade made every message valid | | [Harmony Horizon, 2022](https://securityaffairs.com/141266/apt/harmony-horizon-bridge-lazarus-apt.html) | Multisig | About 100M USD after two multisig keys were compromised | | [Multichain, 2023](https://www.dlnews.com/articles/defi/multichain-halts-use-of-crypto-bridge-saying-ceo-arrested/) | MPC custody | About 125M USD moved out; operational control of the keys sat with one person | | [WBTC, 2024](https://bitgo.com/resources/blog/bitgo-to-move-wbtc-to-multi-jurisdictional-custody-to-accelerate-global/) | Custodian | Custody moved to a new joint venture; several integrators reduced or ended exposure | | [BTC Relay, 2016](https://github.com/ethereum/btcrelay) | SPV relay on Ethereum | No forgery on record; depended on volunteers paid per header to keep relaying | | [tBTC v1, 2020](https://blog.keep.network/details-of-the-tbtc-deposit-pause-on-may-18-2020/) | SPV relay plus bonded signers | Paused three days after launch for a redemption bug, not an SPV forgery; users' BTC was not at risk | Figures are as reported by the linked post-mortems, announcements and press coverage. ## How Writz keeps the light client honest | Layer | Status | What it does | | - | - | - | | Proof of work, difficulty and chainwork checks on every header | Live | Rejects headers without the required work and follows the most-work chain | | Merkle proof against a stored header, 64-byte transaction refusal | Live | A transaction is accepted only if it is in a block the light client already accepted | | Confirmation depth | Live, set per deployment | 6 confirmations planned on mainnet | | Exposure caps | Global cap live, per-borrower cap planned | Bound what any verification failure could cost lenders | | Proof-depth binding, median-time-past | Planned | Close known SPV edge cases | | On-chain freshness rule | Planned | Loans that depend on the Bitcoin tip are refused when the tip is too old | | Multiple relayers, permissionless submission | Planned for mainnet | Any honest relayer keeps the light client live | | Independent monitor with automatic pause of new loans | Planned for mainnet | Compares the on-chain tip with several Bitcoin sources; can only pause new deposits and borrows, never move funds | | Zero-knowledge proof of the Bitcoin header chain as a second check | Researching | An independent cryptographic check that needs no trusted signer; it could only refuse a tip, never credit value | *** **Next:** [Audits →](/security/audits) # Stride threat model Source: https://docs.writz.xyz/security/stride-threat-model # STRIDE Threat Model - Writz Protocol **Version:** 0.1 (Draft) **Date:** June 2026 **Scope:** Phase 1 architecture (SPV contract + Writz Lend + P2WSH locking script + relayer) **Methodology:** Microsoft STRIDE (Spoofing, Tampering, Repudiation, Information Disclosure, Denial of Service, Elevation of Privilege) *** ## System components in scope | Component | Type | Trust level | | - | - | - | | Bitcoin P2WSH locking script | Bitcoin on-chain | Trustless - Bitcoin consensus | | `bitcoin-spv` Soroban contract | Stellar on-chain | Trustless - Soroban VM | | `writz-lend` Soroban contract | Stellar on-chain | Trustless - Soroban VM | | Writz SPV relayer | Off-chain service | Untrusted - convenience only | | Reflector oracle (a second source planned) | Off-chain + on-chain | Partially trusted | | Protocol co-signing key | Off-chain | Trusted - Writz operator | | Liquidation keeper | Off-chain | Trusted - Phase 1 | | User Bitcoin wallet (Xverse) | Client-side | Untrusted | | User Stellar wallet (Freighter) | Client-side | Untrusted | *** ## Data flow diagram ``` [User Bitcoin Wallet] │ │ BTC → P2WSH address (on Bitcoin) ▼ [Bitcoin Network] ────────────────────────────────────────────── │ │ block headers + Merkle proof (via relayer or self-serve) ▼ [SPV Relayer] ─────────────────────────────────────────────── UNTRUSTED │ │ headers + proof + raw_tx (user submits directly to Soroban) ▼ [bitcoin-spv contract] ─────────────────────────────────────── TRUSTLESS │ │ SpvVerificationResult { txid, block_hash, confirmations } ▼ [writz-lend contract] ────────────────────────────────────── TRUSTLESS │ │ │ │ │ │ BTC/USD price feed (SEP-40) │ │ ▼ │ │ [Pyth / Reflector Oracle] ─────────────────── PARTIALLY TRUSTED │ │ │ USDC transfer (Stellar Asset Contract) ▼ ▼ [User Stellar Wallet] [Protocol Treasury / USDC Pool] [Protocol Co-signing Key] ─────── co-signs P2WSH release on Bitcoin [Liquidation Keeper] ─────────── calls liquidate() on Writz Lend ``` *** ## Threat analysis - STRIDE ### S - Spoofing (impersonating a legitimate entity) #### S1: Attacker submits a fabricated SPV proof **Threat:** An attacker constructs a fake SPV proof for a Bitcoin transaction that was never mined (or was mined with fewer confirmations than required) and submits it to the SPV contract. **Mitigation:** * The SPV contract validates the full header chain: each header's `prev_block_hash` must equal SHA256d of the previous header. Fabricating a 6-deep valid chain requires solving 6 Bitcoin proof-of-work puzzles - infeasible at current difficulty. * The Merkle proof must correctly connect the transaction to the Merkle root embedded in the block header. Forging a valid Merkle proof without the actual transaction requires a SHA256 preimage attack - computationally infeasible. * The raw transaction bytes are parsed on-chain; the txid is recomputed as SHA256d(raw\_tx) and verified against the Merkle proof. **Residual risk:** LOW - the entire security model is backed by Bitcoin's proof-of-work consensus. #### S2: Attacker impersonates the Writz protocol co-signing key **Threat:** An attacker obtains the protocol private key and co-signs a fraudulent P2WSH release, draining all BTC collateral. **Mitigation (today):** The protocol key alone cannot move Bitcoin: the cooperative path also requires the user's signature, so a stolen protocol key can at most refuse or delay releases, and the user's time-lock exit (Path B) never needs it. **Mitigation (planned):** A 2-of-3 protocol side written into the P2WSH script, three keys held separately, so Bitcoin enforces the threshold natively and no single key can co-sign a release. Threshold ECDSA (MPC) is not planned. The signing policy, what the protocol signers can and cannot sign, will be published. **Residual risk:** MEDIUM with a single protocol key; LOW once the 2-of-3 protocol side ships. #### S3: Oracle price spoofing **Threat:** An attacker manipulates the BTC/USD price fed to Writz Lend to artificially trigger liquidations or allow over-borrowing. **Mitigation:** * `writz-lend` reads Reflector's external-prices SEP-40 feed (CEX and DEX aggregation), which an on-chain flash loan cannot move. A price older than one hour is rejected, so a stale or unavailable feed blocks price-dependent calls rather than mispricing them. * Planned: a second source (Pyth, pull model) with a deviation guard, so a single bad feed is caught by the other. (RedStone dropped: Stellar is not in its supported-chain list. DIA parked: its documented testnet contract address returns "contract not found". See `docs/research/oracle-design.md`.) * TWAP (time-weighted average price) delays are considered as additional manipulation resistance. **Residual risk:** LOW for retail manipulation. Theoretical risk from compromised institutional publishers - mitigated by publisher diversity. *** ### T - Tampering (unauthorized data modification) #### T1: Attacker tampers with the P2WSH redeem script **Threat:** An attacker modifies the redeem script after the P2WSH address is generated to change the spending conditions. **Mitigation:** The P2WSH address commits to the SHA256 hash of the redeem script. The script is revealed only at spend time, and Bitcoin verifies `SHA256(revealed_script) == address_hash`. Any modification to the script changes the hash and invalidates the address. **Residual risk:** NONE - Bitcoin's P2WSH construction is cryptographically binding. #### T2: Attacker tampers with the Soroban contract state **Threat:** An attacker modifies position data (debt amount, satoshi amount, status) directly in contract storage. **Mitigation:** Soroban's execution model guarantees that contract storage can only be modified by the contract itself within its own execution context. The VM provides full isolation. **Residual risk:** NONE - Soroban VM isolation is a fundamental security property. #### T3: Attacker modifies the SPV proof data in transit **Threat:** A man-in-the-middle attacker modifies the block headers or Merkle proof between the relayer and the user's submission to the Soroban contract. **Mitigation:** The user submits the proof directly to the Soroban contract (the relayer is a data source, not an intermediary). The contract cryptographically validates all provided data. Even if the relayer provides modified data, the contract will reject it. Users can self-serve from any public Bitcoin data source. **Residual risk:** LOW - the user can always bypass the relayer entirely. *** ### R - Repudiation (denying having performed an action) #### R1: Protocol denies co-signing a BTC release **Threat:** A user repays their USDC loan but the protocol denies the repayment occurred and refuses to co-sign the P2WSH release. **Mitigation:** * Repayment is recorded on the Stellar blockchain - fully public and immutable. The `repay_full` event is emitted at repayment time with the P2WSH scriptPubKey. * The user's CLTV timelock provides a backstop: if the protocol refuses to co-sign, the user waits for the timelock to expire and recovers their BTC unilaterally. * The timelock is set at loan origination block + loan duration + 7-day safety buffer - if the worst happens, the user is never permanently locked out. **Residual risk:** LOW - the timelock is the cryptographic guarantee that eliminates protocol repudiation risk. #### R2: Keeper denies initiating a liquidation **Threat:** The keeper claims to have initiated a liquidation that didn't occur, or denies initiating one that did. **Mitigation:** The `liquidate()` call and its result are recorded on Stellar's ledger. The `liquidate` event includes the keeper's address, the position txid, and the debt amount. All on-chain, all immutable. **Residual risk:** NONE for on-chain record. Note: `writz-lend` uses a designated keeper, so keeper reliability is a concern - mitigated by the stale-keeper fallback: after 24 hours with no keeper liquidation or heartbeat, any caller may liquidate an undercollateralized position. *** ### I - Information Disclosure (unauthorized access to private data) #### I1: On-chain position amounts are public **Threat:** `writz-lend` positions are plaintext: position amounts, debt sizes, health ratios, the depositor's Stellar address and the Bitcoin deposit are visible on-chain. This exposes users to front-running and competitive analysis, as in any public lending market. **Mitigation:** None in v1, by design. Writz v1 has no privacy layer, and this is disclosed in the user documentation. Privacy is being designed as a separate, opt-in edition with its own contracts, pool, audit and trusted-setup ceremony (see `docs/how-it-works/privacy-edition.md`); it is in progress, testnet only, not on the v1 mainnet plan and has no date. **Residual risk:** HIGH for users who need confidential positions; accepted for v1. ### E - Elevation of Privilege #### E1: Attacker triggers liquidation on a healthy position **Threat:** An attacker calls `liquidate()` on a position that is actually healthy, clearing the owner's position against their will. **Mitigation:** * `liquidate()` performs an on-chain collateral ratio check before proceeding: `health_ratio = collateral_value / debt`. If health ≥ liquidation threshold (120%), the call reverts with `PositionHealthy`. * Only the authorized keeper address can call `liquidate()` while the keeper is active. After 24 hours of keeper inactivity anyone may call it, but the on-chain ratio check above still applies to every caller. * `liquidate()` never moves BTC: the liquidator pays the USDC debt and receives a USDC bonus from the reserve. **Residual risk:** LOW (on-chain ratio check, keeper access control while active). The oracle price remains the main input to watch (S3). #### E2: Attacker uses an undisclosed reentrancy exploit to drain the USDC pool **Threat:** A malicious USDC token contract or cross-contract call enables reentrancy, allowing multiple withdrawals from a single repayment. **Mitigation:** * Soroban does not support EVM-style reentrancy: all contract calls are sequential within a transaction and state changes are applied atomically. * The `repay()` function updates internal state (debt, status) before calling `token.transfer()` - a checks-effects-interactions pattern. **Residual risk:** VERY LOW - Soroban's execution model makes traditional reentrancy much harder than EVM. #### E3: Attacker exploits integer overflow in the interest rate model **Threat:** An attacker crafts a deposit or borrow scenario that causes integer overflow in the kinked interest rate calculation or the collateral ratio check. **Mitigation:** * All financial math uses Rust's `i128` (128-bit signed integer), providing sufficient range for all realistic BTC and USDC amounts. * `saturating_mul` and `saturating_add` are used throughout the rate model - overflow silently caps rather than wrapping. * The Soroban compilation profile has `overflow-checks = true` in release mode - integer overflow panics rather than wrapping. * Unit tests cover the rate model at boundary values (U=0%, U=75%, U=100%, U>100%). **Residual risk:** LOW - tested at boundaries, saturating arithmetic, and compiler overflow checks. *** ## Residual risk summary | Threat | Severity | Mitigation state | Residual | | - | - | - | - | | S1: Fake SPV proof | Critical | Bitcoin PoW | NONE | | S2: Protocol key compromise | Critical | User key required on every path; 2-of-3 protocol side planned | MEDIUM (single key), LOW (2-of-3) | | S3: Oracle price manipulation | High | Reflector with staleness check; second source with deviation guard planned | LOW | | T1: P2WSH script tampering | Critical | SHA256 commitment | NONE | | T2: Storage tampering | Critical | Soroban VM isolation | NONE | | T3: Proof data in transit | Medium | User self-service | LOW | | R1: Protocol repudiation | High | CLTV timelock backstop | LOW | | R2: Keeper repudiation | Low | On-chain immutability | NONE | | I1: Public position amounts | High | None in v1 (by design); separate privacy edition in progress | HIGH (accepted for v1) | | I2: Relayer data collection | Medium | Self-service available | MEDIUM (P1), LOW (P2) | | D1: Relayer DoS | Low | Redundant relayers and a tip freshness rule planned (the header light client needs headers submitted continuously); the user's time-lock exit is unaffected | LOW | | D2: Compute spam | Low | Soroban fee model | LOW | | D3: Key unavailability | High | CLTV timelock backstop | LOW (users), HIGH (ops) | | E1: False liquidation | High | On-chain check + keeper ACL | LOW | | E2: Reentrancy | High | Soroban model + CEI pattern | VERY LOW | | E3: Integer overflow | High | i128 + saturating + overflow-checks | LOW | *** ## Security priorities for audit The external audit before mainnet should focus on, in priority order: 1. **SPV verification correctness** - is the SHA256d implementation correct? Does header chain validation correctly prevent header substitution attacks? 2. **Interest rate model overflow** - are there edge cases in the kinked rate formula that could cause incorrect interest calculation at extreme values? 3. **Cross-contract call trust** - can a malicious contract be registered as the SPV contract to bypass deposit verification? 4. **Protocol key security** - the 2-of-3 protocol signers, their published signing policy, and key recovery procedures 5. **ZK circuit soundness** - not part of v1; the privacy edition's circuits get their own audit before that edition launches *** *Last updated: June 2026 - v0.1 draft. Full revision planned before the external audit.*